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
WaterUI App Development
Build views with reactive state. When unsure, search examples/*/src/lib.rs for existing patterns.
CRITICAL: Runtime And Testing Semantics
- WaterUI is fine-grained reactive with reconstruction semantics. If parent-driven control flow rebuilds a component instance, that instance's local state resetting is expected and correct.
- Do not fix rebuild-driven resets by caching hidden state across rebuilds. If state must survive, lift it into explicit reactive ownership at the right level.
- Prefer
Binding,Computed, and other signal inputs for dynamic behavior. Avoid reading.get()in view bodies when a reactive API can accept the signal directly. - Hydrolysis widget chrome is provided by a backend-neutral
WidgetTheme. For Material Design 3 output, installhydrolysis_m3::install(&mut env)before running or previewing a Hydrolysis app. - Hydrolysis Material 3 colors use Material You system roles for WaterUI theme tokens. Use
hydrolysis_m3::MaterialColorSource::new(source_color)to configure source color, variant, contrast level, spec version, and platform; call.schemes()to generate paired light/darkMaterialColorSchemes, then install one side withinstall_with_source,install_with_color_schemes, orinstall_with_colors. - For Material-specific view colors, use
hydrolysis_m3::color::*role tokens such asPrimary,OnPrimary,PrimaryContainer,SurfaceContainerHighest, andOnSurfaceVariant. These resolve from the installedMaterialColorScheme, while WaterUI's portabletheme_color::*tokens map semantic roles such asAccent,AccentContainer,Tertiary, andTertiaryContainerto the active platform theme. - Use
water preview --backend hydrolysis --theme material3for Hydrolysis Material 3 visual checks. Pass--exprwhen previewing an inline Rust expression; the CLI writes the expression into generated Rust preview code and rustc compiles it normally withwaterui::prelude::*andwateruiin scope.
CRITICAL: Reactive-First Pattern
WaterUI is a reactive framework. ALWAYS pass Bindings directly to APIs instead of using .get() or watch.
Most WaterUI APIs accept impl Signal or impl IntoSignalF32 - pass bindings directly for automatic reactivity:
// ✅ CORRECT - Pass binding directly, updates automatically
Photo::new(url).blur(blur_value.clone()) // blur updates as slider moves
view.visible(is_visible.clone()) // visibility reacts to state
view.opacity(opacity_value.clone()) // opacity animates reactively
view.disabled(is_loading.clone()) // disabled state follows loading
text!("Count: {count}") // text updates automatically
// ❌ WRONG - Static value, requires manual refresh
Photo::new(url).blur(blur_value.get()) // blur frozen at initial value
view.visible(is_visible.get()) // visibility never changes
watch(count.clone(), |c| text(format!("{c}"))) // unnecessary indirection
Rule: If an API accepts a value that might change, check if it accepts impl Signal and pass the binding.
Quick Start
use waterui::prelude::*;
fn main() -> impl View {
let count = Binding::i32(0);
vstack((
text!("Count: {count}").headline(),
button("+1")
.action(|State(count): State<Binding<i32>>| count.set(count.get() + 1))
.state(&count),
))
}
Views
Functions and closures are views:
fn card(title: &str) -> impl View {
vstack((text(title).title(), Divider))
}
// Use directly - no wrapper needed
vstack((card("Hello"), card("World")))
Conditional rendering:
// Show or hide (Option<impl View> is a View)
is_new.map(|b| b.then(|| badge("New")))
// Binary choice (if-else)
when(is_logged_in, || dashboard()).otherwise(|| login_form())
// Multi-branch (if-elif-else)
when(state.equal_to(0), || "Loading")
.or(state.equal_to(1), || "Ready")
.otherwise(|| "Error")
State
// Use type-specific constructors (Binding::new does NOT exist)
let toggle = Binding::bool(false);
let count = Binding::i32(0);
let value = Binding::f64(1.5);
let name = Binding::container(String::new()); // heap types (String, Vec, etc.)
let text = Binding::container(Str::from("hello")); // Str type
// Pass by reference to child views
fn section(count: &Binding<i32>) -> impl View { ... }
Reactive Transforms
Methods on signals (no .clone() needed for transforms):
count.not() // bool negation
count.select(a, b) // if-else
count.equal_to(5) // equality check
count.gt(0) // comparisons: lt, le, ge
count.is_empty() // for strings/collections
count.map(|v| v * 2) // custom transform
count.zip(&other).map(|(a,b)| a + b) // combine signals
Convert to Computed: signal.computed()
Reactive Modifiers
Pass bindings directly to modifiers for real-time updates:
let opacity = Binding::f64(1.0);
let blur = Binding::f64(0.0);
let is_visible = Binding::bool(true);
let is_disabled = Binding::bool(false);
let scale_factor = Binding::f64(1.0);
view
.opacity(opacity.clone()) // reactive opacity
.visible(is_visible.clone()) // reactive visibility
.disabled(is_disabled.clone()) // reactive disabled state
.scale(scale_factor.clone(), scale_factor.clone()) // reactive scale
// Filters also accept reactive values
Photo::new(url)
.blur(blur.clone()) // blur updates in real-time
.saturation(saturation.clone()) // saturation updates in real-time
.brightness(brightness.clone()) // brightness updates in real-time
Event Handlers
IMPORTANT: Inject handler state with .state() after .action() or
.action_async(); never clone bindings just to capture them. State<T> extracts
the injected value from the action environment.
// Single state
button("Click")
.action(|State(count): State<Binding<i32>>| count.set(count.get() + 1))
.state(&count)
// Multiple states are separate typed extractor parameters
button("Reset")
.action(
|State(x): State<Binding<i32>>, State(y): State<Binding<i32>>| {
x.set(0);
y.set(0);
},
)
.state(&x)
.state(&y)
// Four states example
button("Submit")
.action(
|State(url): State<Binding<Str>>,
State(blur): State<Binding<f64>>,
State(status): State<Binding<String>>,
State(handler): State<DynamicHandler>| {
// Use all four injected values
},
)
.state(&url)
.state(&blur)
.state(&status)
.state(&handler)
// Async
button("Load").action_async(|_| async { fetch().await })
// Lifecycle
view.on_appear(|| setup())
view.on_change(&signal, |new_val| handle(new_val))
Text
IMPORTANT: Use text() for static text and text! for reactive text. Never use watch() just to build text. Also never write waterui::text!; import the macro and use text! directly.
// Static text - use text() function
text("Hello").title() // semantic sizes: title, headline, body, caption, footnote, sub_headline
// Reactive text - use text! macro (auto-updates when bindings change)
text!("Count: {count}") // single binding
text!("{a} + {b} = {sum}") // multiple bindings
text!("Value: {value:.2}") // with formatting
text!("{FOCUSED_READOUT}") // const &str capture is fine if text! behavior is desired
// text! returns LocalizedText with font methods
text!("Status: {status}").sub_headline()
text!("Small: {value}").caption()
Layout
hstack((a, b, c)).spacing(8.0)
vstack((a, b)).padding()
zstack((background, content))
scroll(content)
spacer() // flexible space
spacer().height(16.0) // fixed space
// From iterator - use .collect() for dynamic layouts
let buttons: HStack<_> = items.iter().map(|i| button(i.label)).collect();
Programmatic scrolling is explicit and repeatable through ScrollController.
List targets item indices, while ScrollView targets content coordinates:
let list_scroll = ScrollController::<usize>::new(0);
let list = List::for_each(rows, row_view).scroll_controller(&list_scroll);
list_scroll.scroll_to(50_000);
let content_scroll = ScrollController::<Point>::new(Point::zero());
let viewport = scroll(lazy_content).scroll_controller(&content_scroll);
content_scroll.scroll_to(Point::new(0.0, 2_400.0));
List::for_each and ScrollView containing a lazy stack materialize only the
visible window, including after a deep programmatic jump. Use List::content
when section markers or heterogeneous static rows are required.
Colors
// Built-in (zero-sized, efficient)
Blue, Green, Red, Orange, Purple, Cyan, Yellow, Pink, Grey
// Custom
const BRAND: Srgb = Srgb::from_hex("#3B82F6");
// Usage - colors are Views
view.background(Blue)
view.foreground(BRAND)
Blue.size(80.0, 80.0) // colored rectangle
BRAND.with_opacity(0.5)
Theme colors: Foreground, MutedForeground, Accent, AccentContainer,
AccentForeground, Tertiary, TertiaryContainer, Background, Surface,
SurfaceVariant, Border
Icons
Icons come from packaged icon-set crates — pick one set per app and depend on it:
waterui-icons-material-icon (Material Symbols), waterui-icons-lucide,
waterui-icons-fontawesome7, waterui-icons-sf-symbol (Apple only).
use waterui_icons_material_icon as mdi;
mdi::check_circle() // an icon view
mdi::delete().size(20.0, 20.0) // size it
mdi::flag().foreground(Accent) // theme color
mdi::calendar_today().tint(Srgb::from_hex("#4A84F6")) // explicit tint
Match the icon set to the design language: a Material 3 (hydrolysis_m3) app uses
Material icons, not Lucide. SystemIcon/SF Symbols are Apple-only — for
portable code d
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 | 3mo | Review | Intermediate |
| tauri | 76 | 1mo | Review | Advanced |
| rust-errors | 5 | 1mo | No flags | Advanced |
| hula-skill | 3 | 7mo | 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
|