REALM is dead, long live REALM
20 October 2026 · Ribhararnus Pracutian
In June 2025 I put REALM into maintenance mode and told anyone starting a new project to use something else. REALM is now back as v2, a rewrite. This post covers why v1 stopped, what changed, and how to start.
Why v1 stopped
The maintenance post gave three reasons:
- Shadow DOM and the Web Component APIs kept changing across browsers. Keeping v1 working everywhere took more time than one person had.
- Good component libraries had appeared, with communities behind them.
- I no longer used REALM in anything I shipped, so I got no feedback from actual use.
There was a fourth reason I did not write down. v1 was built quickly and had no test suite. A later audit found 22 confirmed bugs, including the one that broke Unicons when a page imported many icons.
Why v2 exists
Two things changed.
First, the platform settled. The APIs v2 depends on, such as ElementInternals, custom states (:state()), adoptedStyleSheets and AbortSignal, now behave the same in Chromium, Firefox and WebKit. v2 runs its full test suite in all three browsers on every change, so I find out when that stops being true.
Second, REALM's original goal still holds. You can build a working app with a browser, a text editor, HTML files and one script tag from a CDN. There is no npm, bundler or compiler. Most front-end tools assume a build step before anything shows on screen. REALM does not.
The design also suits code assistants. A REALM component is plain HTML with a short script, and each feature uses the platform's own names: <slot>, onclick, fetch, action, method. There are no template expressions or custom syntax to learn, so language models can read and write REALM code with few mistakes. The site supports this:
- llms.txt lists every page, and llms-full.txt holds the whole documentation in one file.
- Every page has a Markdown copy. Add
.mdto the address, or use the "View as Markdown" link at the bottom of each page. - The runtime's behaviour is written down as specifications. Every scenario in them is a test with the same name, so the specs and the code stay in sync.
What changed from v1
One tag, three parts
In v1, everything was a tag. Data lived in <element-state>, changes were <set-state>, clicks were <trigger-event> inside <element-flow>, requests were <http-request> with <response-ok> and <response-fail>, and markup sat inside a <template>. Simple components were pleasant to write, but anything unusual needed deep trees of tags.
In v2, a component is one <custom-element> with a <script>, a <style> and markup:
<custom-element name="counter-button">
<script type="realm">
const counter = useState({ count: 0 });
function increment() {
counter.count++;
}
</script>
<style type="realm">
button { font-weight: bold; }
</style>
<button onclick="increment()">Clicked <slot name="#counter.count"></slot> times</button>
</custom-element>
<counter-button></counter-button>The script is plain JavaScript with a few named helpers: defineProps for typed attributes, useState for state, observe for side effects. REALM builds each component in the browser.
Native slots and handlers
- Values are shown with native
<slot name="#…">and<slot name="@…">elements. - Events are native
on*attributes, such asonclick="save(item)", that call functions from the script. - Attributes are bound with v1's
_prefix:_href="#link.url". <is-visible>,<is-hidden>and<repeat-list>are still there. Lists take akey, so focus and typed text survive reordering, and therepeatattribute works inside tables and selects.
Requests as attributes
fetch, into, render, busy, trigger, poll and a few others go on the element that starts the request. Forms use their native action and method. Stale requests are cancelled, so an older, slower response can no longer overwrite a newer one. Results fire the browser's own load and error events. See Fetching data.
import-element
<import-element from="…"> loads an HTML file and defines every component in it. In v2 each URL is fetched once, however many tags ask for it, and defining a component twice does no harm. This fixes the bug that broke Unicons. See Importing components.
Other changes
- Fine-grained updates. A change updates only the slots and bindings that read it.
- Typed props.
prop.number().int().min(0).default(0). Invalid values warn and fall back instead of crashing. - Form fields.
form-associatedcomponents submit, reset, validate and restore like built-in inputs. - Accessibility. Default roles,
delegates-focus, and arrow-key navigation throughfocusgroup. - Styling from state.
:host(:state(attr-variant-is-outline)),var(--attr-*)andvar(--state-*)replace v1's regex-based CSS rewriting. - Stores.
defineStoreshares state between components, with persistence that keeps value types. - Cleanup. Every listener, timer, observer and request a component starts stops when the component is removed.
- Security. No
eval,new Functionorblob:URLs. HTML values are sanitised, and nonce-based Content Security Policies work.
REALM UI
@realmorg/ui is a set of about sixty accessible ui-* components, such as buttons, dialogs, menus, tabs and data tables. They are written with REALM, and this website uses them. Browse them in Components.
Unicons v2
Unicons is rebuilt on REALM v2. Instead of one file per icon, a single <u-icon> element renders any of about 50,000 icons in 31 sets, and each set loads once, only when a page uses it.
How to start
Add two lines to any HTML page:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>Then write a <custom-element> in the page, or put components in their own files and load them with <import-element>. Pin an exact version in production.
- Quick start
- Tutorial
- Examples
- Migrating from v1, with a tag-by-tag cheat sheet
v2 is in alpha. Bug reports and feedback are welcome on GitHub.