A lightweight extraction of silkenweb without CSS-in-Rust features. Silkenweb is a reactive web framework for Rust with fine-grained reactivity, server-side rendering, and hydration support.
| Crate | Description |
|---|---|
silkenweb |
Main framework: elements, DOM abstractions, routing, SSR, hydration, animation |
silkenweb-base |
Core primitives and browser API wrappers |
silkenweb-macros |
Proc macros for deriving element traits (Element, ChildElement, etc.) |
silkenweb-signals-ext |
Extensions for futures-signals (signal products, value abstractions) |
silkenweb-task |
Cross-platform task management (browser microtasks / tokio on server) |
silkenweb-test |
Testing utilities for browser and SSR tests |
- Pure Rust API with type-safe HTML and SVG element construction
- Fine-grained reactivity via futures-signals
- Multiple DOM modes:
Dry(SSR),Wet(client),Hydro(hydration),Template(reusable) - Routing, animation, and local/session storage APIs
- Cross-platform: browser (
wasm32-unknown-unknown) and server (tokio)
use futures_signals::signal::{Mutable, SignalExt};
use silkenweb::prelude::*;
use silkenweb::elements::html::{button, div, span};
use silkenweb::value::Sig;
let count = Mutable::new(0);
let count_text = count.signal().map(|i| format!("Value: {i}!"));
let app = div()
.child(button().on_click({
let count = count.clone();
move |_, _| count.replace_with(|n| *n - 1);
}).text("-1"))
.child(span().text(Sig(count_text)))
.child(button().on_click(move |_, _| {
count.replace_with(|n| *n + 1);
}).text("+1"));use silkenweb::dom::Dry;
use silkenweb::elements::html::p;
let rendered = p::<Dry>().text("Hello, world!").freeze().to_string();
assert_eq!(rendered, "<p>Hello, world!</p>");Elements use a fluent builder pattern. Functions like div(), button(), input() create elements; methods chain to add attributes, children, classes, and events.
div()
.class("container")
.child(h1().text("Hello"))
.child(button().on_click(|_, _| { /* handler */ }).text("Click"))Use r#type for the type attribute (Rust keyword): input().r#type("text").
State lives in Mutable<T>. Wrap signals in Sig() to bind them to the UI:
let name = Mutable::new(String::new());
input()
.value(Sig(name.signal_cloned()))
.on_input({
let name = name.clone();
move |_, el| name.set(el.value())
})Key signal methods:
.signal()/.signal_cloned()— get a signal from aMutable.signal_ref(|val| ...)— map over a reference to the current value.map(|val| ...)— transform signal values.broadcast()— share a signal across multiple consumers.set()/.set_neq()— update value (neq only if changed).lock_ref()/.lock_mut()— synchronous access to current value
Combine signals with map_ref!:
use futures_signals::map_ref;
let combined = map_ref! {
let a = mutable_a.signal(),
let b = mutable_b.signal() => {
*a + *b
}
};Components are functions generic over <D: Dom>, returning a concrete element type. Props are passed via trait objects or Rc:
pub fn search_bar<D: Dom>(data: &Rc<impl SearchBarInfo + 'static>) -> Div<D> {
div()
.class("search-bar")
.child(input()
.placeholder(Sig(data.placeholder()))
.value(Sig(data.value()))
.on_input({
let data = data.clone();
move |_, el| data.set_value(el.value())
}))
}The <D: Dom> generic enables the same component code for client rendering (Wet), SSR (Dry), and hydration (Hydro).
// Single child
div().child(p().text("hi"))
// Multiple from iterator
ul().children(items.iter().map(|i| li().text(i.name())))
// Conditional
div().optional_child(some_option_element)
div().optional_child(Sig(signal_returning_option))
// Dynamic list from SignalVec
div().children_signal(my_signal_vec.map(|item| li().text(item.name())))Use .map() with .then_some() and .optional_child():
let panel = is_visible.signal().map({
let data = data.clone();
move |visible| visible.then_some(settings_panel(&data))
});
div().optional_child(Sig(panel))// Static
div().class("active")
// Conditional via signal — return Option<&str>
let cls = is_open.signal().map(|open| open.then_some("expanded"));
div().classes(Sig(cls))Event handlers take |event, element|. Clone state into the closure:
button().on_click({
let data = data.clone();
move |_, _| data.submit()
})
input().on_input({
let data = data.clone();
move |_, el| data.set_value(el.value())
})
// Prevent default
anchor().on_click({
let data = data.clone();
move |e, _| {
e.prevent_default();
data.navigate();
}
})Use spawn_local for fire-and-forget async work. Combine with .for_each() on signals for reactive effects:
use silkenweb::task::spawn_local;
spawn_local(search_term.signal_cloned().for_each({
let api = api.clone();
move |term| {
let api = api.clone();
async move {
if !term.is_empty() {
api.search(&term).await;
}
}
}
}));Same component code works across rendering modes:
// Server: render to HTML string
use silkenweb::dom::Dry;
let html = my_component::<Dry>(data).freeze().to_string();
// Client: hydrate server-rendered HTML
use silkenweb::{dom::Hydro, hydration::hydrate};
hydrate("app", my_component::<Hydro>(data)).await;use silkenweb::router;
// Navigate
router::set_url_path("settings");
// React to URL changes
let page = router::url_path().signal_ref(|path| {
match path.as_str() {
"/settings" => settings_page(),
_ => home_page(),
}
});
div().child(Sig(page))MIT OR Apache-2.0