Lifecycle & system state
@ponchia/ui/css/state.css is an opt-in vocabulary for the states real apps
spend their time in — saving, saved, queued, offline, stale, conflicted, locked,
reviewed, and background work still running after the initiating interaction is
gone. These are usually improvised per product, so even good apps feel
inconsistent. This is the canonical set: a labelled state object with a rationed
tone, a page/document sync bar, and a persistent background-job row.
@import '@ponchia/ui';
@import '@ponchia/ui/css/state.css';
Bronto ships the visual states and the canonical wording. The host owns the state machine, retry policy, persistence, cancellation, and announcements. Persistent state deserves persistent UI — a toast is secondary, not the answer. The tone is a second channel; the label is the state, so it survives forced-colors and screen readers (WCAG 1.4.1).
.ui-state
A leading tone dot, a __label, and an optional __detail. Add --busy to
pulse the indicator for an in-progress state (reduced-motion-safe).
<span class="ui-state ui-state--saving ui-state--busy">
<span class="ui-state__label">Saving…</span>
</span>
<span class="ui-state ui-state--saved">
<span class="ui-state__label">Saved</span>
<span class="ui-state__detail">2m ago</span>
</span>
State matrix
Use the canonical label; the modifier bakes in the tone.
| State | Class | Canonical label | Tone | Busy? |
|---|---|---|---|---|
| Saving | ui-state--saving |
"Saving…" | accent | yes |
| Saved | ui-state--saved |
"Saved" / "All changes saved" | success | — |
| Queued | ui-state--queued |
"Queued" / "Pending" | muted | — |
| Offline | ui-state--offline |
"Offline" | warning | — |
| Stale | ui-state--stale |
"Out of date" | warning | — |
| Conflict | ui-state--conflict |
"Conflict" | danger | — |
| Error | ui-state--error |
"Failed" / "Couldn't save" | danger | — |
| Locked | ui-state--locked |
"Locked" / "Read-only" | muted | — |
| Reviewed | ui-state--reviewed |
"Reviewed" | success | — |
| Needs review | ui-state--needs-review |
"Needs review" | warning | — |
"Syncing" and "Retrying" are the saving tone with their own label — use
ui-state--saving ui-state--busy and write the word.
.ui-syncbar
A page- or document-level status strip: a state on one side, optional actions on the other.
<div class="ui-syncbar">
<span class="ui-state ui-state--saved">
<span class="ui-state__label">All changes saved</span>
<span class="ui-state__detail">just now</span>
</span>
<button class="ui-button ui-button--subtle ui-button--sm" type="button">View history</button>
</div>
.ui-job
A durable row for background jobs, imports, exports, sync runs, and pipelines. It is deliberately not a task runner: the host owns polling, retry/cancel semantics, queue position, partial failures, and completion messages. Bronto paints a persistent status/progress object so long-running work is not hidden in a transient toast.
<article
class="ui-job ui-job--running"
style="--job-progress: 64%"
aria-labelledby="job-title"
>
<div class="ui-job__head">
<h3 class="ui-job__title" id="job-title">Importing listings</h3>
<span class="ui-state ui-state--saving ui-state--busy">
<span class="ui-state__label">Running</span>
<span class="ui-state__detail">64%</span>
</span>
</div>
<p class="ui-job__body">124 of 194 records processed. Latest checkpoint saved.</p>
<div
class="ui-job__progress"
role="progressbar"
aria-label="Import progress"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="64"
>
<span class="ui-job__bar"></span>
</div>
<div class="ui-job__actions">
<button class="ui-button ui-button--subtle ui-button--sm" type="button">View log</button>
<button class="ui-button ui-button--ghost ui-button--sm" type="button">Cancel</button>
</div>
</article>
Use the written state as the source of truth:
| State | Class | Typical label | Use when… |
|---|---|---|---|
| Queued | ui-job--queued |
"Queued" | The job is accepted but not running yet. |
| Running | ui-job--running |
"Running" / "Syncing" | Work is actively progressing. |
| Blocked | ui-job--blocked |
"Blocked" / "Waiting" | Work cannot proceed without another system or user action. |
| Failed | ui-job--failed |
"Failed" | The job stopped and needs retry, inspection, or acknowledgement. |
| Complete | ui-job--complete |
"Complete" | Work finished and the result is available. |
For determinate jobs, set --job-progress as a percentage on .ui-job and
put role="progressbar" plus aria-valuenow/min/max on .ui-job__progress.
For indeterminate jobs, omit the progress block or omit aria-valuenow, and
make the written state clear ("Running", "Waiting for worker", "Retrying").
Use ui-job--compact for dense queues.
Severity — .ui-severity
.ui-state answers what is this thing doing. Severity answers how bad is
it. Bronto already shipped the tones as per-component modifiers, but never the
scale — the tier names, their order, and the attribute carrying them. So
every consumer invents the ladder, and inside one app it drifts: one surface
saying critical|error|warning|note, the next bad|warn, a third
critical|warning|info|ok, under two different attribute names. Findings then
do not sort against alerts, and a filter written for one list misses the other.
The ladder, worst to best:
| Level | Means |
|---|---|
critical |
Broken now, and still losing something. |
error |
Something failed; it is not currently getting worse. |
warning |
A threshold was crossed; nothing has failed yet. |
notice |
Worth reading, no action implied. |
ok |
Checked and healthy — an assertion, not the absence of news. |
unknown |
Not measured, stale, or the check itself failed. |
unknown sits outside the ordering on purpose. It is not "slightly worse
than ok", it is "we do not know" — and collapsing it into ok is how a dead
collector reads as a healthy system. SEVERITY_LEVELS therefore excludes it.
The level travels on data-level, one attribute name, so the same selector
works on a chip, a row, a dot, or your own element via var(--severity-tone):
<span class="ui-severity" data-level="critical">Critical</span>
<li class="ui-severity-row" data-level="warning">
<span class="ui-severity-row__title">Disk 84% on kpi-1</span>
<span class="ui-severity-row__meta">12m</span>
</li>
Colour is never the only channel (WCAG 1.4.1): .ui-severity carries an
author-written label, and .ui-severity-dot is only for rows that also name
their level in text.
Painting a mark Bronto does not ship — .ui-severity-tone
The three classes above all draw something: a chip, a ruled row, a background
dot. A host painting its own mark — an SVG shape, a gradient stop, a canvas —
needs the tone as a value with none of that. .ui-severity-tone paints
nothing and only resolves --severity-tone from data-level:
<circle class="ui-severity-tone" data-level="critical" fill="var(--severity-tone)" />
Reach for it when background is not the property you need. An SVG shape
ignores background entirely, so without this a consumer restates the whole
tier table in fill — and then it can drift from the rows beside it.
Timestrip — .ui-timestrip
A status list answers what. The strip answers when, on one axis, at a
glance: a cluster of marks at the leading edge is a fresh incident, a lone mark
pinned to the trailing edge has been wrong for a while. It takes the same
data-level as the ladder, so a strip cannot disagree with the rows under it.
<div class="ui-timestrip" role="img" aria-label="When these alerts started, last 24 hours">
<span class="ui-timestrip__axis"></span>
<span class="ui-timestrip__now"></span>
<span class="ui-timestrip__event" data-level="critical" style="--at: 0.92"></span>
<span class="ui-timestrip__event" data-level="warning" style="--at: 0.4"></span>
<span class="ui-timestrip__event" data-level="ok" data-outside style="--at: 0"></span>
</div>
This is geometry, not a chart. The host owns the window, the clock, and the
arithmetic: it normalises each event to a position in 0..1 and writes it as
--at, where 0 is the oldest edge and 1 is now. Bronto owns the rail, the
now marker, and the mark. There are no scales, no ticks, no axis labels, and no
time parsing — a surface that wants those wants a chart, and Bronto
refuses chart scales.
data-outside marks an event older than the window. It stays at the edge at
reduced weight rather than vanishing, because a strip reading "quiet" while a
row says "failing for three days" is worse than one reading crowded.
The strip is decorative on its own: give it an aria-label naming what it
plots, and let the rows beneath it name each level in words.
Recipe
import { ui, severity, SEVERITY_LEVELS } from '@ponchia/ui/classes';
ui.state({ state: 'saving', busy: true }); // "ui-state ui-state--saving ui-state--busy"
ui.state({ state: 'conflict' }); // "ui-state ui-state--conflict"
ui.job({ state: 'running' }); // "ui-job ui-job--running"
// Bundles the class WITH data-level, so the attribute that carries the meaning
// cannot be forgotten — the class alone paints the neutral tone and silently
// loses the level.
severity('critical'); // { class: 'ui-severity', 'data-level': 'critical' }
severity('warning', { part: 'row' }); // { class: 'ui-severity-row', … }
severity('nope'); // { class: 'ui-severity', 'data-level': 'unknown' }
severity('critical', { part: 'tone' }); // { class: 'ui-severity-tone', … }
SEVERITY_LEVELS; // ['critical','error','warning','notice','ok'] — sort/filter from this
Scope
CSS only — there is no JS yet. Auto-updating elapsed time ("2m ago"), live progress text, polling, cancellation, retry, and conflict-resolution affordances are the host's job. A small optional behavior may come later if a real consumer needs it.
Related
Use the reporting toolbox
to place state.css in report surfaces, and use
reference.md for the generated class catalog.