# Common Patterns
This page is for the moment when “hello world” is no longer enough and a real app starts to form.
The goal is not to teach every API again.
It is to show how to keep SLT code readable as screens, widgets, and state grow.
## The default shape of a real SLT app
For most medium-sized apps, the most readable shape is:
- one plain Rust `App` struct for app state
- one top-level `run(...)` or `run_with(...)` closure
- a few `render_*` helper functions for big panels or screens
- occasional hooks for truly local persistent state
That keeps the public grammar small without turning the closure into a 400-line blob.
## Application state lives in normal Rust
```rust
use slt::{Context, KeyCode};
struct App {
count: i32,
dark: bool,
}
fn main() -> std::io::Result<()> {
let mut app = App {
count: 0,
dark: false,
};
slt::run(|ui: &mut Context| {
if ui.key('q') || ui.key_code(KeyCode::Esc) {
ui.quit();
}
if ui.button("+1").clicked {
app.count += 1;
}
ui.checkbox("Dark mode", &mut app.dark);
ui.text(format!("count: {}", app.count));
})
}
```
Use plain structs when the state belongs to your app, your domain, or your screens.
## Local persistent state with hooks
```rust
let count = ui.use_state(|| 0i32);
ui.text(format!("{}", count.get(ui)));
if ui.button("+1").clicked {
*count.get_mut(ui) += 1;
}
```
Use hooks when:
- the state is local to one render subtree
- introducing a top-level field would add more noise than clarity
- you want to prototype quickly
Keep hook call order stable across frames.
## When to use app state vs hooks
Use app state when:
- multiple screens need the value
- the value matters outside rendering
- you want explicit ownership and easier refactoring
Use hooks when:
- the value is local to one widget subtree
- the lifetime is purely UI-local
- you want a lightweight scratchpad for a small interactive fragment
The mistake is not using hooks.
The mistake is using hooks for everything until the screen becomes impossible to reason about.
## Derived state with `use_memo`
```rust
## Components
A "component" in SLT is not a framework concept. There is no `Component` trait, no virtual DOM, no lifecycle hooks beyond the few state primitives you already know. A component is whatever helper function you find yourself extracting when the same shape repeats.
This section walks the four building blocks that make components ergonomic:
1. Functions for the shape itself
2. `use_state_named` for state that survives across frames inside the component
3. `provide` / `use_context` for values that propagate through deep trees
4. `.with` / `.with_if` for conditional styling
The order matters. Reach for the simpler tool first.
### Components as Functions (the canonical pattern)
The simplest reusable component is a free function that takes `&mut Context` and any explicit state it needs. No framework magic, no registration step.
```rust
use slt::{Border, Context, Trend};
fn metric_card(ui: &mut Context, label: &str, value: f64, trend: Trend) {
let _ = ui.bordered(Border::Single).pad(1).col(|ui| {
ui.text(label).dim();
ui.text(format!("{:.1}", value)).bold();
let arrow = match trend {
Trend::Up => "▲",
Trend::Down => "▼",
Trend::Flat => "—",
};
ui.text(arrow);
});
}
```
Usage from a screen:
```rust
fn render_dashboard(ui: &mut Context, app: &App) {
ui.row(|ui| {
metric_card(ui, "Revenue", app.revenue, Trend::Up);
metric_card(ui, "Latency p99", app.p99_ms, Trend::Down);
metric_card(ui, "Active users", app.users, Trend::Flat);
});
}
```
Trade-offs:
- Fully explicit. Rust's ownership tells you exactly who reads and who mutates what.
- Type-safe. The compiler enforces parameter shapes at every call site.
- No hidden state. The function only renders; it does not remember anything between frames.
The cost is parameter count. When a component grows past four or five arguments, group related values into a small struct and pass `&Props` instead of repeating five-arg signatures across the codebase.
```rust
struct MetricCardProps<'a> {
label: &'a str,
value: f64,
trend: Trend,
accent: Color,
}
fn metric_card(ui: &mut Context, p: &MetricCardProps<'_>) {
// ...
}
```
This is still "just a function." No ceremony.
### Component-local State with `use_state_named`
Sometimes a component needs state that must survive across frames but does not belong to the caller. A collapsible panel knows whether it is expanded. A pagination control knows the current page. Threading those values up through every call site pollutes the caller's API.
`use_state_named` is the answer. It is `use_state`, but keyed by an explicit `&'static str` instead of by hook call order:
```rust
use slt::{Border, Context};
fn expandable_card(
ui: &mut Context,
id: &'static str,
title: &str,
body: impl FnOnce(&mut Context),
) {
let expanded = ui.use_state_named::<bool>(id);
let _ = ui.bordered(Border::Single).col(|ui| {
let label = if *expanded.get(ui) { "▼" } else { "▶" };
if ui.button(label).clicked {
let v = *expanded.get(ui);
*expanded.get_mut(ui) = !v;
}
ui.text(title).bold();
if *expanded.get(ui) {
body(ui);
}
});
}
```
Usage:
```rust
expandable_card(ui, "card.networking", "Networking", |ui| {
ui.text("eth0 192.168.1.42");
ui.text("wlan0 10.0.0.7");
});
expandable_card(ui, "card.disks", "Disks", |ui| {
ui.text("nvme0n1 931 GiB");
});
```
When you need a default different from `Default::default()`, use `use_state_named_with`:
```rust
let page = ui.use_state_named_with::<usize>("pager", || 1);
```
Rules of the road:
- IDs are `&'static str`. Pick something descriptive — `"card.networking"`, `"pager.users"`, not `"x"`.
- Two calls with the same id at the same scope share state. This is sometimes what you want (siblings agreeing on a value) and sometimes a bug (two unrelated cards collapsing together). When in doubt, suffix with the data key: `"card.disks"` vs `"card.networking"`.
- Unlike positional `use_state`, named state does not depend on call order. You can branch around a named-state read without losing the value next frame.
- For the full method surface on the returned `State<T>`, see `STATE_APIS.md`.
### Context Injection with `ui.provide` / `ui.use_context`
Some values want to propagate through a deep tree without being passed to every function. The classic examples are theme, the current user, and feature flags. Threading them through ten render helpers is parameter-drilling, and it makes refactoring painful.
`provide` makes a value available inside a closure. Anything inside that closure can read it via `use_context`:
```rust
use slt::{Color, Context};
struct AppContext {
username: String,
show_debug: bool,
}
fn main() -> std::io::Result<()> {
slt::run(|ui| {
let app = AppContext {
username: "sb".into(),
show_debug: true,
};
ui.provide(app, |ui| {
render_home(ui);
});
})
}
fn render_home(ui: &mut Context) {
let app = ui.use_context::<AppContext>();
ui.text(format!("Hello, {}", app.username));
if app.show_debug {
ui.text("debug mode on").dim();
}
}
```
How it behaves:
- **Scoped.** The value is alive only inside the body closure passed to `provide`. Outside that closure, `use_context::<AppContext>()` panics — there is no value.
- **Shadowing.** Nested `provide` calls of the same type shadow the outer one for the duration of the inner closure. Think of it as a LIFO stack, one stack per `TypeId`.
- **Optional reads.** `try_use_context::<T>() -> Option<&T>` returns `None` instead of panicking. Use it when a component should work both inside and outside the provider.
```rust
fn render_footer(ui: &mut Context) {
if let Some(app) = ui.try_use_context::<AppContext>() {
ui.text(format!("logged in as {}", app.username)).dim();
} else {
ui.text("anonymous").dim();
}
}
```
When *not* to use context:
- The value lives in your top-level app struct and you already pass `&mut App` around. Do not duplicate it into context.
- The value is needed by exactly one render helper that is two scope levels away. A parameter is clearer.
A good heuristic: reach for `provide` when the same value is needed by three or more render helpers across two or more scope levels.
### Conditional Styling with `.with_if`
Conditional styling is everywhere — a row that highlights when selected, a label that turns red when invalid, a button that dims when disabled. Without help, this clutters the call site:
```rust
let mut t = ui.text("Status");
if is_error {
t = t.bold().fg(Color::Red);
}
if is_selected {
t = t.bg(Color::DarkGray);
}
```
`.with_if(cond, modifier)` compresses that into a chain that reads top-to-bottom:
```rust
ui.text("Status")
.with_if(is_error, |t| {
t.bold().fg(Color::Red);
})
.with_if(is_selected, |t| {
t.bg(Color::DarkGray);
});
```
The closure runs only when the condition is true. The modifier receives a mutable handle to the same builder, so you can chain any styling method inside.
For unconditional grouping — factoring shared modifier blocks out of multiple call sites — use `.with(modifier)`:
```rust
fn dim_label(t: &mut TextBuilder<'_>) {
t.dim().italic();
}
ui.text("uptime: 3d 4h").with(dim_label);
ui.text("region: us-west-2").with(dim_label);
```
Both `.with` and `.with_if` are available on text and on container builders, so the same idiom works for `bordered`, `row`, `col`, etc.:
```rust
ui.bordered(Border::Single)
.with_if(panel_focused, |c| {
c.title("(focused)").pad(2);
})
.col(|ui| {
// ...
});
```
### When to use which
| Function + explicit args | Small components, fewer than 3-4 params | `metric_card`, `header_row`, `kv_pair` |
| `use_state_named` | Component has LOCAL state that should survive across frames | collapsible panel, pagination cursor, sort direction |
| `provide` / `use_context` | Values cross 3+ scope levels | theme, logged-in user, feature flags, request id |
| `.with_if` | Styling depends on a runtime condition | selected row, error text, disabled state, focused panel |
| `.with` | Factor shared style blocks out of multiple call sites | "dim label", "code-style text", "subtle border" |
### Anti-patterns
These look tempting but make code harder, not easier:
- **Reaching for context when a parameter is clearer.** If a value is used in one helper, just pass it. Context is for values that propagate, not for "I do not feel like typing the parameter."
- **Using `use_state_named` for app-level state.** Page-level state, selected tab, current user — these belong in your top-level `App` struct so that tests, persistence, and refactors stay sane. Named state is for state that is genuinely local to a component instance.
- **Reusing the same id by accident.** `expandable_card(ui, "card", ...)` called five times shares one boolean across all five cards. Pick ids that name the *instance*, not the *kind*.
- **Nesting `provide` purely to "override defaults."** If you find yourself wrapping every render helper in a fresh `provide`, the value should probably be a parameter or a method argument instead.
For full working apps that combine these patterns, see `COOKBOOK.md`. For the per-method reference on `State<T>` and friends, see `STATE_APIS.md`. For the single-file AI-oriented reference, see `COMPLETE_REFERENCE.md`.
## Split big screens into render helpers
```rust
fn render_sidebar(ui: &mut Context, app: &mut App) {
ui.text("Navigation").bold();
let _ = ui.list(&mut app.sidebar);
}
fn render_content(ui: &mut Context, app: &mut App) {
ui.text(format!("Selected: {}", app.current_title()));
}
fn render_app(ui: &mut Context, app: &mut App) {
ui.row(|ui| {
panel(ui, "Sidebar", |ui| render_sidebar(ui, app));
panel(ui, "Content", |ui| render_content(ui, app));
});
}
```
If a closure becomes hard to scan, extract render helpers before inventing a new abstraction layer.
## Forms and validation
```rust
let mut email = TextInputState::with_placeholder("you@example.com");
ui.text_input(&mut email);
Ok(())
} else {
Err("Invalid email".into())
}
});
```
For larger forms, reach for `FormField` and `FormState`.
## Focus and keyboard shortcuts
```rust
if ui.key('q') {
ui.quit();
}
if ui.key_code(KeyCode::Enter) {
submit();
}
if ui.raw_key_code(KeyCode::Esc) {
close_overlay();
}
```
Use `raw_*` shortcuts for keys that must work regardless of modal or overlay state.
## Screen helpers and navigation
```rust
});
});
```
Use `screen(name, &mut screens, ...)` when you want declarative rendering that only runs for the active screen. Each screen gets isolated hook state and focus.
Use manual `push()` / `pop()` logic on `ScreenState` when you need explicit navigation transitions.
## Modal, overlay, and screen composition
```rust
screens.push("settings");
}
});
screens.pop();
}
});
if show_modal {
ui.modal(|ui| {
ui.text("Confirm?").bold();
});
}
```
Use screens for view-level navigation and modal/overlay for transient UI layers.
## Error boundaries and recovery
```rust
ui.error_boundary(|ui| {
ui.text("Protected subtree");
});
```
Use `error_boundary` or `error_boundary_with` when you want one subtree to fail without taking down the whole app.
This is especially useful for experimental widgets, user-generated content, or plugins.
## Custom widgets: focus and interaction
```rust
let focused = ui.register_focusable();
let response = ui.interaction();
if response.hovered {
ui.tooltip("Hovered");
}
```
Use `register_focusable()` when the widget needs keyboard participation.
Use `interaction()` when the widget needs click/hover without wrapping everything in a container.
## Async background messages
```rust
let tx = slt::run_async(|ui, messages: &mut Vec<String>| {
for message in messages.drain(..) {
ui.text(message);
}
})?;
tx.send("Background work done".into()).await?;
```
Enable with the `async` feature.
## Animation patterns
```rust
// Tween: smooth transition over N ticks
let mut fade = Tween::new(0.0, 1.0, 30);
let opacity = fade.value(ui.tick());
// Spring: physics-based, responds to target changes
let mut spring = Spring::new(0.0, 0.2, 0.85);
if hovered {
spring.set_target(1.0);
} else {
spring.set_target(0.0);
}
spring.tick();
let scale = spring.value();
// Stagger: offset animation across list items
let mut stagger = Stagger::new(0.0, 1.0, 20).delay(3).items(items.len());
for (i, item) in items.iter().enumerate() {
let alpha = stagger.value(ui.tick(), i);
ui.text(item)
.fg(Color::Rgb(255, 255, (alpha * 255.0) as u8));
}
```
Animation types are standalone structs that compute values from `ui.tick()`.
Pass computed values to style and layout methods.
See `docs/ANIMATION.md` for the full API.
## Responsive layout
```rust
match ui.breakpoint() {
Breakpoint::Xs | Breakpoint::Sm => {
ui.col(|ui| { /* stacked layout */ });
}
_ => {
ui.row(|ui| { /* side-by-side layout */ });
}
};
```
Use `breakpoint()` for width-dependent layout decisions.
ContainerBuilder also supports responsive methods like `.gap_sm(1).gap_lg(2)`.
## Custom widgets
```rust
use slt::{Color, Context, Style, Widget};
struct Rating {
value: u8,
max: u8,
}
impl Widget for Rating {
type Response = bool;
fn ui(&mut self, ui: &mut Context) -> bool {
let focused = ui.register_focusable();
let mut changed = false;
if focused {
if ui.key('+') && self.value < self.max {
self.value += 1;
changed = true;
}
if ui.key('-') && self.value > 0 {
self.value -= 1;
changed = true;
}
}
let stars: String = (0..self.max)
.map(|i| if i < self.value { '★' } else { '☆' })
.collect();
ui.styled(
stars,
Style::new().fg(if focused {
Color::Yellow
} else {
Color::White
}),
);
changed
}
}
```
If you add a new built-in widget to the library itself, also follow the checklist in `CONTRIBUTING.md`.
## Testing and verification
```rust
use slt::TestBackend;
let mut backend = TestBackend::new(40, 10);
});
backend.assert_contains("Hello");
```
Use `TestBackend` for headless rendering checks and snapshot-style assertions.
For runtime-contract work, add a custom `Backend` + `frame()` test too.
## Heuristics that keep SLT readable
- If a builder chain repeats three times, extract a helper.
- If a closure becomes hard to scan, split it into `render_*` functions.
- If state is shared across screens, move it into your app struct.
- If state is local to one subtree, hooks are fine.
- If behavior depends on previous-frame data, test at least two frames.
SLT stays pleasant when you keep the public grammar small even as the codebase grows.