Skip to main content

Crate makeover_timing

Crate makeover_timing 

Source
Expand description

The time axis of the make-family design system.

makeover resolves colour, makeover-geometry resolves distance, and makeover-layout names what a thing is without resolving anything. This crate answers the remaining question of the same shape: how long.

The move is the one the family makes everywhere. A duration is named by what it is waiting for, and the number follows:

IntentWhat it waits forResolves to
Intent::Reverta control returning to its resting label after confirming1500ms
Intent::Cleara status line emptying itself2000ms
Intent::Dismissa transient notice’s lifetime3000ms
Intent::Debouncetyping settling before the work starts150ms

Whether a toast should live 3000ms or 3500ms is unanswerable on its own. Whether a message is a toast or a banner is not. That is the whole argument for the layer, and it is the same one Gap makes about six pixels.

§One duration per intent, on every renderer

An intent resolves to exactly one duration everywhere. Not one per renderer, not one per theme.

The alternative was considered and rejected on 2026-08-21: a per-renderer table, resolving the way Density resolves gaps. Distance has a renderer axis because a fingertip is coarser than a cursor and a terminal cell is coarser than a pixel — the surface differs. Time does not differ that way. A second is a second in a browser, in egui and in a terminal, and the reader waiting it out is the same reader. What looked like a renderer disagreement in the tree turned out to be drift: MNW debounced one typeahead at 150ms and two others at 200ms, in one repo, on one renderer, and no per-renderer table would have caught that.

So a divergence here is a bug report, not an axis.

§What is deliberately not a timing intent

Three classes were measured out of scope on 2026-08-18, and leaving them out is most of what makes the four above coherent.

A race is not an intent. Waiting 300ms before navigating because the write “should” have landed, or 150ms before closing a dropdown so a mousedown can beat the blur, is a synchronisation bug wearing a duration’s clothes. Naming those would launder them into design decisions and give every future one a token to hide behind. They get fixed per screen.

Severity is not an intent either, and it is not a fifth number. Two renderers already reached for one: MNW gives an error toast 6000ms against an ordinary one’s 3000ms, and audiofiles’ footer never expires an error at all while an ordinary message goes at 30s. Read together those are not two durations, they are one statement — a message the user must not miss does not go away on its own — and makeover-layout already has the word for it: such a message is not Notice::transient. It is a banner, and a banner has no lifetime. See notice_lifetime.

A poll interval is not an intent. A retry backoff, a health check, an update check: those are answerable from what they talk to, not from what a reader can follow, and nothing here has an opinion about them.

§Motion is a separate axis

Intent says how long a state lasts. Motion says how long a change takes. CSS itself draws that line — a setTimeout against a transition-duration — and folding the two together is what makes a “timing scale” unusable: 300ms of fade and 3000ms of toast are not two rungs of one ramp, they are answers to different questions.

Motion has one rung today because the tree has one measured transition. It is an enum rather than a constant so the second one has somewhere to go, and it grows when something is measured, not when a scale looks short.

§Cadence is a third question

Intent is how long a state lasts and Motion is how long a change takes. Both are one-shot: something starts, it ends. Cadence is how often a repeating mark repeats, which neither of the other two can answer without lying about its own shape.

The crate header’s exclusion of poll intervals does not cover it, and the difference is the same one that exclusion rests on. A backoff or a health check is answerable from what it talks to. How fast a mark may blink before it reads as an alarm is answerable from the reader, which is what every rung in this crate is answerable from.

§Reduced motion

The first thing in the family to have a motion-off path, opened here because Cadence is the first token whose whole existence is movement. A duration that is not animating anything is unaffected by it: Intent::Debounce is a wait, not a stroke, and reducing motion does not make input settle faster.

Two halves, because a browser and an egui window learn about the preference differently. A web surface gets a prefers-reduced-motion block in the generated stylesheet and needs no code. Every other renderer asks activity_blink with the bool its own platform gave it, exactly as it asks notice_lifetime with the bool the description gave it.

§Where the numbers came from

Every value below is a count from the tree, taken 2026-08-18 and re-checked 2026-08-21, not a preference:

revert     1500ms   MNW: 6 hand-rolled sites, plus core/clipboard.ts's own default
clear      2000ms   MNW: 4 sites
dismiss    3000ms   MNW: the toast renderer's lifetime
debounce    150ms   audiofiles SEARCH_DEBOUNCE, MNW docs-search.js
fade        300ms   MNW: TOAST_FADE_MS, matching the .fade-out transition

The one contested value is the debounce, where MNW’s two category typeaheads sit at 200ms against everything else’s 150ms. 150 wins on the count and on the cross-renderer agreement, and the 200s conform.

Cadence::Activity is the exception and the only rung here not counted off the tree, because the mark it times does not exist yet: wiki loading-and-progress-standard settled on 2026-08-26 that an unmeasured wait blinks rather than spins, and nothing had drawn one. Writing it here before the three renderers reach for it is the point of the task that added it. What it is answerable to is stated on the member.

§Consumers

Web surfaces bake timing_css in at build time. Nothing here changes at runtime, so there is no load-time JS step, exactly as with geometry. egui and ratatui surfaces read Intent::duration instead, which is why this is a crate rather than a stylesheet.

Enums§

Cadence
How often a repeating mark repeats.
Intent
A duration named by what it is waiting for.
Motion
How long a change takes, as opposed to how long a state lasts.

Functions§

activity_blink
How fast the activity mark blinks, given whether the reader has asked for less motion.
notice_lifetime
How long a notice lives, given whether the description calls it transient.
reduced_motion_css
The motion-off block, for a reader who has asked for less of it.
timing_css
The time axis as a stylesheet, inside the family’s cascade layer.
timing_css_declarations
Emit the time axis as CSS declarations, no selector.
timing_css_vars
Emit the whole time axis: a :root { … } block, then the motion-off block.