RoadieRoadie
  • Home
  • Brand
  • Foundations
  • Content
  • Tokens
  • Components
  • Charts
  • Widgets
  • Appearance
HomeBrandFoundations
Appearance

Foundations

  • Visual

    • Colors
    • Elevation
    • Iconography
    • Shape
    • Theming
  • Content

    • Date and time
    • Layout
    • Prose styles
    • Typography
  • Behaviour

    • Accessibility
    • Interactions
    • Motion
    • View transitions
  • Building apps

    • Forms
    • Linking
    • Navigation
    • Performance
    • Records modelAlpha
    • Tables

Motion

Docs updated
20 Oct 2026

ReferenceMotion tokens

Motion is feedback. Every transition tells the user what happened, what's happening, or where to look. If it doesn't do one of those jobs, cut it.

Motion principles

Three principles define how Oztix products move. Every animation decision should trace back to one of them.

Responsive

Motion reacts to input at once. Like a crew backstage, things happen when they should.

Grounded

Elements feel like they have weight. Things land where they belong, with just enough give to feel real.

Efficient

Every animation earns its place. If it doesn't orient, direct, or confirm, cut it.

Duration scale

Duration scales with distance and size. A toggle switch moves fast, and a drawer needs more time. Match the duration to the physical scale of the element. On the docs site, each duration is a bar scaled to the longest, with its value, its class, and its job.

  1. --duration-instant0msduration-instant
    Use for a state change that shouldn't animate
  2. --duration-fastest50msduration-fastest
    Use for a change that should feel immediate without jumping
  3. --duration-fast100msduration-fast
    Quick exits, such as Navigator's labels fading out
  4. --duration-normal150msduration-normal
    Focus rings, the press scale, and popups
  5. --duration-moderate200msduration-moderate
    Hover colour and shadow, disclosures, dialogs, and the error shake
  6. --duration-slow300msduration-slow
    Drawers, pop-in entrances, and toasts leaving
  7. --duration-slower400msduration-slower
    Toasts entering and restacking
  8. --duration-slowest600msduration-slowest
    One-shot attention cues, such as a nudge or a pop
  9. --duration-ambient1800msNone
    One loop of a tint pulse or an indeterminate bar
  10. --duration-sweep2400msNone
    One pass of the shimmer across loading surfaces
  11. --stagger-base30msNone
    The delay between items, times each item's index

duration-instant to duration-slowest are Tailwind classes. The two ambient durations and --stagger-base have no class, so read them with var(), as the loading utilities do.

TierStepsUse for
Microduration-fastest to duration-normalFocus, press, and popups
Componentduration-moderate and duration-slowHover colour, disclosures, dialogs, and drawers
Pageduration-slower and duration-slowestToasts, and the nudge and pop cues
Ambient--duration-ambient and --duration-sweepOne loop or pass of a loading animation

Easing curves

Easing gives motion its character. The three curves suit any property, and the two springs are for transforms only. On the docs site, each easing is drawn as its curve over a dashed line at its end value, with its value, its class, how far it overshoots, and its job.

  1. --ease-standardcubic-bezier(0.4, 0, 0.2, 1)
    ease-standardOvershoot None
    Colour, opacity, and shadow, and popups entering and leaving
  2. --ease-entercubic-bezier(0, 0, 0.2, 1)
    ease-enterOvershoot None
    Elements arriving and form fields changing state. Starts fast, settles gently.
  3. --ease-exitcubic-bezier(0.4, 0, 1, 1)
    ease-exitOvershoot None
    Elements leaving. Starts slow, speeds up out.
  4. --ease-springlinear(0, 0.006, 0.025 2.8%, 0.101 6.1%, 0.539 18.9%, 0.721 25.3%, 0.849 31.5%, 0.937 38.1%, 0.968 41.8%, 0.991 45.7%, 1.006 50.1%, 1.015 55%, 1.017 63.9%, 1.001 85.9%, 1)
    ease-springOvershoot 1.7%
    Small transforms, such as the press scale and the tap pop. Too little overshoot to see as a bounce.
  5. --ease-spring-livelylinear(0, 0.012 1.7%, 0.044 3.3%, 0.092 5%, 0.223 8.3%, 0.607 16.7%, 0.744 20%, 0.859 23.3%, 0.949 26.7%, 1.015 30%, 1.073 35%, 1.094 40%, 1.085 46.7%, 1.008 66.7%, 0.991 81.7%, 1)
    ease-spring-livelyOvershoot 9.4%
    Transforms that should visibly bounce, such as toasts

