Skip to content

Motion

FieldValue
TypeSkill Resource
Source~/.copilot/skills/design/references/motion.md
DescriptionNot specified

Source Content

Motion

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 */
}
TokenValueReduced-motionTailwind classUse for
--motion-instant80ms40msduration-instantImmediate flips (toggle, checkbox)
--motion-fast120ms60msduration-fastHover/press feedback, icon swap
--motion-standard160ms80msduration-standardDefault for most UI transitions
--motion-emphasis200ms100msduration-emphasisDropdowns, selection changes
--motion-overlay220ms100msduration-overlayModals, drawers, large overlays
--motion-slow280ms120msduration-slowProgress fills, page-level changes
--ease-standardcubic-bezier(0.22, 1, 0.36, 1)-ease-standardEnters — decelerate into place
--ease-exitcubic-bezier(0.4, 0, 1, 1)-ease-exitExits — accelerate away
--ease-gentle (alias --motion-gentle)cubic-bezier(0.16, 1, 0.3, 1)-ease-gentleOverlays, dropdowns — soft settle
--ease-linearlinear-ease-linearShimmer, sweep animations

Transition property groups (Tailwind custom extensions)

ClassProperties animated
transition-controlcolor, background-color, border-color, box-shadow, opacity
transition-indicatortransform, width, height, opacity, background-color, box-shadow
transition-disclosurecolor, background-color, border-color, box-shadow, opacity, transform
transition-overlayopacity, transform, box-shadow
transition-valueleft, 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.

FamilyExample utilitiesWhat it does
Controls.motion-control, .motion-control-emphasis, .motion-indicator, .motion-valueHover/press feedback on buttons and inputs
Overlays.motion-overlay, .motion-overlay-panel, .motion-dropdown-panelOpen/close for popovers, dropdowns, modals
Disclosure.motion-disclosure, .motion-chevronExpand/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-emphasisEdge-anchored slide-in sheets keyed off data-state/data-side
List/row.motion-list-row, .motion-table-row, .motion-surface-card, .motion-stickySubtle row/surface state changes
Value.motion-bar-fill, .motion-gaugeProgress/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-fadeLong-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:

ClassDuration / EasingEffect
animate-motion-fade-in / -fade-outstandard / fast, ease-standard / ease-exitOpacity 0↔1
animate-motion-scale-in / -scale-outoverlay / fastScale 0.985↔1 + fade
animate-motion-slide-up / -slide-downstandard, ease-standardSlide 0.5rem + fade
animate-motion-popemphasis, gentleScale 0.92→1.02→1 bounce
animate-motion-shakeemphasis, ease-standardX shake ±3px
animate-motion-shimmer / -highlight-sweep1.5s/1.4s, linear, infiniteBackground sweep
animate-motion-value-changestandard, ease-standardSlide-up + fade for numbers
animate-motion-status-pulseemphasis, ease-standardBox-shadow ring pulse
animate-motion-blur-in / -blur-outoverlay/fast, gentle/ease-exitBlur + scale + fade
animate-motion-wiggle-softemphasis, gentle±2deg rotate
animate-motion-bounce-in520ms, gentle, bothDrop with overshoot
animate-motion-float / -breathe3s / 2.4s, ease-in-out, infiniteIdle ambient motion
animate-motion-pulse-scale / -glow-flashemphasis/700msScale or ring flash emphasis
animate-motion-stroke-in600ms, ease-standard, forwardsSVG 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 transform and opacity. They run on the compositor and do not trigger layout or paint, so they stay smooth under load.
  • Avoid animating layout propertieswidth, height, top, left, margin. They force reflow every frame. Prefer transform: translate/scale, or the grid-template-rows: 0fr → 1fr trick for height reveals (see the accordion utility).
  • Use will-change sparingly. 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

AssetWhat it isHow to use it
./assets/motion.cssReusable 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