Skip to content
F
GitHubLight theme

Template compiler for HTML

Modern template DX for HTML, without a framework.

Write @if, @forand @switch in plain HTML. A Rust compiler turns your template into a render(context) function, or into HTML directly. Angular-style authoring, without Angular, and with nothing to adopt around it.

  • HTML output
  • Rust compiler
  • Server-first
  • Optional browser tools
users.flowflow
<ul class="user-list">
  @for (user of context.users; track user.id) {
    <li [class.active]="user.active">
      {{ user.name }}

      <button type="button" (click)="select(user.id, $el)">
        Select
      </button>
    </li>
  } @empty {
    <li>No users</li>
  }
</ul>

<script data-flowview>
  function select(id, element) {
    const list = element.closest("ul");
    for (const item of list.querySelectorAll("li")) {
      item.classList.toggle("active", item.contains(element));
    }
    list.setAttribute("data-selected", String(id));
  }
</script>

Rendered by FlowView

live
  • Ada Lovelace
  • Grace Hopper

Where it fits

One template language, four places to use it

FlowView compiles templates. The host decides when to render, where to serve and how to navigate.

Astro

Embed a <template flowview> region in any.astro file, or import a standalone.flow file. HTML is produced at build or request time. Add FlowView Events for (click) bindings.

astro
<template flowview={context} is:raw>
  @if (context.user) {
    <h1>Hello {{ context.user.name }}</h1>
  }
</template>

Vite and server HTML

With @flowview/vite, a.flow file becomesexport function render(context). Your server decides when to call it, which makes Hono, Node.js, Cloudflare Workers and other Vite-built servers a natural fit.

Honots
import { render } from "./users.flow";

app.get("/users", async (c) => {
  return c.html(render({ users: await getUsers() }));
});

Native Rust

compile_static() parses once and returns an immutableCompiledStaticTemplate; render it with as many contexts as you like. No JavaScript engine is involved, so templates can only use a small deterministic expression subset: property paths, literals, comparisons and boolean operators.

rust
use flowview_compiler::{compile_static, StaticCompileOptions};

let template = compile_static(source, StaticCompileOptions::default())?;

for page in &pages {
    let context = serde_json::to_value(page)?;
    let html = template.render(&context)?;
    // write html ...
}

Small browser UIs

Compose @flowview/dom,@flowview/reactive and Events when you want one. FlowView does not make templates reactive. Your code owns the state, and an expliciteffect() calls view.update().

ts
import { createView } from "@flowview/dom";
import { effect, signal } from "@flowview/reactive";
import { render } from "./items.flow";

const state = signal({ loading: false, items: [] });
const view = createView("#app", render);

// The one explicit boundary: state -> view.
effect(() => {
  view.update(state());
});

Compiler

One parser, two render targets

The same .flow source and the same AST feed a JavaScript backend and a native static HTML backend, and both follow one specification.

JavaScript target

For Astro, Vite, Hono, Node and Workers, and for browser code after a build. Source maps point errors back to your.flow file.

Native static target

For Rust tools, documentation generators, reports and server-side Rust applications. Structured diagnostics carry a code, line and column.

FlowView is not a static site generator. Routing, Markdown and deployment stay with your host.

Escaped by default, raw on purpose

{{ value }} is escaped. {{{ value }}} inserts trusted HTML exactly as given and is never sanitized. Templates are trusted source.

Read the syntax guide →

Events and bindings

Native events and honest bindings

Events are a separate, optional compiler. Bindings are plain template syntax. Neither needs a component model.

Events

(click), (input), (change),(submit) and other native events compile to one delegated listener per event type, so DOM you render later keeps working. The Rust HTML compiler never sees (click).

Eventshtml
<button (click)="save()">Save</button>

<input (input)="search($event)" />

<select (change)="change($event)">
  <option value="a">A</option>
  <option value="b">B</option>
</select>

<form (submit)="submit($event)">
  <button>Send</button>
</form>

<button (click)="remove('item-1', $el)">Remove</button>

<script data-flowview>
  function save() {}
  function search(event) { /* event.target.value */ }
  function change(event) {}
  function submit(event) { event.preventDefault(); }
  function remove(id, element) {}
</script>
See Events live →

Bindings

Boolean attributes, attr.* and class.*bindings, and full-value attribute interpolation. Generic DOM property binding is not supported.

flow
<button [disabled]="context.loading">Save</button>
flow
<div [class.active]="context.selected">...</div>
flow
<section [attr.aria-busy]="context.loading">...</section>
flow
<a href="{{ context.profileUrl }}">Profile</a>

Packages

Small packages you can pick from

Most projects need two or three. Nothing here is required just because it exists.

Optional state

State lives in your code. A signal changes, an effect runs, and view.update() replaces the target HTML with a fresh render. There is no virtual DOM and no automatic dependency tracking inside templates.

  1. state()your signal
  2. effect()re-runs when state changes
  3. view.update()explicit call
  4. FlowView HTMLreplaces the target
Which packages do I need? →

Why trust it

A compiler should be boring when it breaks

FlowView is pre-1.0, and syntax or generated output may still change before 1.0. What is in place today is checked, not just claimed.
  • Executable specificationAll 99 HTML requirements are enforced by named tests.
  • Fuzzed and property-testedThe parser faces adversarial input on a schedule.
  • Structured diagnosticsErrors carry a code, line and column. Source maps point back to your template.
  • Packed consumer testsThe npm tarballs are installed into fresh projects without Rust, before every release.
  • Declared compatibilityNode 22.12+, Vite 6, 7 and 8, Astro 5, 6 and 7, Prettier 3. Checked on Linux, macOS and Windows.
  • Browser acceptanceThis website is the acceptance app: every listed capability is exercised in a real browser.

Scope

What FlowView deliberately is not

Your host owns application architecture. FlowView owns template compilation.
  • Not a component framework
  • Not a router
  • Not a static site generator
  • Not a CMS
  • No dependency injection
  • No virtual DOM
  • No automatic hydration
  • Not an Angular compatibility layer