waterui
Builds reactive Rust UIs with support for WaterUI and Hydrolysis widgets.
Install
mkdir -p .claude/skills/waterui && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/3603" && unzip -o skill.zip -d .claude/skills/waterui && rm skill.zipInstalls to .claude/skills/waterui
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Build cross-platform apps with WaterUI. Use when writing views, handling state, styling UI, or debugging WaterUI Rust code. Covers reactive bindings, layout, components, and the water CLI.Key capabilities
- →Configure Material 3 color schemes
- →Register reactive bindings for UI components
- →Preview layouts via CLI integration
- →Generate theme token mappings
How it works
Injects specific Rust procedural macros and color scheme configurations into the project based on the requested design system tokens.
Inputs & outputs
When to use waterui
- →Write reactive views in Rust
- →Configure Material Design theme tokens
- →Debug fine-grained reactive component state
About this skill
Building apps with WaterUI
WaterUI is a Rust UI framework that renders to real native widgets (UIKit/AppKit, Android View, GTK4) or to its own GPU renderer, from one view tree. It is fine-grained reactive: a value change updates exactly the widget that reads it, without rebuilding the surrounding tree.
Almost every mistake in WaterUI code comes from writing it as if it were React or SwiftUI. The five rules below are what actually differ. Read them before writing code.
Reference map
Read the file that matches the task. Each is self-contained; none reference each other beyond pointers.
| Topic | File |
|---|---|
Signals, Binding, Computed, collections, async tasks, animation, conditionals | references/reactivity.md |
| Component catalog with real signatures: layout, controls, menus, text, lists, forms, overlays | references/components.md |
| Photos, video, media picking, web views + JS bridge, shaders, particles, charts, maps | references/media.md |
| Gestures, taps, hover, cursor, drag & drop | references/interaction.md |
| Tabs, navigation stacks, toolbars, transitions, split views, windows | references/navigation.md |
| Colors, theme tokens, dark mode, icons, shapes, gradients, Material 3 | references/styling.md |
| Translations, plurals, locale switching, formatting, RTL | references/i18n.md |
#[waterui::test], #[waterui::bench], #[preview], snapshots | references/testing.md |
water mcp tool surface, animation stepping, agent drive loop | references/mcp.md |
water CLI, Water.toml, Cargo features, assets, permissions, platforms, embedded | references/project.md |
| Compile errors, silent bugs, and their fixes | references/troubleshooting.md |
When an API is still unclear, the compiled examples in examples/*/src/lib.rs of the
WaterUI repository are ground truth — they are built in CI, so they are never stale.
The prose companion to this skill is the book at https://book.waterui.dev, which goes
deeper on the topics here and covers ones this skill does not (plugins, resolvers and
hooks, shaders, error handling, library authoring). Each book release is pinned to an
exact WaterUI commit, shown in the book itself — check that pin against the version you
depend on before copying from it.
The five rules
1. Pass the signal, never a snapshot of it
Reactive APIs take impl IntoComputed<T>, impl IntoSignalF32, or &Binding<T>.
Handing them .snapshot() reads the value once and freezes it — the UI then never updates,
and nothing fails at compile time, so this bug is silent.
view.opacity(fade.clone()) // reacts
view.opacity(fade.snapshot()) // frozen forever — a plain f32
Photo::new(url).blur(blur.clone()) // reacts
text!("Count: {count}") // reacts
.snapshot() belongs inside event handlers and .map() closures, where you genuinely want
the value at that instant. It does not belong in a view body.
2. watch is not the reactive primitive — it is the escape hatch
watch(signal, |v| ...) tears down and rebuilds its entire subtree on every change, so
any state living inside that subtree is destroyed. Three things replace nearly every use:
text!("{status}") // reactive text — not watch + format!
Photo::new(url).blur(blur.clone()) // reactive value — pass the signal
Lazy::for_each(rows.clone(), row_view) // dynamic set of views — a collection
Reach for watch only for a genuinely one-off structural swap where no signal-aware API
and no collection applies. Check those three first, every time.
3. Inject handler state with .state(), do not capture clones
.action() takes a handler: a function whose parameters are extractors resolved from
the environment. .state(&value) puts a value in that environment; an extractor pulls it
out. State<T> is the wrapper for a type the app does not own — Binding, Rc, a
third-party value; a Clone type the app does own gets #[state] once and is then
written bare. This keeps handlers as plain named functions instead of a thicket of move
closures. The same machinery drives every callback in the framework — .on_tap, gestures,
drops, menu commands, list edits — not just buttons.
button("Increment")
.action(|State(count): State<Binding<i32>>| *count.get_mut() += 1)
.state(&count)
Repeated parameters of the same state type bind positionally — bare #[state] types
and State<T> spellings share one sequence: the first .state() call feeds the first
parameter of that type.
button("Search")
.action(|State(q): State<Binding<Str>>, State(hist): State<Binding<Vec<Str>>>| {
hist.with_mut(|h| h.push(q.snapshot()));
})
.state(&query) // -> first parameter
.state(&history) // -> second parameter
Beyond two or three pieces of state, stop threading them individually. Put them in
one Clone struct, inject it once on a container, and write handlers as free functions.
This is the idiomatic shape for a real screen:
#[state]
#[derive(Clone)]
struct Editor {
rows: ReactiveList<Row>,
editing: Binding<bool>,
}
fn toggle_editing(state: Editor) {
state.editing.toggle();
}
fn content(state: Editor) -> impl View {
vstack((
button("Edit").action(toggle_editing),
List::for_each(state.rows.clone(), row_view),
))
.state(&state) // injected once, visible to every handler below
}
Async is the same shape: .action_async(|State(x): State<Binding<Str>>| async move { … }).
4. A changing set of views is a collection, not a watch
ForEach/List diff by Identifiable id, so inserting one row touches one row.
watch over a Vec rebuilds everything and can escalate to a full-window rebuild.
use waterui::Identifiable; // the derive is NOT in the prelude
use waterui::component::lazy::Lazy;
use waterui::reactive::collection::List as ReactiveList;
#[derive(Clone, Identifiable)]
struct Row { #[id] id: u64, title: Str }
let rows = ReactiveList::from(seed_rows); // bulk-seed; .push/.insert/.remove diff by id
Lazy::for_each(rows.clone(), |row| text(row.title)) // reactive sequence in a stack
List::for_each(rows.clone(), |row| ListItem::new(...)) // platform list: lazy, editable
Note the shape: ForEach is a collection of views, not a view. A container consumes
it — Lazy::for_each(data, f) is the shorthand for Lazy::vstack(ForEach::new(data, f)),
and List::for_each is the list-control equivalent. Writing ForEach::new(..) where a
view is expected is a trait-bound error, not a runtime surprise.
List realizes only the visible window — it handles 100,000 rows, including after a
programmatic jump. Use it whenever the data is a list of rows; use the Lazy stacks when
you just need a reactive sequence inside your own layout. A derived row set (filtered
or sorted from other state) is still a collection: wrap the derived signal in
SignalCollection rather than watching a Vec.
5. Rebuild-driven state loss is correct behavior, not a bug to patch
A component's body may be expensive and may do one-time setup. When a parent's control
flow (when, watch, a route change) reconstructs a component, that instance is gone
and a new one initializes — losing its internal state is the intended semantics.
If some state must survive, that state was owned at the wrong level: lift it into a
Binding held by the parent and pass it down. Never try to preserve it with hidden
caches, hook-like slots, or position-keyed storage. Those do not exist in WaterUI and
adding them is an architectural error.
Quick start
use waterui::app::App;
use waterui::prelude::*;
fn counter() -> impl View {
let count = Binding::i32(0);
vstack((
text!("Count: {count}").headline(),
button("+1")
.action(|State(count): State<Binding<i32>>| *count.get_mut() += 1)
.state(&count),
))
.spacing(8.0)
.padding()
}
pub fn app(env: Environment) -> App {
App::new(counter, env)
}
use waterui::prelude::*; brings in views, layout, colors, text, controls, navigation,
menus, State, Str, AnyView, SignalExt, and AnimationExt. Several everyday names
live outside it:
use waterui::Identifiable; // the derive macro
use waterui::reactive::binding; // the general Binding constructor
use waterui::reactive::collection::List as ReactiveList;
use waterui::widget::condition::when; // conditionals
use waterui::animation::Animation; // animation curves
use waterui::component::lazy::Lazy; // reactive stacks over a collection
use waterui::views::ForEach; // the collection itself
use waterui::gesture::{DragGesture, LongPressGesture, TapGesture};
use waterui::cursor::CursorStyle;
use waterui::drag_drop::{Files, Transferable};
use waterui::env::with; // scope a value to a subtree
use waterui::task::{sleep, spawn_local}; // async utilities
Components behind cargo features are also absent until you enable them. waterui's
defaults are gpu, assets, media, inspector, snackbar; webview and
flow-markdown are opt-in in Cargo.toml. Canvas, charts, barcodes and particles have
opt-in facade features canvas, chart, barcode and particle, exposing the
independent crates as waterui::canvas, waterui::chart, waterui::barcode and
waterui::particle. Direct dependencies such as `w
Content truncated.
When not to use it
- →Non-Rust UI projects
- →Web-only development lacking WaterUI primitives
Prerequisites
Limitations
- →UI state resets during component rebuilds are expected behavior
- →Requires Hydrolysis-compatible backend
How it compares
It manages complex reactive state and theme injection tokens automatically, avoiding manual boilerplate for fine-grained reactivity.
Compared to similar skills
waterui side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| waterui (this skill) | 1 | 4mo | Review | Intermediate |
| tauri | 76 | 2mo | Review | Advanced |
| rust-errors | 5 | 2mo | No flags | Advanced |
| hula-skill | 3 | 8mo | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
You might also like
tauri
EpicenterHQ
Tauri path handling, cross-platform file operations, and API usage. Use when working with file paths in Tauri frontend code, accessing filesystem APIs, or handling platform differences in desktop apps.
rust-errors
EpicenterHQ
Rust to TypeScript error handling patterns for Tauri apps. Use when defining Rust errors that will be passed to TypeScript, handling Tauri command errors, or creating discriminated union error types.
hula-skill
HuLaSpark
HuLa project skill for frontend (Vue 3 + Vite + UnoCSS + Naive UI/Vant), backend (Tauri v2 + Rust + SeaORM/SQLite), full-stack flows, and build/release work. Use when the user mentions hula or HuLa or requests changes in this repository; after triggering, ask which scope (frontend/backend/fullstack/build-release) to enable.
makepad-shaders
ZhangHanDong
CRITICAL: Use for Makepad shader system. Triggers on: makepad shader, makepad draw_bg, Sdf2d, makepad pixel, makepad glsl, makepad sdf, draw_quad, makepad gpu, makepad 着色器, makepad shader 语法, makepad 绘制
waller-wallpaper-session
gvastethecreator
Extend or debug the Wallpaper Session, monitor drafts, preview flows, editor flow, and profile composition across React, Tauri, and Rust. Use for monitor wallpaper features, profile bugs, preview issues, or domain-model changes.
tauri-window-management
bkywksj
|