Motion
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/design/references/motion.md |
| Description | Not specified |
Source Content
Motion
- Principles
- Duration and easing tokens
- The hard rule: name the properties
- prefers-reduced-motion is mandatory
- Reusable motion utility classes
- Tailwind animation classes
- One-shot vs stateful
- Performance
- The vendored asset
Motion is functional, not decorative. A transition exists to explain a state change — what just opened, what direction it came from, what is now active. If a motion does not make the interface easier to understand, delete it. Keep everything simple and fast: the default reaction to “should this animate?” is a short, scoped transition, not a bespoke keyframe sequence. Canonical decision: ADR-004.
Principles
- Explain, do not entertain. Motion shows cause and effect: a panel slides from the edge it is anchored to, a value nudges when it updates. Ornamental motion is noise.
- Fast by default. Most UI transitions belong in the 120–200ms band. Long durations are reserved for large surfaces (overlays, drawers) where a slow move reads as deliberate.
- Direction has meaning. Enters decelerate into place; exits accelerate away. Pair the easing to the direction (see below).
- Share, never re-author. Motion is a small set of named utilities, not a transition re-typed on every component — this is the same never-duplicate rule the whole skill follows (css/architecture.md).
- Controls never shift layout on hover/focus.
Duration and easing tokens
Durations and easings are CSS custom properties, so themes and config control them in one place and no component hardcodes a millisecond value. Source of truth: src/styles/motion.css + tailwind.config.ts.
/* tokens.css — the one place motion literals live */:root { /* Durations: fast by default; longer only for large surfaces */ --motion-instant: 80ms; /* immediate state flips: toggle, check */ --motion-fast: 120ms; /* control hover/press, icon swap */ --motion-standard: 160ms; /* default — most UI transitions */ --motion-emphasis: 200ms; /* dropdowns, selections */ --motion-overlay: 220ms; /* modals, drawers, large overlays */ --motion-slow: 280ms; /* progress fills, page-level changes */
/* Easings: match the curve to the direction of travel */ --ease-standard: cubic-bezier(0.22, 1, 0.36, 1); /* snappy enter — decelerate in */ --ease-exit: cubic-bezier(0.4, 0, 1, 1); /* fast exit — accelerate away */ --ease-gentle: cubic-bezier(0.16, 1, 0.3, 1); /* softer settle — overlays, dropdowns */}| Token | Value | Reduced-motion | Tailwind class | Use for |
|---|---|---|---|---|
--motion-instant | 80ms | 40ms | duration-instant | Immediate flips (toggle, checkbox) |
--motion-fast | 120ms | 60ms | duration-fast | Hover/press feedback, icon swap |
--motion-standard | 160ms | 80ms | duration-standard | Default for most UI transitions |
--motion-emphasis | 200ms | 100ms | duration-emphasis | Dropdowns, selection changes |
--motion-overlay | 220ms | 100ms | duration-overlay | Modals, drawers, large overlays |
--motion-slow | 280ms | 120ms | duration-slow | Progress fills, page-level changes |
--ease-standard | cubic-bezier(0.22, 1, 0.36, 1) | - | ease-standard | Enters — decelerate into place |
--ease-exit | cubic-bezier(0.4, 0, 1, 1) | - | ease-exit | Exits — accelerate away |
--ease-gentle (alias --motion-gentle) | cubic-bezier(0.16, 1, 0.3, 1) | - | ease-gentle | Overlays, dropdowns — soft settle |
--ease-linear | linear | - | ease-linear | Shimmer, sweep animations |
Transition property groups (Tailwind custom extensions)
| Class | Properties animated |
|---|---|
transition-control | color, background-color, border-color, box-shadow, opacity |
transition-indicator | transform, width, height, opacity, background-color, box-shadow |
transition-disclosure | color, background-color, border-color, box-shadow, opacity, transform |
transition-overlay | opacity, transform, box-shadow |
transition-value | left, width, height, opacity, color, box-shadow, background-color, transform |
The hard rule: name the properties
Never transition: all. Always name the exact properties that move. all animates things you never meant to — including layout — which causes jank and surprise repaints. Stylelint blocks the all keyword for you (css/linting.md).
/* button.css — control feedback only */.button { /* WRONG: .button { transition: all var(--motion-fast); } animates layout too */ /* RIGHT: name what moves, and pair ease-standard with the enter direction */ transition: background-color var(--motion-fast) var(--ease-standard), color var(--motion-fast) var(--ease-standard);}Pair --ease-standard with enters and on-states; pair --ease-exit with dismissals and off-states. The curve is half the message.
prefers-reduced-motion is mandatory
Every motion layer must honor @media (prefers-reduced-motion: reduce). Shorten the durations, drop transforms down to opacity-only, and stop infinite loops. Define it once at the token layer so every utility inherits the reduced timings for free — the design system already halves every --motion-* duration and drops transforms automatically.
/* tokens.css — one block reduces motion everywhere downstream */@media (prefers-reduced-motion: reduce) { :root { --motion-instant: 40ms; --motion-fast: 60ms; --motion-standard: 80ms; --motion-emphasis: 100ms; --motion-overlay: 100ms; --motion-slow: 120ms; }
/* Stop infinite/decorative loops from running forever */ *, *::before, *::after { animation-iteration-count: 1 !important; }}Stylelint’s a11y/media-prefers-reduced-motion rule verifies that a file using motion includes a reduce query (css/linting.md).
Reusable motion utility classes
The vendored asset defines a small set of .motion-* utility classes. Each bundles a scoped transition + a duration token + an easing token under one name, so a component opts into shared motion with a single class instead of re-typing a transition.
| Family | Example utilities | What it does |
|---|---|---|
| Controls | .motion-control, .motion-control-emphasis, .motion-indicator, .motion-value | Hover/press feedback on buttons and inputs |
| Overlays | .motion-overlay, .motion-overlay-panel, .motion-dropdown-panel | Open/close for popovers, dropdowns, modals |
| Disclosure | .motion-disclosure, .motion-chevron | Expand/collapse affordances, rotating carets |
| Accordion | .motion-accordion-panel (+ -body inner wrapper) | Height reveal via grid-template-rows: 0fr → 1fr |
| Drawer | .motion-drawer-panel, .motion-drawer-panel-emphasis | Edge-anchored slide-in sheets keyed off data-state/data-side |
| List/row | .motion-list-row, .motion-table-row, .motion-surface-card, .motion-sticky | Subtle row/surface state changes |
| Value | .motion-bar-fill, .motion-gauge | Progress/metric transitions |
| iOS patterns | .motion-ios-push, .motion-ios-sheet, .motion-ios-banner, .motion-ios-alert, .motion-ios-collapse, .motion-ios-zoom, .motion-ios-flip, .motion-ios-fade | Long-form iOS navigation/sheet patterns for mobile-first surfaces (each ~400–620ms, gentle/cubic-bezier(0.32, 0.72, 0, 1)) |
| Interactive | .motion-interactive (transform+opacity+shadow+color, fast), .motion-press (active-state opacity 0.68) | General clickable feedback |
| Stagger | .motion-stagger (30ms × index on first 5 children) | Sequenced list entrances, pair with .motion-pop per item |
The full set — including overlay-direction variants, drawer offsets, and platform sheet primitives — lives in ./assets/motion.css. Copy and rename to taste; treat it as a worked reference, not a hard dependency.
Tailwind animation classes (animate-*)
One-shot enter/exit animations exposed via tailwind.config.ts:
| Class | Duration / Easing | Effect |
|---|---|---|
animate-motion-fade-in / -fade-out | standard / fast, ease-standard / ease-exit | Opacity 0↔1 |
animate-motion-scale-in / -scale-out | overlay / fast | Scale 0.985↔1 + fade |
animate-motion-slide-up / -slide-down | standard, ease-standard | Slide 0.5rem + fade |
animate-motion-pop | emphasis, gentle | Scale 0.92→1.02→1 bounce |
animate-motion-shake | emphasis, ease-standard | X shake ±3px |
animate-motion-shimmer / -highlight-sweep | 1.5s/1.4s, linear, infinite | Background sweep |
animate-motion-value-change | standard, ease-standard | Slide-up + fade for numbers |
animate-motion-status-pulse | emphasis, ease-standard | Box-shadow ring pulse |
animate-motion-blur-in / -blur-out | overlay/fast, gentle/ease-exit | Blur + scale + fade |
animate-motion-wiggle-soft | emphasis, gentle | ±2deg rotate |
animate-motion-bounce-in | 520ms, gentle, both | Drop with overshoot |
animate-motion-float / -breathe | 3s / 2.4s, ease-in-out, infinite | Idle ambient motion |
animate-motion-pulse-scale / -glow-flash | emphasis/700ms | Scale or ring flash emphasis |
animate-motion-stroke-in | 600ms, ease-standard, forwards | SVG path draw (pathLength="1") |
Also available via the tailwindcss-animate plugin: animate-in / animate-out, fade-in / fade-out, zoom-in-95 / zoom-out-95, slide-in-from-* / slide-out-to-*.
One-shot vs stateful
Stateful — use a transition
A property that toggles between two states (open/closed, active/inactive, hover/rest) is a transition. It animates whenever the value changes and reverses cleanly. Reach for a .motion-* utility.
One-shot — use a keyframe animation
An entrance, exit, or attention cue that plays once is an @keyframes animation. Define the keyframes once, then expose them through an animate-*-style utility class. Respect reduced motion by collapsing transform-based frames to a plain fade.
/* motion.css — one-shot entrance, defined once, reused by class */@keyframes motion-fade-in { from { opacity: 0; transform: translateY(0.5rem); } to { opacity: 1; transform: translateY(0); }}
.animate-fade-in { /* enter direction → ease-standard; duration from a token, never a literal */ animation: motion-fade-in var(--motion-standard) var(--ease-standard);}
@media (prefers-reduced-motion: reduce) { .animate-fade-in { /* drop the transform; fade only, faster */ animation: motion-fade-in var(--motion-fast) var(--ease-standard); transform: none; }}Performance
- Animate
transformandopacity. They run on the compositor and do not trigger layout or paint, so they stay smooth under load. - Avoid animating layout properties —
width,height,top,left,margin. They force reflow every frame. Prefertransform: translate/scale, or thegrid-template-rows: 0fr → 1frtrick for height reveals (see the accordion utility). - Use
will-changesparingly. Hint only elements that are about to move (drawers, large overlays), and only while they are in motion. Leaving it on permanently wastes memory and can hurt the very performance it promises.
The vendored asset
| Asset | What it is | How to use it |
|---|---|---|
| ./assets/motion.css | Reusable motion utility + keyframe layer (tokens, .motion-* utilities, @keyframes, reduced-motion fallbacks) | Copy into your project and adapt the tokens to your own scale and timings |