Rust GUI frameworks
2018 Web 2026: Foundational and universal
06

wasm-bindgen: Rust meets the DOM

The bridge that makes every Rust web UI framework possible.

wasm-bindgen generates the glue between Rust compiled to WebAssembly and JavaScript. web-sys builds on it to give Rust typed access to browser APIs: the DOM, canvas, fetch, WebGPU. It is the most depended-on crate in this book, with over 5,000 dependents, and nearly every web target here rests on it: Yew, Leptos, Sycamore, winit's web backend and wgpu's browser backend all list it.

wasm-bindgen-dom/src/bin/one.rs ✔ compiled · wasm-bindgen 0.2, web-sys 0.3 · rustc 1.95.0
use book_error::{Context, Error};
use wasm_bindgen::prelude::*;

// Runs automatically when the .wasm module is loaded in the page. A returned
// error becomes a JavaScript exception (see `From<Error> for JsValue`).
#[wasm_bindgen(start)]
fn run() -> Result<(), Error> {
    let document = web_sys::window()
        .context("no global `window` (not running in a browser?)")?
        .document()
        .context("window has no document")?;
    let p = document.create_element("p")?;
    p.set_text_content(Some("Hello from Rust!"));
    document.body().context("document has no <body>")?.append_child(&p)?;
    Ok(())
}

fn main() {}

Using a browser API takes only a few lines, and you must enable each web-sys type you use as a Cargo feature (here Window, Document, Element, HtmlElement). That keeps the compiled .wasm small, and it is why Rust web frameworks ask you to list features so carefully.

wasm-bindgen-dom/src/bin/two.rs ✔ compiled · wasm-bindgen 0.2, web-sys 0.3 · rustc 1.95.0
use wasm_bindgen::prelude::*;

// Call *into* JavaScript...
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(message: &str);
}

// ...and expose Rust *out* to it.
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    log(&format!("greeting {name}"));
    format!("Hello, {name}!")
}

fn main() {}

A returned error becomes a JavaScript exception, through the From<Error> for JsValue impl on the error-handling page, instead of a panic that aborts the module. The boundary runs both ways. extern "C" blocks import JavaScript functions into Rust and #[wasm_bindgen] on a function exports it out. Build tools like Trunk or wasm-pack run the wasm-bindgen step for you. Both snippets were compiled for wasm32-unknown-unknown. The crate is still 0.2.x, stable in practice and pre-1.0 on paper.

crates.io first publish.