this is an idea I had sketched out and wanted to write down in Zulip!
so in the past when we've talked in the past about Roc in the browser, there's been a baseline assumption of doing it as some sort of Virtual DOM system.
it occurred to me recently that we never actually talked about what a simpler platform would look like - something like "I want to use all the browser APIs, but I want to write Roc instead of JavaScript" and what that platform would look like.
in other words, just like how basic-webserver is just trying to do the most basic thing, and then something like nea is trying to do something fancier, it seems like a good idea to have a basic-dom and then there can be fancier alternatives that are doing things like abstracting the browser APIs more
I think the fundamental goals would be:
roc test, backed by a fake DOM and simulated functions (e.g. don't actually try to implement a whole layout system, but rather specify in the test what DOM functions like getBoundingClientRect() should return)I think it would also be fine to make some things more formalized, e.g. querySelector in JS takes a query string that uses CSS selectors which would need to be parsed, whereas query_selector! in Roc could take a Query value that you build up in a config-builder style.
DOM node property access would have to be done with effectful functions, e.g. in JS there's node.textContent but in Roc it would need to be:
Node.get_text_content! : Node => Str
Node.set_text_content! : Node, Str => Str
here's how defining a custom element in Roc could look:
CustomElem.define!(
"my-custom-element",
CustomElem
.class("MyCustomElement")
.constructor(|this| {
this.super!()
})
.connected_callback(|this| {
})
.disconnected_callback(|this| {
})
)
and here's how the wasm <-> JS boundary could look - basically converting object methods to plain functions that pass the object as the first argument: (these can now come across from JS to wasm as externref types, which modern browser support - basically they're an opaque pointer that wasm can't dereference, but rather can hold onto and pass back to JS - perfect for this use case)
const wasmDomApi = {
documentQuerySelector: (selector) => document.querySelector(selector),
nodeQuerySelector: (node, utf8selector) => node.querySelector(utf8Decoder.decode(utf8selector)),
getTextContent: (node) => node.textContent,
getFirstChild: (node) => node.firstChild,
getNextSibling: (node) => node.nextSibling,
// ...etc
};
WebAssembly.instantiateStreaming(fetch("roc_app.wasm"), {
env: wasmDomApi
});
anyway, something to think about!
Interesting idea. When I made a brief attempt at building a virtual DOM lib in Roc a couple of years ago, this is roughly where I drew the boundary - everything in Roc except calls to DOM APIs.
The main difference is that if you are writing a virtual DOM lib you only need like 10 or 20 actual DOM functions. But if you are making bindings to the DOM API, that's truly enormous. I know you are only picking a subset, but the trick is in deciding which subset, especially if there is no particular design in mind.
One annoying detail is that strings can't be passed across the Wasm boundary. You have to pass them as pointer and length, and convert to and from JS strings with TextEncoder and TextDecoder.
If you are using raw dom apis, I assume that means registering tons of callbacks. Will this work well with roc and a basic platform?
I'm not sure, maybe! :smile:
Oh yeah for callbacks, you can't pass them across the boundary from Wasm to JS. You have to build a system for that. For example you put each user-defined callback into some List or Dict and give it a numeric ID. Then you pass the ID over to JS instead. You create a JS closure that captures the ID, serializes the JS arguments to bytes, and passes it all back to Wasm. Then a Wasm a dispatch function finds the original user callback and calls it.
Problem: In the most common cases, the argument passed to the callback will be an EventTarget, which is unserializable! In particular it contains a DOM node, which forms a doubly-linked list, pointing at its parent _and_ at its children. Even if it was serializable it would be too big so you wouldn't want to serialize it.
One way to solve that is to make a system for specifying only certain fields to serialize and send back to Wasm. So you could say you want event.target.children[0].value (like jq syntax or some AST version of it). Then you arrange for your JS callback to dig out that value and return it to Wasm.
The other approach is to use the newer "reference types" feature in Wasm. Then Wasm can hold on to an opaque JS reference and call back out to JS again to manipulate it and access fields.
yeah I was thinking of externref (the new feature) - it's apparently widely supported now!
that's actually all what I wanted from roc for the web client. client is not only about dom and documents, there are still workers. it would be nice to have smth similar to wasm-bindgen from the rust world: a Web IDL based codegen with simple custom api wrapper builders
related: "When Is WebAssembly Going to Get DOM Support?" https://queue.acm.org/detail.cfm?id=3746174
Would be really interesting to see how this compares to javascript based frameworks and where potential drawbacks or benefits are.
Was there already some thinking about how "graphical" platforms could share the same "principles"? E.g. if one would build something like Ctrl-K / Command Menu, would we imagine an abstraction layer that could make the same work for desktop and web?
https://hacks.mozilla.org/2026/02/making-webassembly-a-first-class-language-on-the-web/
It would be nice to have a Roc implementation of WebAssembly Component Model!
I have a rough basic-dom platform I've was poking at, I was using it to test the interpreter and find bugs or validate our platform host boundary (particularly for wasm).
I haven't looked at it in a while, the reason I stopped pushing it along was that we really need much smaller object files i.e. dev or llvm backends for it to be usable in any way.
My plan was to revisit that at some point and share it in a minimal working state in case anyone wanted to explore optimizations like skipping the JS glie altogether etc.
Last updated: Jun 16 2026 at 16:19 UTC