Skip to content

Motion

Duration and easing are separate tokens. Pick one of each. Patterns ship a default pair so the common case is already decided, and you can change speed or curve without a new token.

KindDefault durationDefault easingUse for
Chrome--af-duration-fast--af-ease-standardHover, border, text, shadow
Movement--af-duration-md--af-ease-outDrawer, accordion height, entering panels
Keyframes--af-animation-duration-*--af-ease-linear or --af-ease-in-outSpinner, skeleton

Swap either token when the default is wrong. A faster drawer is --af-duration-fast with the same --af-ease-out. A stronger hover is --af-ease-emphasized with the same --af-duration-fast. Do not write ms or cubic-bezier(...) in product CSS.

Two duration families:

FamilyForExamples
--af-duration-*CSS transitionsHover color, border, accordion height, drawer
--af-animation-duration-*CSS keyframe animationsSpinner, skeleton, entrance keyframes
TokenDefaultUse for
--af-duration-fast120msHover, color, border, opacity, focus chrome
--af-duration-md200msDefault layout motion (panels, accordion)
--af-duration-slow320msLonger panel / emphasis transitions
TokenDefaultUse for
--af-animation-duration-fast200msShort micro-motion
--af-animation-duration-md400msDefault keyframe length
--af-animation-duration-slow600msSpinners, short loops
--af-animation-duration-slower1500msSkeleton shimmer, long loops

Hover each swatch. Same property change; different --af-duration-*.

<button class="demo-motion-swatch" style="transition-duration: var(--af-duration-fast)">Fast</button>
<button class="demo-motion-swatch" style="transition-duration: var(--af-duration-md)">Md</button>
<button class="demo-motion-swatch" style="transition-duration: var(--af-duration-slow)">Slow</button>
TokenDefaultUse for
--af-ease-standardcubic-bezier(0.2, 0, 0.2, 1)Default UI transitions (buttons, fields)
--af-ease-emphasizedcubic-bezier(0.2, 0, 0, 1)Stronger decelerate / emphasis
--af-ease-outcubic-bezier(0, 0, 0.2, 1)Entering panels, drawer slide, accordion
--af-ease-incubic-bezier(0.4, 0, 1, 1)Exiting / accelerating away
--af-ease-in-outcubic-bezier(0.4, 0, 0.2, 1)Symmetric loops (skeleton)
--af-ease-linearlinearProgress, indeterminate spin

Same duration (--af-animation-duration-slower); each row uses a different --af-ease-*. Watch how the dots accelerate.

  • standard

  • emphasized

  • out

  • in

  • in-out

  • linear

animation: demo-ease-travel var(--af-animation-duration-slower) var(--af-ease-standard) infinite alternate;

Transitions (most pattern chrome):

.af-btn {
  transition:
  background-color var(--af-duration-fast) var(--af-ease-standard),
  border-color var(--af-duration-fast) var(--af-ease-standard),
  color var(--af-duration-fast) var(--af-ease-standard);
}

Keyframes (Spinner, Skeleton):

.af-spinner {
  animation: af-spin var(--af-animation-duration-slow) var(--af-ease-linear) infinite;
}

.af-skeleton {
  animation: af-skeleton-load var(--af-animation-duration-slower) var(--af-ease-in-out) infinite;
}

Layout motion (Drawer slide, Accordion):

transition:
transform var(--af-duration-md) var(--af-ease-out),
height var(--af-duration-md) var(--af-ease-out);

Airframe respects prefers-reduced-motion: reduce in two places:

  1. Tokens (@airframeui/tokens) — every --af-duration-* and --af-animation-duration-* becomes 0.01ms, including values you override in :root.
  2. Base (@airframeui/core) — transition-duration / animation-duration forced near zero on all elements; animation-iteration-count: 1; scroll-behavior: auto.

Override timing in :root when you need a different product feel — reduced-motion still collapses those overrides:

:root {
  --af-duration-fast: 80ms;
  --af-duration-md: 160ms;
  --af-animation-duration-slower: 2s;
}

Force reduced motion in a subtree (previews, demos, dense admin chrome) with af-reduce-motion:

Default

af-reduce-motion

<div class="af-reduce-motion">
  <span class="af-spinner" aria-hidden="true"></span>
</div>

Demos on this page: If your OS prefers reduced motion, the hover comparisons above will snap. Turn the preference off (or use a browser without it) to feel the curves.

  1. Two tokens. A transition sets one --af-duration-* and one --af-ease-*. Change either. No hardcoded ms or cubic-bezier outside the token file. validate_markup rejects raw timing.
  2. Start from the default pair. Chrome is fast + standard. Movement is md + out. Keyframes use --af-animation-duration-*.
  3. Do not add a shorthand. Duration and easing stay separate so a product can retune speed without retuning the curve.
  4. Do not disable reduced motion. Do not set long durations under @media (prefers-reduced-motion: reduce). The token collapse and base rules are the contract.