It was a brownbag session. My colleagues were presenting MobX -- how it was superior to Redux, how it finally solved the state management problem. And my mind wandered.
Not because it wasn't interesting. But because I kept taking steps back.
Past React. Past the virtual DOM. Past JavaScript. All the way back to Delphi -- which if you used it, you know was Win32 done right. A beautiful retained-mode API sitting on top of a system layer, where you described what you wanted and the runtime figured out how to render it. No reconciler. No hooks rules. No "why did this re-render."
I pulled a colleague aside afterwards. Told him I thought we'd built the entire web application platform on the wrong foundation, and that someone should fix it properly.
He told me it was too hard. Impossible. Swimming against the tide.
Eight years of failing
I want to be honest about what "eight years" actually means here, because it wasn't eight years of solid focused work. It was eight years of dabbling, failing, giving up, then being pulled back in by another brownbag or another frustrated frontend conversation.
I've always called myself a backend engineer. I'd jump into frontend when I had to, and every time I did I'd come back with the same quiet frustration. Not because the DOM is bad -- it's genuinely excellent at what it was designed for. HTML is a document model. For content sites, blogs, marketing pages, e-commerce -- it's the right tool, and React and its ecosystem serve that use case well.
The problem is that we started building applications with it. Trading dashboards. Design tools. Kiosks. Data-heavy enterprise UIs. The kind of thing you'd build in WPF or SwiftUI -- retained-mode, stateful, performance-critical. And for that category, a document model is the wrong foundation. Some developers have never known anything else -- they were born into a world where React was already the default, where the reconciler mental model was just "how the web works." Like the Matrix: they can't see the document model underneath because they've never been outside it.
For those of us who remember Delphi, WPF, Qt, SwiftUI -- the mismatch has always been visible.
The moment that hit hardest was watching Blazor fail.
I had real hope for Blazor. Microsoft, serious engineering investment, the promise of writing C# for the web. And it failed -- not catastrophically, but it never became what it could have been. I stopped thinking about this problem entirely for a while after that. Years, actually.
But watching Blazor fail also taught me something. Every canvas framework that tried to target the web had the same fundamental problem. Flutter Web, egui, Blazor, Compose -- all ports, not designs. They were built somewhere else and shoved into the browser. They lost what the web actually offers -- the CDN, the browser cache, the URL distribution model -- and they fought the browser on everything else.
The insight, when it finally crystallized: the only way to do this right is to design for the browser from day one. Not port to it.
What I actually built
EffinDom is a POSIX-style display server for WebAssembly UI. The browser is our Win32. fui-as -- the AssemblyScript SDK -- is our Delphi.
The demo home page -- 100,000-item virtual list on the left, controls on the right. Three routed pages, all running in a canvas with zero DOM UI.
The architecture is a strict three-tier stack:
Tier 1 -- Core (effindom-core.wasm): A stateless C++ WebGL microkernel. Raw drawing instructions. Knows nothing about UI or text. Dumb, fast, memory-safe.
Tier 2 -- UI (effindom-ui.wasm): The retained-mode runtime. Yoga flexbox layout. HarfBuzz + ICU text shaping. Input routing, semantics projection, focus management.
Tier 3 -- FUI-AS: The app-facing SDK. AssemblyScript -- TypeScript-style, compiles to WebAssembly. SwiftUI-inspired fluent API. No HTML. No CSS. No virtual DOM diffing.
A minimal app looks like this:
import { Button, SelectionArea, Text } from "./Fui";
class HelloWorld {
private count: i32 = 0;
private readonly label: Text;
constructor() {
this.label = new Text("Clicked 0 times");
}
buildPage(): SelectionArea {
return new SelectionArea()
.fillWidth()
.fillHeight()
.child(
new Button("Click me").onClickWith<HelloWorld>(this, (owner) => {
owner.count += 1;
owner.label.text(
"Clicked " + owner.count.toString() + " time" +
(owner.count == 1 ? "" : "s"),
);
}),
)
.child(this.label);
}
}
export function createHelloWorldPage(): SelectionArea {
return new HelloWorld().buildPage();
}
No JSX. No hooks. No useEffect. A retained scene graph that does what you mean.
The thing that makes it actually web-native
Every other canvas framework I looked at had the same payload problem. Flutter Web bundles the Skia engine, the Dart runtime, and your app into one monolithic blob. egui statically links its own text shaping and GPU rasterizer. Visit two Flutter apps, download the engine twice.
EffinDom's answer is the Web DLL model.
Tier 1 and Tier 2 are immutable, content-hashed WebAssembly modules served from a CDN. Once a browser downloads them, they're cached forever -- shared across every EffinDom app the user visits. The ICU data, the fonts, the HarfBuzz shaper: all cached, all shared.
Your app payload is just your business logic. Under 100 KB over the wire with Brotli -- even for feature-heavy apps. Your app payload is just your business logic.
This is what web-native actually means -- architecture designed for the browser's actual physics, not retrofitted onto them.
From a web native feel point of view, you have to play around with the demo -- a huge amount of effort have been put into this.
Accessibility wasn't an afterthought -- it was the architecture
Most canvas frameworks get accessibility wrong for the same reason: they design the rendering pipeline first and try to retrofit ARIA on top. Screen reader support becomes a feature request, not a foundation.
I built EffinDom the other way around. ARIA was a first-class architectural requirement before the first control was written. The browser bridge, the coordinate syncing, the real-time semantic tree -- these aren't features added later. They're load-bearing parts of how EffinDom works.
The result: a full ARIA-compliant hidden DOM projected behind the canvas, perfectly synced to GPU coordinates. Every time the layout changes, the semantic tree updates. VoiceOver reads the mirror; the user sees the canvas. They're in sync.
Every control ships with semantic roles and accessible labels baked into the constructor. Not bolted on. Not optional. Built in.
Try this on macOS
- Open the live demo
- Turn on VoiceOver (⌘F5)
- Press ⌃⌥A (Control + Option + A) to read all
VoiceOver highlights canvas-rendered text with the native macOS ring as it reads -- pixel-perfectly synced to the actual GPU coordinates of what's on screen. The native macOS VoiceOver highlight, on canvas text, because EffinDom gives the semantic tree real coordinates.
Tab navigation works too -- that's a separate focus ring baked into the framework itself. Two independent accessibility systems, both working natively, on a canvas app with no HTML inputs anywhere.
Checkbox state changes announce in real time. Slider values are read out. The semantic tree updates as the UI updates -- VoiceOver always reads what the user actually sees.
Flutter Web still can't do this properly. Most canvas frameworks treat accessibility as out of scope. EffinDom treats it as infrastructure.
And since I'm showing off -- the theme system works properly too. This is the same app in light mode on the Advanced page:
Lock-step OS theme interpolation -- frame-by-frame color transitions synced to system theme shifts. Same app, different theme.
What it took
I started writing GUI code at 14 in 1992 -- selling an Editbox control for $1,000, manually programming VGA sequencer registers to switch between four 64 KB memory banks mid-draw. VGA's memory aperture was only 64 KB wide but 640×480×256 colors is 300 KB of pixel data. You had to program the sequencer to switch banks mid-operation, mid-line sometimes. Get it wrong and the screen turned to glitch-art. Get it right and you had a GUI.
That's the kind of problem EffinDom is built on. Thirty years of knowing what good software feels like at the hardware level.
I'll also say this honestly: WPF had a team of hundreds at Microsoft. SwiftUI had a team of hundreds at Apple. I'm one engineer building this at night while my kids sleep. Recent AI tooling is what made that gap closable -- not the architecture or the hard-won lessons, those were earned, but the grunt work that would have taken a team now takes one person. I think that's genuinely one of the most exciting things about where we are right now.
Where it is today
Here's the Text & Fonts page -- rich text with inline styling, custom font stacks, emoji, and live TextArea configuration:
Real HarfBuzz text shaping. Not bitmap fonts. Not CSS tricks. Inline styled spans, emoji, custom font stacks -- all rendered natively on canvas.
The SDK ships today with:
- Full control set: Button, Text, TextInput, TextArea, Checkbox, RadioButton, Dialog, VirtualList, RichText, ContextMenu and more
- Yoga flexbox layout with SwiftUI-style fluent API
- HarfBuzz + ICU text shaping, RTL/BiDi, surgical font subset injection
- Transitions and animations
- Drag and drop, external file drop
- Context-aware right-click menus, built-in Ctrl+F / ⌘F find engine
- Automatic IndexedDB state persistence, seamless back/forward navigation
- HTTP networking (GET, POST, PUT, DELETE) baked into the runtime
- Adaptive 4-flavor WASM compilation (wasm64+SIMD, wasm64, wasm32+SIMD, wasm32)
- npx scaffolder -- running in under 2 minutes
Is EffinDom right for you?
Use EffinDom if you're building: dashboards, editors, design tools, kiosks, data visualizations, trading UIs, simulators, enterprise web apps or anything that behaves more like a desktop application than a webpage.
Use HTML/React/Vue if you're building: content sites, blogs, marketing pages, e-commerce stores, anything where the document model is a natural fit.
This isn't a religious war. The DOM is the right tool for documents. EffinDom is the right tool for applications. Both can coexist -- they serve fundamentally different purposes.
Zero third-party runtime dependencies. Zero vulnerabilities.
Run npm install on a scaffolded fui-as app. Check the output:
found 0 vulnerabilities
The only runtime dependencies are EffinDom's own packages. No third-party code runs in the browser. Not a few dependencies -- none.
A typical React app has hundreds of transitive dependencies. Every one is a potential supply chain attack vector. Log4Shell, XZ Utils, the Polyfill.io compromise -- enterprises and banks have entire security teams whose job is auditing that graph. Some organizations can't ship to production without a full dependency audit that takes weeks.
EffinDom's answer is: there is no third-party runtime dependency graph to audit. The only code that runs in the browser is yours and EffinDom's own hermetically bundled, content-hashed packages.
The runtime is hermetically bundled inside the npm package. No external CDN calls at build time. No transitive packages. The WASM binaries are content-hashed -- you know cryptographically exactly what you're running. Air-gapped build environments work out of the box.
Your app is just your code. That's the entire attack surface.
Honest alpha status (v0.1.x)
Core rendering, the full control set, layout, theming, routing, accessibility, networking, and state persistence are all solid and heavily tested. No handle leaks -- incremental GC is running.
Mobile: the demo is responsive across screen sizes. Touch input, fling scrolling, and pull-to-refresh work. Find-in-page works on mobile too -- it draws a text overlay above the canvas rather than highlighting canvas text directly, which is a known limitation of the approach. Pinch-to-zoom and long press aren't there yet.
Layout edge cases: conflicting constraint trees produce undefined behavior. Two sibling columns both calling .fillWidth(), for example -- both asking to fill the parent's width, no defined winner. The framework won't crash but the result isn't guaranteed. Avoid ambiguous constraint trees and you'll be fine.
This is early alpha. The foundation is solid. The edges are still being polished.
Try it
npx @effindomv2/create-fui-as-app my-app
cd my-app
npm install
npm run dev
→ Live demo (pinch-to-zoom and long press not yet supported)
→ fui-as SDK (AssemblyScript)
→ (under construction) fui-kt SDK (Kotlin)
→ (under construction) fui-rs SDK (Rust)
→ EffinDOM runtime
On licensing and sustainability
The runtime is MIT -- use it freely forever. The fui-as/fui-kt/fui-rs SDKs are dual-licensed: AGPL for open source use, commercial license for proprietary products.
I'm a solo maintainer with a young family building this after hours. AGPL means open source use is genuinely free, and commercial use funds the project's survival. If you're at a company building something commercial on top of this and AGPL doesn't fit, that commercial license is how this stays alive long term. If you know someone who should be talking to me, reach out: zionsatidev@gmail.com
The web deserves better infrastructure than a 1995 document viewer. I've spent eight years and a lot of failed experiments believing that. I think I've finally built something worth believing in.




Top comments (0)