--- title: "Pages and Templates" author: "@chris" author_url: https://elements.dev/u/chris published: 2026-10-02T15:11:56.281Z url: https://elements.dev/feed/01a0fd2b-fe1d-7fd0-9c72-42e26acf6efe kind: lesson format: article --- # Pages and Templates by [@chris](https://elements.dev/u/chris) ยท 2026-10-02 ## Description Real HTML with typed attributes, rendered complete on the server and live in the browser, with routes and caching built in. A page in Elements is a route that gathers data and a template that renders it. The template is real HTML, with typed attributes the route fills in. It arrives in the browser as a complete page, rendered on the server, and from that moment it is reactive: change the data and the page updates itself, down to the one element that changed. One file of HTML does both jobs. ## A page is a route and a template The route reads what the page needs and hands it to the template: ```typescript app/pages/channel/index.ts import { Request, Response, sql } from "@elements/app"; import { Channel } from "./models"; import { messages } from "#app/shared/services/chat"; import html from "./template"; export default function route(req: Request, res: Response) { let channel = sql(` select * from channels where id = ${req.params.id} `).firstOrThrow("no such channel"); return new html({ channel, messages: messages.view({ channelId: channel.id }) }); } ``` The template declares what it takes, right on its opening tag, and renders it: ```ehtml app/pages/channel/template.ehtml import { LiveView } from "@elements/app"; import { Channel, Message } from "./models"; function onSend(form: { text: string }, messages: LiveView) { messages.insert({ text: form.text }, () => form.text = ""); } , )>

#{channel.name}

  • {message.text}
  • , private form = { text: "" }, )>
    onSend(form, messages)}>
    ``` That is a live chat channel: the page renders with its messages, a new message appears for everyone watching, and the form clears itself after sending. ## Templates take typed attributes Every template declares its attributes on its opening tag, with types: ``. The route passes them with `new html({ ... })`, one template passes them to another as attributes, and the type checker checks every hand-off. Pass the wrong shape and the build goes red at that line. Rename a field and your editor renames it in every template that uses it. An attribute marked `private` is the template's own state, like `form` above, with a default value. There is no separate component class and no props interface to keep in sync. ## Real HTML with lightweight syntax extensions A template is the HTML you already write, plus a few lightweight syntax extensions: - `{...}` puts a TypeScript expression anywhere HTML takes a value: text, attributes, class lists. - `e:if`, `e:for` and `e:switch` show, repeat or choose elements. - A top-level tag with a capital letter, like ``, is a template you can use anywhere, with `` for the content placed inside it. That is the language. Templates are type checked like any `.ts` file, so a misspelled field is a build error with a line number, never a blank spot on the page. ## Reactive, with nothing to wire up Change the data and every part of the page that reads it updates on its own. The runtime patches the page at the smallest level the change needs, so a new message adds one row and leaves the rest of the page alone. Inputs bound with `value={form.text}` stay in step both ways: typing updates the data, and setting the data updates the input. And it crosses templates: a child that changes an attribute it was given updates the parent, with no events or callbacks in between. ## Reactive UI from the browser console Building a page means seeing it in every state it can be in: empty, full, loading, an error, a hundred rows. In Elements you see each one immediately. In the browser's developer console, `$` is the page's template attributes. Set any of them and the page updates automatically, because the UI is reactive. There is no reload, no code change and no clicking through the app to reach that state. ```js Browser console $.form.error = "Wrong password." ``` **Set a template attribute in the console, and the reactive UI updates:** ![Chrome with the developer console on the left, where $.form.error = "Wrong password." has been entered, and the Glowhollow sign-in page on the right now showing a Wrong password. error above the email and password fields](/learn/01a0fd2b-f0a8-7f1b-8e8e-2ce8ecf38900/01a0fd2b-fe1d-7fd0-9c72-42e26acf6efe/images/01a0fd2b-fedf-76a3-8ac8-084afa3743eb) ## Rendered on the server, live in the browser The first response is the complete page, rendered on the server, so visitors and search engines see everything at once, with nothing to wait for. Then the browser picks the page up and makes it reactive, from the same template, with no second version of the page to write. A route can send live data with the page, such as a LiveTable view or a channel, and the browser keeps receiving its updates after it loads. ## Data from the route and from the server A route gathers a page's data when it loads. An `@rpc` function gets more whenever the page needs it, such as when a form is sent, and you call it from a template like any other function. Both run on the server, both read the signed in user from `session`, and both are type checked at build time, so the data a template receives is the data the server sent. ## Route functions live next to their pages, wired up in the router Each page's route function lives in the page's own folder, beside its template, like `app/pages/channel/index.ts` above. Your app imports them and says which URL each one answers, so a URL goes exactly where you say, not wherever a folder happens to sit: ```typescript index.ts import home from "#app/pages/home"; import channel from "#app/pages/channel"; import admin from "#app/pages/admin"; app.route("/", home); app.route("/c/:id", channel); app.route("/admin/*", admin); ``` A larger part of your app, like `admin` here, can be a router with its own routes, mounted at a prefix. Patterns follow the web standard `URLPattern`, with named segments, wildcards and inline patterns like `:id(\d+)`. The same routes serve pages and API responses: return a template and it renders, return an object and it is sent as JSON. Send someone elsewhere with `redirect(url)`, which works the same in a route, a template and an `@rpc` function. ## Caching that does the right thing Every page is cached the right way by default, using the HTTP standard: - Every page carries an ETag made from its source, its data and the session, so a page that has not changed answers `304 Not Modified` and the browser keeps what it has. - Every script, stylesheet and image has a hash in its file name, so browsers and any CDN in between cache it until it changes. - Pages for a signed-in user stay private. There is nothing to configure and no cache to clear by hand. ## Styled from the first line Every app comes with a design system. A bare `