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
The HTM/A mark: the slash from its wordmark, blocky, with a cross at its centre, extruded
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

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 route here
one

The server sends HTML

two

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

measured4.5 KBBrotli, the whole framework
measured1,276Lines of source
measured0Dependencies
measured126Tests passing

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.

Pick one
the runtime server

Reads the actor files and serves them. No Rust required at all

the compile-time macro

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

Bundle sizes of five front-end frameworks
FrameworkBundle
HTM/A4.5 KB
HTMX14 KB
Vue33 KB
React45 KB
A typical React application, with its ecosystemabout 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.