HTMX and Web Components Instead of React

First published at Wednesday, 17 June 2026

HTMX and Web Components Instead of React

I work as a software architect at FernUniversität in Hagen on LEAD:FUH, a learning-analytics data platform handling highly sensitive student data. This post completes the frontend story the CSS post started: That one was about styling, this one is about why there is no frontend framework underneath it.

Start from the shape of the application

Choosing a frontend stack before looking at the application is how React becomes a default instead of a decision. So, inventory first. The frontends on a data platform are a metadata catalog, operations dashboards, privacy reports, and detail pages: Search something, browse a list, open a detail view, expand a data preview, follow lineage. Every one of these interactions is naturally a request followed by a response, rendered as a page or a fragment of one.

This shape matters because the single biggest cost of the SPA model is that it duplicates the server. State lives twice, once in the database and once in the client store. Rendering logic lives twice, as templates on the server and components on the client. And between the two halves you must now build and version an API which exists only to feed your own frontend. For an application which is genuinely client-state-heavy, like a collaborative editor or a design tool, this duplication buys responsiveness you cannot get any other way, and it is worth every line. For a catalog with a search box it is pure overhead.

So instead of asking "why not React?" we asked: Which of our actual problems would React solve? We went through the list. There were none.

HTMX: the server stays the source of truth

What the request/response-shaped majority of the platform needs is hypermedia with less full-page reloading, and that is precisely the niche HTMX [1] fills. The server renders HTML and HTMX swaps fragments of it into the page. There is no client-side state store, because the state is where it always was: in the database, projected through a template.

Two conventions carry most of the design:

  • One canonical URL, content-negotiated. A search like /catalog?q=… returns either the full page or just the result-cards fragment, depending on whether HTMX made the request. There are no separate /partials/… routes to keep in sync with the real ones. The URL surface stays small, and every fragment is also a bookmarkable page.

  • The URL carries the state. Search terms and filters live in the query string, not in a JavaScript store. Back/forward works. Copying the URL shares the exact view. Nobody implemented "deep linking" as a feature, it is simply what URLs do when you do not take it away from them.

None of this is new. It is how the web worked before we collectively decided to reimplement the browser inside the browser. The contribution of HTMX is removing the last excuse for abandoning that model: full-page reloads.

Web components: islands for the genuinely interactive

A real application is never purely request/response. Ours has sortable tables, dialogs, tabs, live status indicators, a relative-time display, a line chart, and, as the most demanding one, an interactive lineage graph. This is where the client-side logic genuinely lives, and where we use vanilla web components: standard HTMLElement subclasses on a shared base class of twenty lines, served as plain ES modules. No build step, no virtual DOM, no framework runtime [2] .

Two things surprised me about how far this carries. First, the ceiling is much higher than the folklore suggests: The lineage graph with pan, zoom, node selection, and dynamic layout is a web component like any other, and at no point did it hit a wall which a framework would have moved. Second, the component count stays low, around twenty for the whole platform, because most of what SPA codebases express as components is just server-rendered HTML with CSS in our stack. A component has to earn its place by having actual behaviour.

The two layers compose cleanly: HTMX swaps a fragment in, the browser upgrades any custom elements in it, and the components attach their behaviour. There is no hydration step and there are no "client boundary" annotations. The component model of the platform is the browser's.

The accessibility dividend

There is a quieter benefit to making components an explicit opt-in, and it took me a while to name it: Every component starts with the question "does a semantic element already exist for this?", and remarkably often the answer is yes. A native <dialog> brings focus trapping, the Escape key, and correct semantics. A <details> element is a disclosure widget with keyboard support built in. A real <button> is focusable, announces itself to a screen reader, and activates on both Enter and Space. A <table> is navigable by assistive technology in ways a grid of <div>s never will be.

You can write semantic, accessible HTML in React as well, and good teams do. But the component-first model puts the burden on remembering: Everything is a component, so nothing forces the pause in which you ask whether the platform already solved this. The result is the framework ecosystem's signature failure mode, the <div onClick> which is invisible to keyboards and screen readers. Not out of malice, but because nothing in the workflow ever asked.

Our inversion makes that pause structural. Semantic HTML is the default and a custom element is an exception which has to be argued for. And when a component is justified, it wraps and enhances native elements instead of replacing them: The sortable table is still a <table>, the dialog component still renders a <dialog>. Here is the essence of our dialog component, condensed from the real one:

export class LeadDialog extends LeadElement { static tagName = "lead-dialog"; static props = ["label"]; connectedCallback() { const dialog = document.createElement("dialog"); if (this.id) { dialog.setAttribute("aria-labelledby", this.id + "-title"); } // …move the slotted children inside, render a header with // the label and an aria-labelled close button… this.appendChild(dialog); this._dialog = dialog; super.connectedCallback(); } open() { this._dialog.showModal(); } close() { this._dialog.close(); } }

Notice what the component does not implement: open() is one call to showModal(). The focus trap, the Escape key, the ::backdrop, and the inert page behind the dialog all come from the browser. The component's contribution is composition and labeling. The accessibility heavy lifting was already done by people who do nothing else. Compare this with the modal implementations in any component library, which re-create exactly these behaviours in userland, each with its own bugs.

For a university platform accessibility is a legal obligation, and the cheapest accessibility work is the work the browser has already done.

What we gave up

This is not a free lunch, and pretending otherwise would undersell the decision. We gave up the React ecosystem with its thousand ready-made date-pickers, virtualized grids, and drag-and-drop libraries. When we need such a thing, we build a smaller version of exactly what we need, or we go without. We gave up optimistic UI updates. Every mutation is a round trip, which our users on a university network do not notice, but yours on mobile might. And we gave up the enormous React hiring pool and its training material, which is a real consideration for a team which recruits.

If we were building collaborative editing, offline-first mobile, or anything where the client legitimately owns complex state, this would be a different post. Frameworks are specialized tools. Their specialty, rich client-side state, is something most applications, and very nearly all internal platform frontends, simply do not have.

The ten-year argument

The deciding constraint is operational: This platform is built to be handed over and operated long after the project which built it ends. That horizon changes the framework calculus completely.

A React codebase from ten years ago is a migration project today: class components, deprecated lifecycles, a build toolchain which no longer runs. The frontend stack of this platform is HTML over HTTP, CSS variables, and standard custom elements. The only runtime is the browser, and browser vendors have a quarter-century record of not breaking it. HTMX is the one dependency in the chain, and it is a small, replaceable one, more a convention over fetch and innerHTML than a platform we are married to.

A future maintainer needs to know HTML, CSS, HTTP, and JavaScript. Those are skills every web developer already has and a university can hire for. For code which must outlive its builders, betting on the web platform is the only bet with a track record.

Summary

We looked at the shape of our applications first, and that shape is request/response. HTMX keeps the server the single source of truth for the hypermedia majority, around twenty vanilla web components cover the genuinely interactive islands, and semantic HTML as the default gives us most of the accessibility work for free. The trade-offs are real. But for a platform which must be operated for a decade by people who did not build it, web standards are the safer bet than any framework generation.

References

1
Gross, C., Stepinski, A. & Akşimşek, D. (2023). Hypermedia Systems. hypermedia.systems — the conceptual foundation behind HTMX: hypermedia as the application engine, with the server as the source of truth.
2
Miller, J. (2020). "Islands Architecture." jasonformat.com/islands-architecture — server-rendered pages with isolated interactive islands; we implement the islands as standard custom elements.

Subscribe to updates

There are multiple ways to stay updated with new posts on my blog: