---
title: Widget
description: How a widget is assembled, its lifecycle across cold / warm / animated frames, and the placeholders that substitute when things go sideways.
---
A widget is a single named composition of a fetcher and a renderer,
pinned into a layout slot. The TOML gives it a handle; the runtime
resolves the three pieces, fetches or recomputes the payload, compat-
checks the shape, and paints.
## Anatomy
```toml
[[widget]]
id = "clock" # unique handle — rows reference it
fetcher = "clock" # who produces the payload
format = "%H:%M" # threaded into the fetcher's ctx.format
render = { type = "text_ascii", style = "figlet", font = "banner", align = "center" }
[widget.options] # threaded into the fetcher's ctx.options
# (clock has none; other fetchers like clock_timezones do)
```
A row then binds the widget into a slot:
```toml
[[row]]
height = { length = 8 }
[[row.child]]
widget = "clock" # references the id above
```
Widgets and rows live in separate top-level arrays on purpose:
- Widgets are identity + behaviour. A widget has no position; it just
declares "this is clock_big, it reads from clock and draws with
text_ascii".
- Rows are composition. A row's child references a widget by id and
gives it width, border, title, background. Two rows can reference
the same widget id if they share visual treatment.
## Fields
| `id` | Handle used by rows. Must be unique within the dashboard. |
| `fetcher` | Registered fetcher name (`clock`, `git_status`, `github_repo_prs`, …). |
| `format` | Fetcher-specific format hint. `clock` uses it as a `strftime` string; `basic_static` uses it as a literal body; most others ignore it. |
| `options` | Inline table passed to the fetcher's options deserializer. Each fetcher defines its own typed view via `OptionSchema`. Unknown keys are ignored so adding a new option can't invalidate old configs. |
| `render` | Either a bare string (renderer name) or an inline table `{ type = "…", … }` with renderer-specific options. Omit it to get the shape's default renderer. |
## Shape resolution
Every fetcher declares `shapes()` — the list of shapes it can emit — and
every renderer declares `accepts()` — the list it can draw. The runtime
resolves which shape this widget will use at dispatch time:
1. If `render` pins a renderer, the fetcher's first shape that the
renderer accepts wins.
2. Otherwise the fetcher's `default_shape()` is used, and the default
renderer for that shape is picked.
3. If the fetcher emits a body whose shape the renderer doesn't accept,
the widget renders an in-band `renderer X cannot display Y` error
rather than crashing the splash.
Multi-shape fetchers (`clock`, `basic_read_store`) branch on
`ctx.shape` inside their `fetch` / `compute`. Single-shape fetchers
ignore it.
## Lifecycle
The same widget takes a different path depending on whether it's cached
or realtime:
```
Cached widget
───────────────────────────────────────────────────────────────────
new shell ┌─ reads ──▶ cache (paint instant)
│ │ │
▼ │ │ fresh payload
splashboard ───────────────┤ ▼
│ detached child
│ │ runs fetcher with ctx
│ ▼
└─ writes ──── cache (next render sees fresh)
Realtime widget (clock / system_monitor_cpu / clock_ratio / pomodoro / …)
───────────────────────────────────────────────────────────────────
draw tick ──▶ compute(ctx) ──▶ payload ──▶ renderer
(per frame) < 1 ms, no cache, no I/O
```
The cache lives at `$HOME/.splashboard/cache/<widget_key>.json`. Its
key is `fetcher.cache_key(ctx)` — most fetchers use `name + format`
so two widgets that differ only in `format` get independent cache
slots.
### Loading placeholder
When a cached widget has never run (cache is absent), the runtime
paints a `⏳ …` loading placeholder in the widget's slot and kicks off
the background fetch. The next render picks up the fresh payload.
### Trust placeholder
For local project dashboards (`./.splashboard/dashboard.toml`),
`Network` fetchers are gated behind `splashboard trust`. An untrusted
Network widget renders a `🔒 requires trust` placeholder instead of
running. See [Trust model](/guides/trust/).
### Empty-state placeholder
Anything whose body is empty (`TextBlock` with no lines, `Entries`
with no items, `NumberSeries` with no values, …) short-circuits to a
shared "nothing here yet" placeholder *before* renderer dispatch, so
every widget handles "no data" the same way. `Ratio` and `Calendar`
are exceptions — `0% disk used` and "today's month" are both
legitimate.
### Error placeholder
An unknown renderer name, a shape/renderer mismatch, or a fetcher
failure all render an in-band error string. The splash never crashes
the user's prompt.
## Animation window
Most renderers produce the same output every frame, so splashboard
paints once and exits. When a widget's renderer declares
`animates() = true` (`animated_postfx`, `animated_typewriter`,
`animated_figlet_morph`, `animated_boot`, `animated_scanlines`,
`animated_splitflap`, `animated_wave`), the
runtime upgrades the draw phase into a multi-frame loop capped at
`ANIMATION_WINDOW` (2 seconds). The final frame is left static, so
the splash rests at the effect's end state rather than mid-motion.
`--wait` blocks until every cached widget has fresh data before
painting the first frame. Without it, the cache-first paint flashes
instantly and fresh data arrives on the next render.
## Config scopes
Which dashboard is in scope depends on where you are:
- **Per-directory** — walk up from CWD looking for
`./.splashboard/dashboard.toml` or `./.splashboard.toml`. First match
wins. Travels with a cloned repo.
- **Project fallback** — when CWD is inside a git repo root with no
per-directory override, `$HOME/.splashboard/project.dashboard.toml`.
- **Home** — everywhere else (not a git repo),
`$HOME/.splashboard/home.dashboard.toml`.
- **Baked-in defaults** — when none of the above exist, splashboard
renders its compiled-in fallback so the first run still shows
something.
Settings (`$HOME/.splashboard/settings.toml`) are global — padding,
theme, viewport height, etc. Dashboards only hold widgets + rows.