EffinDOM: https://effindom.dev/
Rust UI Framework - FUI-RS: https://github.com/zion-sati/fui-rs
FUI-RS Live Demo: https://fui-rs-demo.effindom.dev/
Ever since WebAssembly (WASM) was just a JavaScrpt file, I've already dreamt of the death of HTML, CSS and JavaScript itself.
Death. Gone from the face of the earth.
Okay, that's a bit much - but the dream was it should never have been the de facto stack of Web Apps.
HTML, CSS and JavaScript won. There are millions of developers, libraries for everything, answers on forums and AI trained against existing them.
But ask around and you'll quickly find all good developers hate them. These are three historical accidents that turned into a badly mashed up application model. The world is now tightly coupled with these accidents, making replacing them extremely difficult.
You'll find bad UX every time HTML quietly gives up. A workflow graph is a good example; so is a tmeline, waveform editor, diagram surface, node editor, etc. Let alone a combination of them on the same page. The page itself remains HTML. The difficult bits that HTML can't do becomes canvas, SVG, WebGL or a stupid pile of absolutely position divs...
Sure, we see the pixels appear and at first glance it looks great. Until someone tries to use it.
Does the mouse wheel scroll the page, pan the surface, or zoom it? Does trackpad pinch mean browser page zoom or graph zoom? What happens when a drag leaves the surface? Which layer owns the pointer capture? What about keyboard? Does keyboard focus enter it and tab across its inner elements? Can a screen reader understand them? Does its hover state look the same as the one next to it?
The custom control provider decides and every provider invents a different answer. Sure, you might be thinking that they'd have all sorts of knobs for you to fine tune all of these. But that simply leads to a combinatorial explosion, and prohibits unified design systems.
The custom control surface has become a second UI system embedded inside the first one.
This is why beautiful and sophisticated HTML5 controls often feel so wrong. The control simply has no knowledge of the application - mouse handling, wheel momentum, pinch-to-zoom, pull-to-refresh, touch arbitration, focus, selection, keyboard shortcuts, context menus, accessibility, gestures, etc etc etc are rebuilt inside an island. That is if they go as far as that to begin with.
Even if they do. You could argue, sure, it works. But so does a lot of tech debt.
The hack inside a hack
HTML is excellent at documents (do we need to be reminded what DOM stands for? No, not the latex kinda DOM... the document kind). It was also pretty good for building conventional forms (also a document). Somewhere along the way, we turned it into a general application platform because it was already everywhere, not because a document tree, a cascade and a scripting language somehow formed a brilliant UI architecture.
When the document model runs out of vocabulary, the supposedly "clean" way is to insert a custom rendering surface and rebuild what's missing in JavaScript. However, that creates two owners for the same interaction:
- The browser owns page scrolling, page zoom, focus traversal, text input, text rendering, accessibility and platform conventions.
- The embedded surface owns its own hit testing, gesture state, inner scrolling, inner page zoom, inner focus traversal, inner text input, inner text rendering, inner... you get where this is going... But the point is, everything "inner" is often just an approximation of the browser's look and feel.
The boundary is where applications become uncanny. GitHub Actions workflow diagram is a good example - it's so bad no matter how long you spent using it, it still feels uncanny. Wheen input is captured when the user meant to scroll. Pinch changes the wrong scale, drag gets stuck because the release happened elsewhere. Keyboard commands work until focus crosses into the island. The graph is themed, but separately so - chances are they'd be sutbly different.
None of this is impossible to fix. Pointer events, touch actions, ARIA, DOM overlays and a crazy amount of JavaScript can reproduce almost anything.
BUT. Once a custom widget has implemented all of that fully polished, it has built a small UI framework. The next control builds another one, often by another provider. Good luck convincing them what the middle wheel button's meant to do...
A canvas is not a UI framework
So I hear you say - what if the entire page is one enormous canvas? It doesn't solve anything - a canvas gives us pixels. It doesn't give us retained control identity, layout, focus, text editing, IME, selection, clipboard behavior, password-manager integration, browser find, semantics, accessibility, touch conventions, or platform-aware shortcuts.
This is where most canvas UI arguments go off the rails. A canvas is a framebuffer, not an application platform.
EffinDOM exists to provide the missing application model. Canvas is simply one presentation surface used by the browser host.
The runtime owns one retained tree for layout, controls, rendering, input, text, selection, scrolling and semantics - yes that's like exactly what the browser currently does. Except the bet is browsers will no longer have to do this in the future.
We then project the parts that the host platform must own. In the browser, that includes a semantic DOM, mirrored editor state, IME integration and browser-facing behavior. On native desktop, it includes the OS accessbility and input adapters.
Your retained application
│
┌─────────┴─────────┐
▼ ▼
FUI-RS FUI-AS
Rust application AssemblyScript web
SDK SDK
└─────────┬─────────┘
▼
EffinDOM
layout | text | input | semantics
╱ ╲
▼ ▼
Browser/WASM Native desktop
canvas + host Metal | D3D12 | Vulkan
The normal application path would share most of the UI code. Genuine platform-specific capabilities would remain available behind clean boundaries.
Why start with the browser?
Most custom-rendered UI frameworks start natively and add a WASM target later.
That makes complete sense. Native is easier.
You own the window. You own the event loop. You own rendering, input, clipboard, accessibility adapters and file access. There isn't a browser sitting around your application with thirty years of behaviors users expect you to preserve.
I started with the browser precisely because it is harder.
Drawing some buttons onto a canvas is easy. Drawing them while pretending text selection, IME, password managers, browser navigation, pull-to-refresh, trackpad gestures, accessibility, keyboard focus and find-on-page don't exist is even easier.
But that's not a web application. That's a screenshot that responds to mouse clicks.
For EffinDOM to be credible on the web, it had to participate in browser behavior:
- Keyboard, mouse and touch text selection.
- IME composition and CJK input.
- Clipboard operations and platform-specific shortcuts.
- Password-manager and form integration.
- ARIA and semantic inspection.
- Browser and retained find-on-page.
- Routing and independently loaded WASM pages.
- Async fonts and fallback.
- Page zoom, trackpad pinch and nested scrolling.
And yes, a lot of these are the boring things nobody puts in the first demo.
They are also the difference between a demo and a UI framework.
Once those worked, taking the same retained model to macOS, Windows and Linux was still a lot of engineering work.
Metal, D3D12 and Vulkan did not wire themselves up while I slept.
But architecturally, the direction was much cleaner.
The runtime already knew what a button meant. It already knew what text selection meant. It already knew about focus, scrolling, semantics, editing and accessibility. The native hosts only had to connect that meaning to another platform (I used SDL here which gave me a leg up on unifying the 3 major platforms).
EffinDOM isn't a native UI framework that happens to compile to canvas. It was born in the browser.
The hybrid route is still a hybrid
Cross-platform UI reminds me of the car industry.
Electron and WebView applications are conventional hybrids. They put a native body around a browser drivetrain.
It works. It is incredibly practical. It also means every application drags a browser around just so we can pretend a website became a desktop application. That's your average Slack, GitKraken, VSCode etc. * cough give me back my RAM * Those things are more expensive than gold bars by weight these days!
There is now a cleverer middle ground: remove the WebView and render an HTML-oriented application model natively.
That's a pretty substantial engineering achievement. But it's still a hybrid - the equivalent of a plug-in hybrid.
The browser engine may be gone, but the application still begins with concepts designed for the web, and written for devs who are now close to their fifties (I'm one of them).
EffinDOM and FUI are the EV bet.
I'm not trying to work out how to render HTML somewhere else. I'm asking what should replace it.
Of course, HTML has had three decades to install petrol stations on every corner. Component libraries, browser tooling, integrations, hosting, documentation, experienced developers etc.
FUI has a working drivetrain. Its charging network is young. The built-in controls, browser and desktop hosts, packaging, documentation, Agent Skills and access to the Rust ecosystem cover quite a lot. The third-party control ecosystem does not have thirty years behind it.
Early adopters may occasionally have to plan the route themselves.
That's the bet though. Useful applications create demand for more controls and tooling. Better tooling makes the next application cheaper. Eventually, if enough people use it, the ecosystem develops gravity of its own.
The control that nearly broke the dream
You might assume the hardest control was a graph, custom drawn surface or the interactive dancing yarn demo.
Nope.
It was the boring multiline text box everybody assumes the operating system simply gives you: TextArea. But remember, we are bypassing the browser altogether here.
Text editing exposes every shortcut a UI framework is tempted to take.
Monospaced fonts can use specialized paths. Fixed line heights can be optimized differently to variable line heights (yes unicode chars have different heights encoded by their fonts). Non-wrapping text is different from wrapping text. Static text is different from editable text. Large documents should process only visible lines, otherwise the app crawls to a standstill. Incremental edits should not reshape the entire document.
Then all of those optimizations combine.
Every combination must still preserve Unicode boundaries, caret geometry, forward and reverse selection, scrolling, hit testing, tabs, newlines, clipboard operations, undo, accessibility and font fallback.
On the web, EffinDOM renders the visible editor while keeping a mirrored DOM editor for IME and browser integration.
Text inputs also need the correct semantics for password-manager extensions.
Why retained FUI?
I didn't want another framework that repeatedly rebuilds a description of the UI so it can compare that description with the UI it already had (ok fine, I hate react).
FUI uses retained controls.
Construct the control once. Keep its identity. Change the property that changed.
Focus, selection, editor state, scroll position, subscriptions and overlays remain attached to the actual control the application created.
FUI-RS exposes that model to Rust. FUI-AS exposes the same controls and fluent conventions to AssemblyScript.
The syntax belongs to each language. The application model underneath does not change.
The shared EffinDOM runtime is written in C++ deliberately. Rendering, layout, text shaping, input and semantics are infrastructure for FUI-RS, FUI-AS and future language SDKs such as FUI-KT (Kotlin) and FUI-CS (C#).
A narrow ABI (Application Binary Interface) lets the runtime work with Skia, Yoga, HarfBuzz, ICU, SDL and native platform APIs without reimplementing the engine for every language.
FUI-RS is the idiomatic Rust application surface over a language-neutral runtime.
Is any of this real yet?
Yes.
EffinDOM currently runs in browsers through WASM (WebAssembly) and natively on:
- macOS ARM64 and x64 using Metal.
- Windows ARM64 and x64 using D3D12.
- Linux ARM64 and x64 using Vulkan.
No Electron, no system WebView for native apps.
FUI-RS covers browser and native desktop. FUI-AS currently targets the browser (with plans for native desktop in the future).
A universal routed FUI-RS application can compile each browser route into an independently loaded WASM MFE (micro-frontend) while composing the same retained page crates into one native application.
cargo-fui creates, builds and packages the application. It produces DMG, MSIX and AppImage packages. Native-only Rust developers do not need Node.js or npm.
The current uncached browser transfer (cold load) is roughly 7.6 MB compressed:
- EffinDOM core: about 1.9 MB.
- UI runtime: about 810 KB.
- ICU data: about 2.4 MB.
- Six built-in fonts: about 2 MB total.
- FUI-RS home route: about 321 KB.
- FUI-AS home route: about 122 KB.
Most of that is shared infrastructure rather than application code. Within the same top-level site, routes and FUI language SDKs reuse the content-addressed runtime and fixed assets.
A warm load of the FUI-RS demo home page transfers under 500Kb.
What doesn't exist yet?
This is feature-rich early access, not 1.0.
The APIs can still break if my work porting it to Android and iOS deemed it necessary. The third-party control ecosystem is young.
There is one independent community application so far: Galaga-RS, a space shooter with a live browser build.
The point
The web keeps proving applications need more than documents.
Our answer has been to place increasingly sophisticated private UI frameworks inside HTML and try to patch the differences one control at a time. In the world of AI we live in currently, that's a pretty good way to burn tokens.
I refuse to believe another layer of JavaScript is the best the human civilisation can do.
EffinDOM is my attempt to replace the application UI model without throwing away the browser and OS integration users expect.
FUI-RS is the flagship, FUI-AS is the AssemblyScript SDK, and FUI-KT and FUI-CS are early smoke-level language surfaces.
The project is far enough along to build real applications, but early enough that somebody using it can still influence where it goes.
Try to build something that isn't already in the showcase.
Then tell me where it breaks.


Top comments (0)