Easing curves, durations, and prebuilt keyframe animations. All motion must respect
prefers-reduced-motion — see the pattern below.
Write durations as literal values and use tokens for easing curves.
Open Props does ship a --duration-* scale, but it lives in
open-props/extra/durations.css, which
/ui/ui.css does not import — so
var(--duration-quick-1) resolves to nothing and the browser drops the
whole declaration. The browser reports no error, so this can look like a very fast transition.
Choose the nearest value from those already used in the styles:
.12s — a colour change on a small control: a chip, an option row, a hovered surface..15s–.2s — the default for border, background, opacity, rotate, transform. .2s is by far the most common value in the codebase..3s–.4s — movement of a whole element: a side panel, a toast, a dialog..5s — an expressive animation with an elastic or spring curve.
When several rules inside one component share a number, name it once as a
component-local private prop instead of repeating the literal. The side panel does
this with --_duration: its slide and its chevron rotation both read it,
so the two cannot drift apart. See
Tokens § "Pattern: component-local private props".
--ease-*--ease-1..--ease-5 — default curves (gentle → sharp).--ease-in-1..5, --ease-out-1..5, --ease-in-out-1..5 — directional.--ease-elastic-1..5, --ease-spring-1..5, --ease-bounce-1..5 — specialty.--animation-*
Prebuilt keyframes: --animation-fade-in, --animation-fade-out,
--animation-slide-{in,out}-{up,down,left,right}, --animation-scale-up,
--animation-spin, --animation-ping, --animation-blink,
--animation-bounce, shake-x, and more.
Wrap transitions and keyframe animations in
@media (prefers-reduced-motion: no-preference) so users who opt out get static UI.
Final-state rules apply regardless.
Write that query out in full — @media (--motionOK) never matches here.
Open Props names the same query as a @custom-media declaration in
props.media.css, and no browser resolves
@custom-media by itself: it takes a build step, and this project serves its CSS as
written. The browser drops the declaration, then drops every rule inside
@media (--motionOK) — so the transition never runs, regardless of the preference, and the browser reports no error. Put the Open Props name in a comment beside the standard query, the way
the named breakpoints are written (Breakpoints, section
"Open Props and @custom-media").
Five named animations built on Open Props tokens, shipped in /ui/ui.css.
Live demos: Components · Feedback · Status messages.
Use play_once(el, class) from lib/animation.mjs for an animation that runs once. Adding an existing class does not restart it. Removing and re-adding the class within one task also fails, because the browser computes styles only after the task finishes.
.status-success.status-errorshake-x keyframes). Disabled under reduce-motion..reveal-flash--info tint + subtle scale, played by reveal(el), which scrolls the element into view first. This flash identifies the selected element without changing its own colors. Under reduce-motion the tint lasts 1.2 s and the scale bounce is disabled. Keeping the tint helps users identify the element after scrolling. The JavaScript helper also checks this preference when choosing scroll behavior. This avoids relying on every caller to override the default scroll-behavior value of auto..attention-pulse--brand ring expanding out of the control, played by play_once(el, class, {pseudo: '::after'}). This draws attention to a control that received focus after a pointer gesture, when :focus-visible does not match and no ring is painted. It runs on ::after rather than the element, so it can run even when the host declares animation or all of its own; the pseudo argument lets play_once size its fallback timer. Under reduce-motion the ring remains visible for 1.2 s without growing. See Status messages..spinspin(icon, active) from lib/ui.mjs to toggle. This animation continues under reduced motion to indicate activity. If an application overrides --animation-spin with a faster rate, it restores the slower rate under @media (prefers-reduced-motion: reduce).var(--ease-2), never a hand-written cubic-bezier().animation rule ignores prefers-reduced-motion..spin loading spinner.