--ease-spring peaks 1.7% past its end, too little to see as a bounce on a move under 100px, so it reads as a soft settle. --ease-spring-lively overshoots by about 9%, which shows as a bounce, so keep it for surfaces that should catch the eye, such as toasts.

Motion utilities

animate-* is a keyframe animation that plays as soon as the class is on the element. motion-* is a transition that plays as an element enters or leaves, on Base UI's data-starting-style and data-ending-style. Both compose with Tailwind's motion-safe: and motion-reduce: variants, and the reduced motion reset cuts both short, as Accessibility describes.

Duration classes

duration-* sets transition-duration from the scale. Select Play to run each bar over its duration.

function Durations() {
const [played, setPlayed] = useState(false)
const durations = [
'duration-fast',
'duration-normal',
'duration-moderate',

Easing classes

ease-* sets transition-timing-function from the curves. Each bar runs over duration-slowest, so the shape of each curve shows. The lively spring runs past the end of its track.

function Easings() {
const [played, setPlayed] = useState(false)
const easings = [
'ease-standard',
'ease-enter',
'ease-exit',

Entrances

Entrances play once when the element mounts. Select Replay to mount the cards again.

function Entrances() {
const [run, setRun] = useState(0)
return (
<div className='grid gap-4'>
<Button emphasis='normal' size='sm' className='justify-self-start' onClick={() => setRun(run + 1)}>
Replay
UtilityWhat it does
animate-fade-inFades in from transparent
animate-scale-inScales up from 95% as it fades in, on --ease-spring
animate-pop-inFades, slides up, and scales in. Pair with origin-* to set its edge

Attention cues

A cue plays once each time the class goes on, so remount the element, or remove and add the class, to replay it. Add a ticket to see animate-pop on the badge.

function AddToCart() {
const [count, setCount] = useState(2)
return (
<div className='flex flex-wrap items-center gap-3'>
<Button emphasis='strong' onClick={() => setCount(count + 1)}>
Add a ticket
UtilityWhat it does
animate-shakeShakes sideways, for a validation error or an invalid input
animate-nudgeNudges up and back, such as a cart that takes a new item
animate-popPops larger and back, such as a badge whose count changes
animate-pop-tapThe same pop, faster and on a spring, for feedback under a tap

Loading

Loading animations loop until the content arrives. Skeleton applies animate-shimmer, and Progress applies animate-indeterminate when it has no value.

{/* Pulse */}
<div className='h-10 rounded-xl emphasis-subtle animate-pulse-subtle' />
{/* Shimmer */}
<div className='h-10 rounded-xl emphasis-subtle animate-shimmer' />
{/* Indeterminate */}
<div className='relative h-2 overflow-hidden rounded-full emphasis-sunken'>
UtilityWhat it does
animate-pulse-subtlePulses the opacity, for a surface waiting on something
animate-shimmerPulses under a highlight anchored to the viewport, so every placeholder shares one sweep
animate-indeterminateSweeps a bar across a positioned parent that clips its overflow

Tailwind's animate-spin is only for a small status icon that stands in for a pending action, as Toast's loading icon does. Anything larger, or any bar or surface that waits, uses Progress or a loading utility above.

Enter and exit transitions

These transitions run on Base UI's starting and ending styles, so the exit plays before the element unmounts. Open the popover to see motion-scale.

<Popover>
<Popover.Trigger render={<Button>Set times</Button>} />
<Popover.Content>
<Popover.Header>
<Popover.Title>Feathered Anchor Sessions</Popover.Title>
<Popover.Description>Doors 7pm, first act 8pm.</Popover.Description>
UtilityWhat it doesUsed by
motion-scaleScales from 95% and fades. Pair with origin-* to scale from a pointMenu, Popover, and Tooltip
motion-slideFades and slides up on enter, down on exitDialog
motion-drawerSlides off its anchored edge and tracks a live swipeDrawer
motion-toastStacks, fans out, and tracks a live swipeToast
is-disclosure-animatedOpens and closes a <details> or collapsible panel by heightAccordion and Collapsible

Deprecated utilities

These go in v3. Swap each for its replacement.

UtilityReplacement
motion-fade-inanimate-fade-in
motion-scale-inanimate-scale-in
motion-pop-inanimate-pop-in
motion-fade-outmotion-scale or motion-slide
motion-scale-outmotion-scale or motion-slide

Motion patterns

Motion tokens are wired into the other foundations. These are the places they meet.

Interactive elements

is-interactive animates colour, shadow, outline, and transform from the motion tokens, so a button needs no transition of its own. Hover and press the button to see the colour shift and the press scale.

<Button emphasis='strong'>Buy tickets</Button>

--interactive-transition sets each property that animates, with its duration and its easing token.

  1. background-color200msease-standard
  2. border-color200msease-standard
  3. color200msease-standard
  4. box-shadow200msease-standard
  5. outline-width150msease-standard
  6. outline-color150msease-standard
  7. transform150msease-spring

Hover and focus colours follow the intent cascade, so a button inside intent-danger shifts to danger colours on the same durations and easings. is-interactive-field moves form fields on --ease-enter instead. See Interactions for the full breakdown.

Raised surfaces

emphasis-raised lifts and deepens its shadow on hover or keyboard focus only when it also has is-interactive, or is-interactive-within with a focused or hovered target. Their transition animates the change. On its own it has no transition. Hover or tab to each card to compare. A Card with an href takes is-interactive.

{/* Static */}
<Card emphasis='raised'>
<Card.Content>
<Card.Title>Tuxedo Possum Cabaret</Card.Title>
</Card.Content>
</Card>

The shadow moves from shadow-md to shadow-lg over duration-moderate on ease-standard, and the lift rides the transform on ease-spring. See Elevation for the shadow scale.

Staggered lists

Delay each item by --stagger-base times its index, so the eye follows the list in order. --stagger-base has no class. Select Replay to mount the lineup again.

function Lineup() {
const [run, setRun] = useState(0)
const acts = [
'Middle Kids',
'Hiatus Kaiyote',
'Gang of Youths',

Page transitions

A route change that moves the page uses the same tokens through the View Transitions API. The old page leaves over --duration-moderate on --ease-exit, and the new one arrives over --duration-slow on --ease-enter. See View transitions for the recipe.

Guidelines

  • Never block the interface. A user who selects Close on an opening dialog should see it close at once. Prefer transitions, which reverse from where they are, over keyframe animations, which play to the end.
  • Don't make anyone watch a two-second success animation before they can select Next. It's delightful once and infuriating the fiftieth time.
  • Test every animation with reduced motion on before you ship it, as Accessibility describes.
  • Don't force parallax or other scroll-driven motion on someone who has asked for reduced motion. Put it behind motion-safe:.

Give every animation a job

Motion shows cause and effect. If an animation doesn't orient, direct focus, give feedback, or show a state change, cut it.

Do

Scale a button down as it's pressed, to confirm the press landed. Scale a popup in from its trigger, to show what opened it. Each transition has one job.

Ochre Kite Weekender

Don’t

Don't bounce a heading on page load because it looks fun. Motion on static content rarely says anything.

Be specific about what moves

Precision prevents side effects. Name each property that transitions.

transition:
background-color var(--duration-moderate) var(--ease-standard),
transform var(--duration-normal) var(--ease-spring);

Do

List each property. is-interactive does this for interactive elements.

transition: all 150ms ease-out;

Don’t

Don't use transition-all. It animates padding, font size, and anything else that changes, which recalculates layout on every frame.

Use tokens, not magic numbers

Tokens keep motion consistent as the system grows. Hardcoded values drift.

transition:
background-color var(--duration-moderate) var(--ease-standard);

Do

Use the duration and easing tokens, so one change updates every component.

transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);

Don’t

Don't copy 0.2s cubic-bezier(0.4, 0, 0.2, 1) across 15 files. One typo and things feel off.

Springs for transforms, curves for colour

A spring suggests mass and momentum, which only makes sense for something that moves.

transition:
transform var(--duration-normal) var(--ease-spring),
background-color var(--duration-moderate) var(--ease-standard);

Do

Use ease-spring for small transforms, where it settles softly, and ease-spring-lively when the bounce should show. Use ease-standard for colour and opacity.

transition: opacity var(--duration-moderate) var(--ease-spring-lively);

Don’t

Don't put a spring on opacity or colour. Nothing about a colour change has momentum, and an overshoot on opacity just flickers.

Grow from where they are

An element that expands should expand from its anchor.

<div className='origin-[var(--transform-origin)] motion-scale'>…</div>

Do

Scale a popup from its trigger. Base UI sets --transform-origin, and Menu, Popover, Tooltip, Select, and Combobox already scale from it.

<div className='origin-center motion-scale'>…</div>

Don’t

Don't scale a popup from its centre when its trigger is in a corner. It breaks the link between the two.

Stagger, don't flood

Choreography creates hierarchy. Stagger related elements to guide the eye.

<li
className='animate-pop-in [animation-delay:calc(var(--stagger-base)*var(--index))]'
style={{ '--index': index }}
>

Do

Delay each item in a list by --stagger-base times its index, so the eye follows the sequence.

Don’t

Don't animate all 12 cards in a grid at once. The result is a flash, not a reveal.

Accessibility

Motion makes some people feel sick. For them it's a health issue, not a preference. When someone sets prefers-reduced-motion: reduce, Roadie's global reset sets these on every element and its ::before and ::after:

  • animation-duration and transition-duration to 0.01ms, not 0ms, so animationend and transitionend still fire for code that waits on them.
  • animation-iteration-count to 1, so a loop plays once and stops.
  • animation-delay and transition-delay to 0s, so a staggered list appears at once instead of waiting out its delays.
  • scroll-behavior to auto, so a smooth scroll jumps.

Two loading utilities hold a still state instead. animate-shimmer drops its gradient and keeps the tint, and animate-indeterminate holds a full-width bar at a soft tint. animate-pulse-subtle plays one short pass and stays at full opacity.

The reset only reaches CSS animations and transitions on elements. Animation run from JavaScript, such as the Web Animations API or a smooth scrollTo(), and the ::view-transition-* pseudo-elements have to check matchMedia('(prefers-reduced-motion: reduce)') themselves. Roadie's components do: Pane scrolls to the top without smoothing, Calendar skips its swipe animation, and Carousel stops autoplay and jumps between slides. A delay timed in JavaScript, such as the delay on Tooltip or a hover Popover, isn't motion and still waits.

Tailwind's motion-safe: and motion-reduce: variants swap a motion for a still alternative, rather than cutting it short.

To test, open the Rendering tab in Chrome DevTools and set "Emulate CSS media feature prefers-reduced-motion" to "reduce". On macOS, turn on Reduce motion in System Settings, under Accessibility and then Display.

Quick reference

How the motion tokens map to components. See Interactions for the full is-interactive and is-interactive-field breakdown.

WhatDurationEasingHow
Hover colour and shadowduration-moderateease-standardis-interactive
Press scaleduration-normalease-springis-interactive, at scale(0.99)
Field hover and focusduration-moderateease-enteris-interactive-field
Focus ringduration-normalease-standard, or ease-enter on a fieldOutline width and colour
Raised surface liftduration-moderate shadow, duration-normal liftease-standard shadow, ease-spring liftemphasis-raised with is-interactive
Caret rotationduration-moderateTailwind's default, the ease-standard curveSelect and Combobox triggers
Popup enter and exitduration-normalease-standardmotion-scale
Dialog enter and exitduration-moderateease-standardmotion-slide
Drawer enter and exitduration-slowease-entermotion-drawer
Toast enter and restackduration-slowerease-spring-lively on the transformmotion-toast
Toast exitduration-slowease-exitmotion-toast
Accordion and Collapsibleduration-moderateease-enteris-disclosure-animated, on height
Previous page← InteractionsNext pageView transitions →