DOM & Web APIs (web-sys)
Access the DOM and browser APIs from Rust via web-sys and js-sys.
Recipe
use wasm_bindgen::prelude::*;
use web_sys::{console, Document, Window};
#[wasm_bindgen]
pub fn log_message(msg: &str) {
console::log_1(&JsValue::from_str(msg));
}When to reach for this: WASM modules that must manipulate the DOM, fetch URLs, or use Canvas/WebGL directly.
Working Example
use wasm_bindgen::prelude::*;
use web_sys::{Document, HtmlElement, Window};
fn window() -> Window {
web_sys::window().expect("no global window")
}
fn document() -> Document {
window().document().expect("no document")
}
#[wasm_bindgen]
pub fn mount_greeting(name: &str) -> Result<(), JsValue> {
let doc = document();
let body = doc.body().ok_or("no body")?;
let div = doc.create_element("div")?;
div.set_text_content(Some(&format!("Hello, {name}!")));
body.append_child(&div)?;
Ok(())
}
#[wasm_bindgen]
pub fn set_title(text: &str) {
document().set_title(text);
}What this demonstrates:
web_syswraps browser APIs with Rust typesOption/Resultpatterns replace null checks- DOM mutations work from exported WASM functions
- Errors bubble as
JsValueto JavaScript callers
Deep Dive
Feature Flags
web-sys exposes thousands of APIs behind Cargo features.
[dependencies.web-sys]
version = "0.3"
features = [
"console",
"Document",
"Element",
"HtmlElement",
"Window",
"Request",
"Response",
]Enable only what you need to keep compile times reasonable.
Fetch Example
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, Response};
pub async fn fetch_text(url: &str) -> Result<String, JsValue> {
let opts = RequestInit::new();
opts.set_method("GET");
let request = Request::new_with_str_and_init(url, &opts)?;
let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into()?;
let text = JsFuture::from(resp.text()?).await?;
Ok(text.as_string().unwrap_or_default())
}Gotchas
- Enabling all web-sys features - Compile times explode. Fix: List only required features in
Cargo.toml. - DOM without
window- SSR and tests lackwindow. Fix: Guard calls; mock in tests. - Borrow lifetimes - DOM nodes are JS-owned. Fix: Do not store references across await points without understanding GC.
- Closure drop - Event listener closures collected too early. Fix: Store
Closurein a global orforgetintentionally. - CORS failures -
fetcherrors look like network failures. Fix: Configure server CORS headers; surface errors to users.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Leptos / Yew | Declarative UI with Rust | Tiny DOM snippet |
| JavaScript glue layer | Team is JS-heavy | You want end-to-end Rust |
| WASI HTTP (future) | Non-browser HTTP | Browser DOM needed |
FAQs
web-sys vs js-sys?
js-sys covers JS builtins (Object, Array). web-sys covers Web IDL APIs (Document, Canvas).
How do I add event listeners?
Use Closure::wrap and add_event_listener_with_callback.
Canvas rendering?
Enable CanvasRenderingContext2d and HtmlCanvasElement features.
localStorage?
Enable Storage and Window::local_storage feature.
WebSockets?
Use WebSocket feature with wasm-bindgen-futures for async messages.
serde JSON fetch?
Combine fetch_text with serde_json::from_str on the response body.
Testing DOM code?
wasm-bindgen-test in headless Chrome. Or factor pure logic out of DOM calls.
Memory leaks?
Remove listeners when done. Avoid unbounded Closure::forget without need.
Shadow DOM?
Enable ShadowRoot features. API mirrors browser JS closely.
Workers?
DOM APIs are unavailable in Web Workers. Use DedicatedWorkerGlobalScope features instead.
Related
- wasm-bindgen - JS bridge fundamentals
- Frontend Frameworks (Leptos / Yew) - higher-level UI
- wasm-pack & Bundling - npm shipping
- Performance & Size - feature flag discipline
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.