pro/front-end framework/in development, source not yet published
HTM/A Server-side reactivity, in four kilobytes Four and a half kilobytes, and no npm in sight
HTM/A is a front-end framework in which the server holds the state and answers with HTML, and the browser only has to put it where it belongs. Behaviour is declared where the markup is, in attributes on ordinary elements, and nothing arrives as JSON for the browser to assemble. There is nothing to compile, nothing to install and no dependency tree. What follows is one argument in seven steps, and the comparison at the end is with frameworks that are named.
Runs in
- Any browser
- No build step
Built with
- JavaScript
- Rust
- Apache-2.0
- Kind
- Front-end framework, server-rendered
- Weight
- 4.5 KB Brotli, 4.9 KB gzip, the whole of it
- Source
- 1,276 lines
- Dependencies
- None
- Build step
- None. The file is the file
- Surface
- Custom elements and
htma-attributes on ordinary HTML - Transport
- Polling, Server-Sent Events or WebSocket, by one attribute
- Pattern
- EASL: event up, actor, stream of HTML down, in a line
- Accessibility
- WCAG 2.2 AA, native elements before ARIA
- Server
- A runtime server, or a compile-time macro in Rust
- Actors
- One file each: typed state, template and actions together
- State
- Isolated per actor, passed as messages, never shared
- Alongside
- Compatible with minline for templating and translations, neither required
- Tests
- 126 passing
- Licence
- Apache-2.0
Startbefore the argument
Before the argument, the facts
In development, and the source is not yet published, so every figure here rests on trust until it is. What is measured is measured: 4.5 kilobytes compressed with Brotli for the entire framework, 1,276 lines of source, no dependencies at all, and 126 tests passing. The comparison further down names the frameworks it compares against and their sizes, which is the only sort of comparison worth printing.
The argument, in seven steps
- OneThe markup is the programmeAttributes on ordinary elements. No JSX, nothing to compile.
- TwoThe server sends HTMLTwo steps where the usual arrangement takes five.
- ThreeThe flow is a lineEvent up, HTML down. No state to synchronise on both sides.
- FourOne attribute changes the transportPolling, then Server-Sent Events, then WebSocket. No refactor.
- FiveAccessible because it is ordinaryA real button before a div with a role bolted on.
- SixWhat it weighs4.5 KB against a typical bundle a hundred times that.
- SevenTwo ways to run the serverA runtime that needs no Rust, or a macro that is typed at compile time.
Then, for reference
01the markup is the programme
You already know where the button is
The behaviour of a page is written where the page is: on the elements themselves, as attributes, in a file a browser can open without help. There is no separate component language, no transform between what you wrote and what runs, and consequently nothing to install before the first line works.
<htma-endpoint url="/counter" io="websocket">
<h1>Count: <htma-content name="value">0</htma-content></h1>
<button htma-bind="increment">+</button>
</htma-endpoint>
That is a working counter over a live socket, and it is also just HTML. The element says
where to talk and how; htma-content marks the part that will be replaced;
htma-bind says which event goes back. A colleague who has never seen the
framework can read it, which is a property most of the alternatives gave up somewhere around
the second build tool.
The rest of the surface is the same shape: htma-action,
htma-link, htma-modal, htma-sortable. Forms, tabs,
modals, drag and drop, toasts and infinite scroll are all in the four and a half
kilobytes, and none of them arrives as a separate package.
Which raises the obvious question: if the browser is not assembling the page, who is?
02the server sends html
Two steps, where the usual arrangement takes five
The customary route from a change on the server to a pixel on the screen goes: the server sends JSON, the client parses it, the client builds a virtual tree, the client compares that tree to the last one, and the client applies the difference to the real document. Five steps, of which four are the client rebuilding something the server already knew.
The server sends HTML
The browser displays it
Browsers are, after all, exceptionally good at rendering HTML. They have had thirty years of practice and a great deal of money spent on it. Handing that job to a library that has to be downloaded first is a decision, and it is worth occasionally asking what it bought.
Fewer steps is pleasant. What does it change about the shape of the whole thing?
03the flow is a line
Event up, HTML down, and no circle to debug
The pattern has a name in the project, EASL, which stands for event, actor, stream and lined. The first three are the parts and the fourth is the point.
Event the reader clicks, types, scrolls
Actor the server takes it and updates its own state
Stream the server sends back fragments of HTML
Lined in one direction, with nothing pointing back
A single-page application holds state on both sides of the wire and spends a good part of its life keeping the two copies honest. State changes cause renders, renders cause effects, effects change state, and somewhere in that circle sits the afternoon you lost to working out why something rendered twice. There is no second copy here, so there is no synchronisation, no hydration mismatch, and no question of which side was right.
A line is easy to reason about. Is it also easy to make fast?
04one attribute changes the transport
Start by polling. Finish on a socket. Change one word
The transport is an attribute, and the three you would want are all in the same four and a half kilobytes.
<htma-endpoint url="/api" io="fetch" poll="5000"> works anywhere at all
<htma-endpoint url="/api" io="sse"> one direction, efficient
<htma-endpoint url="/api" io="websocket"> both directions, live
Same interface, same features, same markup around it. A prototype that polls every five seconds becomes a production page on a socket by editing one attribute, and nothing else in the document changes. The usual version of that sentence involves a migration and a fortnight.
That ladder is also the relationship with HTMX, which does the first rung well and stops there. Start on plain HTTP as you would with HTMX, and move up when the application asks for it rather than when a rewrite allows it.
All of which is worth little if the result is awkward for the people using it.
05accessible because it is ordinary
A real button, before a div wearing a role
Accessibility here is mostly a consequence rather than a feature. When behaviour is an
attribute on a real <button>, the button is already focusable, already
announces itself, already responds to the space bar and already appears in the tab order.
None of that had to be reimplemented, so none of it can be reimplemented incorrectly.
The project's own rule is native elements before ARIA: reach for the element that already means the thing, and add attributes only where the platform has no element for it. WCAG 2.2 at level AA is the target, and the parts that can be arranged automatically are arranged automatically rather than left as an exercise.
This is the quiet advantage of not rebuilding the document. A framework that constructs its own widgets inherits the whole of accessibility as homework; one that decorates the browser's widgets inherits it as behaviour.
So what does the whole of that weigh?
06what it weighs
Four and a half kilobytes, for the lot
4.9 KB with gzip rather than Brotli. That is the framework entire: forms, modals, tabs, sockets, drag and drop and the rest, not a core that then downloads the interesting parts.
For scale, and the project makes this point better than a chart could: in 1999 the demoscene fitted animated three-dimensional worlds with music into four kilobytes. The argument is not that everybody should write like that. It is that half a megabyte to render a toggle deserves at least a raised eyebrow.
Which leaves the half of the arrangement that is not in the browser at all.
07two ways to run the server
One needs no Rust at all, the other is typed at compile time
The client half is the same either way. The server half comes in two shapes, and which one suits depends on what you are prepared to compile.
Reads the actor files and serves them. No Rust required at all
The same actors as Rust, checked by the compiler before they run
An actor is a single file that holds one endpoint's behaviour, and it is worth seeing whole, because the whole of it is short:
<htma-actor name="Counter">
{% let value: i32 = 0 %}
<htma-template>
<h1></h1>
<button htma-action="decrement">−</button>
<button htma-action="increment">+</button>
</htma-template>
{% action increment sync %} {% value += 1 %} {% endaction %}
{% action decrement sync %} {% value -= 1 %} {% endaction %}
</htma-actor>
State, markup and the actions that change it, in one file and in front of you. The state
is typed, i32 rather than whatever arrives, and under the compile-time macro
that type is checked before anything runs. Each actor keeps its own state and receives
messages; nothing is shared and mutable across them, which is the arrangement that removes
a whole family of question about who wrote what last.
Keeping all three in one file rather than spread across a route table, a controller and a template is the same instinct as the rest of this: the thing you are working on should be in front of you.
Numbersthe comparison in full
Five frameworks, and what they weigh
| Framework | Bundle |
|---|---|
| HTM/A | 4.5 KB |
| HTMX | 14 KB |
| Vue | 33 KB |
| React | 45 KB |
| A typical React application, with its ecosystem | about 150 KB |
Svelte belongs in that list and resists a single figure: it compiles away, so its runtime is around two kilobytes and the output grows with the application rather than sitting at a fixed size. Comparing it by bundle alone would flatter one of us and it is not obvious which.
What the weight buys on a phone: under a second to interactive on a three-G connection against three to eight, and under two megabytes of memory against ten to fifty. Those are the project's own figures and they are the reason any of this matters, because the reader in Lagos or Mumbai on a slow connection is the one who leaves.
Edgeswhere it stops
Where it stops, and what that leaves you
- It wants a server. Every update comes from one, so an application that must keep working on a train through a tunnel is asking for something else. A progressive web app framework is the right tool there and this is not it.
- Complex client-side state machines belong elsewhere. A drawing tool, a spreadsheet, an editor with its own undo history: those keep genuine state in the browser, and a framework built on not doing that will fight you. React and Vue earn their weight in exactly those applications.
- Animation is CSS's job here. That is a deliberate omission and part of why the figure is four and a half kilobytes rather than forty.
- In development, and the source is not published. The 126 tests pass and the sizes are measured, and both become checkable by anybody the day it ships.
- The component ecosystem is small, because it is new. A framework three weeks old has no marketplace, and pretending otherwise would be the first dishonest sentence on this page.
One thing it does not have to do alone. minline handles templating, translations and preferences on the client, and the two are compatible by design rather than by dependency: HTM/A brings the state and the transport, minline brings the small language for putting values into a page. Neither needs the other and neither assumes the other, which is why they can be picked up one at a time. The old HTM/A site was built with minline, which is a recommendation of the pairing and not a requirement of it.
The project's own summary is the fair one and worth repeating: for the great many web applications that are, at bottom, forms and lists talking to a server, four and a half kilobytes is probably enough.