Elements HTML
elements man html Read as markdownElements 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.ehtmlimport { 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.