HTML
elements man html Read as markdownElements html is a reactive html language extension and runtime. It is standard
html, augmented with lightweight extensions for creating templates, declaring
template constructors, and embedding reactive expression blocks. The language
adds a few new attributes (e:if, e:switch, e:for, and group) and
automatic two-way binding on input elements. A reactive runtime attaches to the
server-rendered page and updates the DOM surgically as data changes.
Templates are the unit of composition. They scale from one to many: define them
in a single file or across multiple files and import them like any other
TypeScript export. Each template has a template constructor, a new syntax for
declaring typed attributes on the opening tag (<Greet (name: string)>).
Template constructors are an extension to TypeScript. Attributes are fully typed
and reactive, and the codebase's refactoring tools (rename, find-references,
go-to-definition) work across templates the same way they work across TypeScript
code. A page is a template named <html> exported by default from an html file.
Reactivity is simple: if the data changes, any UI that depends on that data updates automatically. The runtime patches the DOM at the smallest level required by every change. Reactivity crosses template boundaries: a child mutating an attribute received from a parent updates the parent without any plumbing.
Form input bindings are two-way by default. value, checked, and group keep
template state and DOM state synchronized without any extra event wiring.
Editing an input updates the bound variable; updating the bound variable updates
the input.
Elements html renders on the server and the browser by design. The initial response is the fully-rendered DOM, so search engines and first-time visitors get the complete page immediately, with no client-side bundle to download before the first render. After the initial render, the browser attaches and adds reactivity and event handling. A route can also return live data alongside the page, like channel listeners and LiveTables, and the browser keeps consuming those updates continuously after attach.
The additions are intentionally narrow: a TypeScript expression syntax ({...}
anywhere html accepts a value), attributed top-level tags as reusable
templates (<Greet (name: string)>...</Greet>), a small set of control-flow
directives prefixed e: (e:for, e:if, e:switch), and TypeScript colocated
with the template (imports, helpers, route handlers, @rpc functions, all in
the same file). That is the entire syntactic surface. Everything else lives in
the runtime and the compiler.
The compiler is built on top of the TypeScript tooling. Templates participate in
the same build graph as the rest of your code: same type checker, same
go-to-definition, same find-references, same LSP. An attribute typed User is
checked everywhere it flows. Type errors in template source surface at build
time.
An html file is a mix of templates and TypeScript code. A template is a
top-level tag with an identifier (CapitalCase by convention but not required, to
distinguish it from native html elements). The <html> tag is reserved for the
page and is the default export, automatically bundling its code and stylesheets
across the reachable dependency graph. Any other top-level tag is a named
export. TypeScript code goes anywhere outside a template declaration, like
imports, helpers, @rpc functions, and event handlers.
At a Glance
import { Layout } from "#app/shared/templates/layout";
import "./style.css";
interface Form {
name: string;
age: number;
}
/** @rpc */
function saveUser(form: Form): User {
return sql<User>(`insert into users (name, age) values (${form.name}, ${form.age}) returning *`).firstOrThrow("insert returned no row");
}
function onSubmit(form: Form, resetUI: () => void) {
resetUI();
saveUser(form);
}
<html (title: string, users: User[], private form: Form = { name: "", age: 0 })>
<Layout>
<h1>{title}</h1>
<ul>
<li e:for={user of users}>
{user.name} ({user.age})
</li>
</ul>
<form onsubmit={() => onSubmit(form, () => form = { name: "", age: 0 })}>
<input value={form.name} focus={true}>
<input type="number" value={form.age}>
<button>add</button>
</form>
<p e:if={users.length === 0}>no users yet</p>
<p e:else>{users.length} users</p>
</Layout>
</html>
The page renders fully on the server and attaches in the browser. The form is
two-way bound, submit calls an @rpc function, and the list updates reactively
whenever users changes.