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.
Three principles define how Oztix products move. Every animation decision should trace back to one of them.
Motion reacts to input at once. Like a crew backstage, things happen when they should.
Elements feel like they have weight. Things land where they belong, with just enough give to feel real.
Every animation earns its place. If it doesn't orient, direct, or confirm, cut it.
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.
--duration-instant0msduration-instant--duration-fastest50msduration-fastest--duration-fast100msduration-fast--duration-normal150msduration-normal--duration-moderate200msduration-moderate--duration-slow300msduration-slow--duration-slower400msduration-slower--duration-slowest600msduration-slowest--duration-ambient1800msNone--duration-sweep2400msNone--stagger-base30msNoneduration-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.
| Tier | Steps | Use for |
|---|---|---|
| Micro | duration-fastest to duration-normal | Focus, press, and popups |
| Component | duration-moderate and duration-slow | Hover colour, disclosures, dialogs, and drawers |
| Page | duration-slower and duration-slowest | Toasts, and the nudge and pop cues |
| Ambient | --duration-ambient and --duration-sweep | One loop or pass of a loading animation |
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.
--ease-standardcubic-bezier(0.4, 0, 0.2, 1)ease-standardOvershoot None--ease-entercubic-bezier(0, 0, 0.2, 1)ease-enterOvershoot None--ease-exitcubic-bezier(0.4, 0, 1, 1)ease-exitOvershoot None--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%--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%--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.
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-* sets transition-duration from the scale. Select Play to run
each bar over its duration.
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.
Entrances play once when the element mounts. Select Replay to mount the cards again.
| Utility | What it does |
|---|---|
animate-fade-in | Fades in from transparent |
animate-scale-in | Scales up from 95% as it fades in, on --ease-spring |
animate-pop-in | Fades, slides up, and scales in. Pair with origin-* to set its edge |
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.
| Utility | What it does |
|---|---|
animate-shake | Shakes sideways, for a validation error or an invalid input |
animate-nudge | Nudges up and back, such as a cart that takes a new item |
animate-pop | Pops larger and back, such as a badge whose count changes |
animate-pop-tap | The same pop, faster and on a spring, for feedback under a tap |
Loading animations loop until the content arrives. Skeleton applies
animate-shimmer, and Progress applies animate-indeterminate when it has no
value.
| Utility | What it does |
|---|---|
animate-pulse-subtle | Pulses the opacity, for a surface waiting on something |
animate-shimmer | Pulses under a highlight anchored to the viewport, so every placeholder shares one sweep |
animate-indeterminate | Sweeps 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.
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.
| Utility | What it does | Used by |
|---|---|---|
motion-scale | Scales from 95% and fades. Pair with origin-* to scale from a point | Menu, Popover, and Tooltip |
motion-slide | Fades and slides up on enter, down on exit | Dialog |
motion-drawer | Slides off its anchored edge and tracks a live swipe | Drawer |
motion-toast | Stacks, fans out, and tracks a live swipe | Toast |
is-disclosure-animated | Opens and closes a <details> or collapsible panel by height | Accordion and Collapsible |
These go in v3. Swap each for its replacement.
| Utility | Replacement |
|---|---|
motion-fade-in | animate-fade-in |
motion-scale-in | animate-scale-in |
motion-pop-in | animate-pop-in |
motion-fade-out | motion-scale or motion-slide |
motion-scale-out | motion-scale or motion-slide |
Motion tokens are wired into the other foundations. These are the places they meet.
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.
--interactive-transition sets each property that animates, with its
duration and its easing token.
background-color200msease-standardborder-color200msease-standardcolor200msease-standardbox-shadow200msease-standardoutline-width150msease-standardoutline-color150msease-standardtransform150msease-springHover 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.
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.
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.
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.
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.
motion-safe:.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.
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.
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.
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.
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.
Choreography creates hierarchy. Stagger related elements to guide the eye.
<liclassName='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.
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.
How the motion tokens map to components. See
Interactions for the full is-interactive and
is-interactive-field breakdown.
| What | Duration | Easing | How |
|---|---|---|---|
| Hover colour and shadow | duration-moderate | ease-standard | is-interactive |
| Press scale | duration-normal | ease-spring | is-interactive, at scale(0.99) |
| Field hover and focus | duration-moderate | ease-enter | is-interactive-field |
| Focus ring | duration-normal | ease-standard, or ease-enter on a field | Outline width and colour |
| Raised surface lift | duration-moderate shadow, duration-normal lift | ease-standard shadow, ease-spring lift | emphasis-raised with is-interactive |
| Caret rotation | duration-moderate | Tailwind's default, the ease-standard curve | Select and Combobox triggers |
| Popup enter and exit | duration-normal | ease-standard | motion-scale |
| Dialog enter and exit | duration-moderate | ease-standard | motion-slide |
| Drawer enter and exit | duration-slow | ease-enter | motion-drawer |
| Toast enter and restack | duration-slower | ease-spring-lively on the transform | motion-toast |
| Toast exit | duration-slow | ease-exit | motion-toast |
| Accordion and Collapsible | duration-moderate | ease-enter | is-disclosure-animated, on height |