Reactive HTML Without JavaScript Frameworks

How RxHTML Eliminates React, Vue, and Angular

The entire JavaScript ecosystem is garbage, and I say this as someone who built a JavaScript streaming system that was a key part of earning a senior principal promotion at a large tech company. I chose JavaScript out of spite because I knew it would be successful. The language itself is fine -- it is a serviceable glue language. The ecosystem around it is a dumpster fire of churn, supply chain attacks, breaking upgrades, and abandonware.

Every few years a new "next generation" framework appears, and the entire industry relearns the same lessons about component models, state management, and rendering. I experienced enough pain with Docusaurus that I gave up entirely. The time investment felt wasted because I was one upgrade away from an evening of despair.

So I asked a different question: what if the backend is reactive, and the client is just a stateless function of server data plus local view state? What if you could build a full application with HTML templates and zero framework code?

That question led me to build RxHTML.

The Core Insight: Reactivity Belongs in the Infrastructure

The entire front-end engineering game is turning data into HTML and capturing interactions back into data. That is it. All the buzzwords and framework churn miss this fundamental point. If your backend already provides reactive data -- where changes flow automatically to connected clients as minimal JSON diffs -- then the front-end framework becomes unnecessary.

RxHTML is an HTML templating system that compiles to JavaScript. You write templates using standard HTML with a few custom attributes and elements. The compiler turns them into a JavaScript bundle (site.js) and an HTML shell (200.html) with a routing table. Upload those files with the libadama.js runtime and you have a single-page application.

No npm install. No node_modules. No webpack. No babel. No package-lock.json with 847 transitive dependencies.

RxHTML Compilation Pipeline site.rx.html Your templates (HTML + rx: attributes) RxHTML Compiler Parse HTML -> Type Check -> Generate JS + Routing site.js DOM construction code 200.html SPA shell Routing Table URI -> page mapping Browser libadama.js + site.js + style.css WebSocket to Adama backend

Runtime: libadama.js provides three modules

connection.js (WebSocket) tree.js (reactive data trees) rxhtml.js (routing + DOM)

The Forest Structure

RxHTML uses a <forest> as its root element. Inside the forest, you define pages (routable views), templates (reusable components), and an optional <shell> element that configures the generated HTML's <head> content (title, meta tags, stylesheets, scripts):

<forest>
  <page uri="/">
    <h1>Welcome</h1>
  </page>

  <page uri="/chat/$key:string">
    <connection space="chat" key="{view:key}">
      <div rx:iterate="lines">
        <lookup path="who" />: <lookup path="what" /><br />
      </div>
      <form rx:action="send:say">
        <input type="text" name="what" />
        <button type="submit">Say</button>
      </form>
    </connection>
  </page>
</forest>

This compiles to JavaScript that uses the RxHTML runtime for page routing ($.PG), DOM construction, and data binding. The generated code for a simple "Hello World" page looks like:

(function($){
  $.PG(['fixed',''], function(b,a) {
    b.append($.T(' Hello World '));
  });
})(RxHTML);

The variables (b, a) are generated short names for minification. b is the DOM parent (body), a is the page context. $.PG registers a page with routing instructions -- ['fixed',''] means "match the root path exactly."

The Two-Tree Model

This is the part that matters most. RxHTML maintains two separate reactive data trees:

View State (client-owned). Holds UI state: selected tabs, expanded sections, search filters, URI parameters. Accessed with the view: prefix. This is pure client-side state that never touches the server unless explicitly synced.

Data State (server-owned). Arrives over the WebSocket from the Adama document. Holds document fields, table data, and bubble results computed for the current viewer. Accessed directly or with the data: prefix.

Two-Tree Binding Model View State (Client) tab: "settings" search: "hello" page: 3 modal_open: true

Updated by rx:click, rx:sync, URI params Synced to Adama via @viewer

Data State (Server) title: "My App" users: [{...}] filtered: [{...}] count: 42

Arrives over WebSocket as JSON diffs Bubbles react to @viewer changes

DOM Nodes Each node bound to tree values via functional closures Updates propagate directly: tree change -> node update (no diffing) @viewer sync

The binding between trees and DOM nodes uses functional closures. When the compiler generates code like $.L($.pV(a),'name'), it creates something equivalent to:

