Manual Elements HTML

Elements HTML

elements man html Read as markdown

Elements HTML is the HTML you and your agent already know, with a lightweight syntax extension: TypeScript expressions in attributes and in the body, and templates that take typed attributes the way a function takes parameters, public or private. It is reactive and dynamic: change the data and the page updates. It runs on the server and in the browser automatically, from one file, with nothing to configure. The runtime is extremely fast, and does not rely on virtual DOM diffing. This is not JSX: not a custom language rendered through a tree diffing algorithm, with the performance problems that brings, and which in many cases does not render on the server by default. Agents write it flawlessly, because it is standard HTML. Converting to and from Elements HTML is easy.

A page
<html (title: string, private count: number = 0)> <h1>{title}</h1> <button onclick={() => count++}>Clicked {count} times</button> </html>

That is a complete page. The route passes title, the template owns count, the expression in the body prints it, and the click updates it in place.

Templates

A template is a top-level tag whose name starts with a capital letter. It can take a constructor: attributes declared in () on the opening tag, typed like function parameters, each one public (the default) or private. A template with no attributes needs no constructor. A default makes an attribute optional, and an initializer can read other attributes. Callers set public attributes the way they set any HTML attribute. A private attribute is state the template owns: callers cannot set it, and the template can change it.

Attributes
<Greet ( name: string, greeting: string = `Hello ${name}`, private open: boolean = false, )> <p onclick={() => open = !open}>{greeting}</p> </Greet> <Greet name="Ada" />

A page is a template named <html>, the default export of its .ehtml file. Any other top-level tag is a named export, imported like any TypeScript export, so templates scale from one file to many. Attributes are fully typed and the codebase's tooling (rename, find-references, go-to-definition) works across templates the same way it works across TypeScript.

Reactivity

If the data changes, any UI that depends on it updates. The runtime knows which data each binding reads and patches that one text node, attribute or element, with no virtual DOM to rebuild and compare. A list is keyed, so a row that moved is moved, not re-rendered. Reactivity crosses template boundaries: a child mutating an attribute it received from a parent updates the parent, with no plumbing.

Lists, conditions and classes
<li e:for={user of users}>{user.name}</li> <p e:if={users.length === 0}>no users yet</p> <p e:else>{users.length} users</p> <div class={["card", active && "is-active"]}>...</div>

A class array drops its falsy entries, so is-active comes and goes with active, and nothing else on the element is touched.

The page's attributes are live in the browser console as $. Assign to one and the page updates through the same path the template uses, so you can jump to any state without clicking through the UI or rebuilding (reactivity).

Browser console
$.title = "new title"; // re-renders the title $.users = []; // shows "no users yet"

Form bindings

Input bindings are two-way by default. value, checked and group keep the bound variable and the control in step with no event wiring: editing the input updates the variable, and updating the variable updates the input.

Bindings
<input value={form.name}> <input type="checkbox" checked={form.subscribed}>

Server and browser

Every page renders on the server, so the first response is the complete HTML: search engines and first-time visitors get the finished page with no bundle to download before the first paint. The runtime then attaches to that HTML in the browser, and from there the page is reactive: event handlers run in the browser, bindings are live, and changes patch the DOM in place. The server never renders an update and never sends HTML after the first response. When the page asks the server for something (an @rpc call, a channel message, a LiveTable row), the reply is data, and the browser renders it.

The Elements compiler type checks an .ehtml file with the rest of your TypeScript: same type checker, same build graph, same LSP. An attribute typed User is checked everywhere it flows, and a typo in a template is a build error, not a blank page. The compiler emits the JavaScript for the server and the browser and ships the runtime alongside. Elements HTML files end in .ehtml; a .html file is plain HTML.

At a Glance

template.ehtml
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.

The file and the page tag

An .ehtml file is a mix of templates and TypeScript code. A template is a top-level tag whose name starts with a capital letter, which is what distinguishes it from a native HTML element and is required rather than a convention. The <html> tag is the one exception: it 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.

Because the page tag is a real <html>, attributes written on it land on the document element. <html class="page-chat"> puts page-chat on <html>, so use it to scope custom properties and theming and put layout on an element inside the page. elements create page writes that page- prefix so the page class stays out of the namespace component classes live in: a bare .chat rule written for a widget somewhere else in the app would match <html> and restyle the whole document. A page gets type, colour and spacing by default and no width at all; see base and add .page-shell to a <main> when you want a measured, centred column.