function() {
  var node = document.createTextNode("");
  viewstate.subscribe({name: function(name) {
    node.nodeValue = name;
  }});
  return node;
}

The closure captures the DOM node directly. When the tree value changes, the subscriber fires and updates that specific node. No virtual DOM diffing. No reconciliation pass. No component re-renders. The information flow goes from tree change to DOM node with zero intermediate steps.

This is the historical origin of RxHTML. I was building products with both vanilla JS and React, and the repetitive work of binding data changes to DOM changes was maddening. The closure-based approach is not novel -- it is how every spreadsheet works -- but applying it to HTML templating eliminates the entire category of problems that React, Vue, and Angular exist to solve.

How Lists Work

When a field is a list, the $.IT function handles the DOM:

$.IT(container, treeNode, 'lines', false, function(itemNode) {
  var div = $.E('div');
  div.append($.L(itemNode, 'who'));
  div.append($.L(itemNode, 'what'));
  return div;
});

The function argument is the instruction for how to build a DOM element for a new list item. When items appear, the function is called and the DOM element is inserted. When items disappear, their DOM element is removed. When ordering changes, DOM elements are reordered. All of this is driven by the JSON diffs arriving from the server -- the server sends {"@o": [...]} ordering instructions, and the list manager translates them directly into DOM operations.

Forms, Actions, and the Read-Write Loop

Forms in RxHTML send messages directly to Adama channels:

<form rx:action="send:say">
  <input type="text" name="what" />
  <button type="submit">Say</button>
</form>

The compiler generates $.aSD(form, connection, 'say') which hooks the form's submit event to collect all input values into a JSON object and send it to the say channel on the current connection. The channel handler runs on the server, mutates document state, which produces a JSON diff, which flows back over the WebSocket, which updates the data tree, which fires the closures, which updates the DOM.

That entire round-trip -- form submit to visible UI update -- happens with zero JavaScript written by the developer. The feedback loop is: user interacts -> view state or server message -> tree updates -> DOM updates.

For client-side interactivity, RxHTML has a command language on events:

<button rx:click="toggle:show_details">Toggle Details</button>
<button rx:click="set:tab='settings'">Settings Tab</button>
<button rx:click="inc:counter">+1</button>

These modify view state directly. Combined with rx:if for conditional rendering and rx:sync for input-to-view-state binding, you can build surprisingly complex client-side interactions without writing JavaScript.

What This Cannot Do

RxHTML is not a general-purpose JavaScript replacement. It specifically solves the problem of building data-driven UIs connected to a reactive backend. Here is what it does not handle:

Complex client-side logic. If you need sophisticated local computations, animations, or canvas rendering, RxHTML's command language (toggle, set, inc, dec) is insufficient. The escape hatch is Web Components -- I wrapped CodeMirror 6 into a Web Component and it works fine inside RxHTML templates.

SEO for dynamic content. RxHTML's client-side pages produce a single-page application. Search engines that do not execute JavaScript will not see your dynamic content. For content that needs SEO, RxHTML provides <server-page> elements that render on the server and return complete HTML. Server pages support authentication and remote data fetching via <remote-inline>, but they are not reactive -- they are traditional request-response pages. For applications behind a login, SEO is irrelevant.

Offline-first applications. RxHTML depends on a WebSocket connection to the Adama backend. If the connection drops, the UI shows a disconnected state. There is no local persistence or offline queue.

Third-party JavaScript integration. If your workflow depends on npm packages for charting, rich text editing, or other complex widgets, you need to wrap them as Web Components. This works but requires JavaScript knowledge for the wrapper.

The tradeoff is clear: RxHTML gives you zero-framework reactive UIs at the cost of being coupled to the Adama backend and limited to what declarative templates can express. For the 80% of application UI that is "show data, accept input, show updated data," it is dramatically simpler than React. For the 20% that requires complex client-side behavior, you use Web Components or write vanilla JS.

I am currently building the entire Adama web portal with RxHTML, and it works well. I spend my time on the domain problem rather than framework glue. I use Tailwind for styling, RxHTML for data binding, and Web Components for the rare cases where I need real client-side logic. The result is fast, stable, and has zero transitive npm dependencies.