Motion & Animation
Motion is purposeful. Durations are short enough to keep the UI feeling responsive — never decorative for its own sake. Easings echo physical motion: things that arrive decelerate, things that leave accelerate.
All motion honours prefers-reduced-motion at the user-agent level — no per-component opt-in needed. If you author a custom animation, gate it the same way.
Tokens at a glance
Reach for duration-short + ease-standard by default. Use longer durations for entrances and layout shifts where the user expects the motion to last a moment; use instant only when a transition would actually feel slower than no transition at all.
Duration
| Token | Value | Use |
|---|---|---|
instant | 0ms | Toggles, immediate state changes. |
micro | 150ms | Hover/focus state changes. |
short | 200ms | Default for most interactions. |
medium | 300ms | Layout shifts, drawers. |
long | 500ms | Entrances, modals appearing. |
Easing
| Token | Value | Use |
|---|---|---|
standard | cubic-bezier(0.4, 0, 0.2, 1) | Default — symmetric, neutral. |
enter | cubic-bezier(0, 0, 0.2, 1) | Things appearing — decelerating. |
exit | cubic-bezier(0.4, 0, 1, 1) | Things leaving — accelerating. |
Animations
The design system ships two layers of animation utilities. Custom keyframes are tailored to our components (dialog, dropdown, tooltip, sheet). Composable enter/exit animations from tw-animate-css let you combine fade, zoom, slide, and spin modifiers on any element with Radix data-state attributes.
In the live examples below, exit animations briefly hold the resting state before playing out. That mirrors real component behavior: the element exists while the close state is applied, then it is removed after the animation completes.
Custom keyframes
dialog-in
Fade + scale up from 95 %
dialog-out
Fade + scale down to 95 %
dropdown-in
Fade + scale + slide from top
fade-in
Simple opacity entrance
slide-in-right
Slide from right edge
slide-in-left
Slide from left edge
slide-in-top
Slide from top edge
slide-in-bottom
Slide from bottom edge
Composable enter
Fade
animate-in fade-in-0
Fade + Zoom
animate-in fade-in-0 zoom-in-95
Slide from top
animate-in fade-in-0 slide-in-from-top-2
Slide from bottom
animate-in fade-in-0 slide-in-from-bottom-2
Slide from left
animate-in fade-in-0 slide-in-from-left-2
Slide from right
animate-in fade-in-0 slide-in-from-right-2
Dropdown combo
animate-in fade-in-0 zoom-in-95 slide-in-from-top-2
Composable exit
Fade out
animate-out fade-out-0
Fade + Zoom out
animate-out fade-out-0 zoom-out-95
Slide to top
animate-out fade-out-0 slide-out-to-top-2
Slide to bottom
animate-out fade-out-0 slide-out-to-bottom-2
Custom keyframes
These are defined in @mihcm/tokens and use our duration/easing tokens. Each is a single Tailwind class.
| Class | Duration | Easing | Used by |
|---|---|---|---|
animate-dialog-in | 260ms | standard smooth | Dialog, AlertDialog |
animate-dialog-out | 220ms | exit | Dialog closing |
animate-dropdown-in | 220ms | enter smooth | DropdownMenu, ContextMenu, Select, Combobox, Popover |
animate-fade-in | 240ms | standard smooth | Tooltip |
animate-slide-in-right | 420ms | standard smooth | Sheet (side=right) |
animate-slide-in-left | 420ms | standard smooth | Sheet (side=left) |
animate-slide-in-top | 420ms | standard smooth | Sheet (side=top) |
animate-slide-in-bottom | 420ms | standard smooth | Sheet (side=bottom) |
Composable enter/exit system
Powered by tw-animate-css. Start with animate-in or animate-out, then layer modifiers to control fade, scale, slide, and rotation. Works with Radix data-[state=open/closed] attributes for automatic enter/exit.
Base classes
| Class | Description |
|---|---|
animate-in | Starts the enter animation (plays the enter keyframe). |
animate-out | Starts the exit animation (plays the exit keyframe). |
Modifiers
| Enter | Exit | Effect |
|---|---|---|
fade-in-0 | fade-out-0 | Opacity 0 to 1 / 1 to 0 |
zoom-in-95 | zoom-out-95 | Scale from 95% / to 95% |
slide-in-from-top-2 | slide-out-to-top-2 | Translate Y from -8px / to -8px |
slide-in-from-bottom-2 | slide-out-to-bottom-2 | Translate Y from 8px / to 8px |
slide-in-from-left-2 | slide-out-to-left-2 | Translate X from -8px / to -8px |
slide-in-from-right-2 | slide-out-to-right-2 | Translate X from 8px / to 8px |
Radix data-state pattern
The most common usage: pair enter/exit with Radix data attributes so the component animates on open and close automatically.
// HoverCard, NavigationMenu, etc.
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
'data-[side=bottom]:slide-in-from-top-2',
'data-[side=top]:slide-in-from-bottom-2',
)}Built-in height animations
| Class | Description | Used by |
|---|---|---|
animate-accordion-down | Height 0 to auto (expand) | Accordion |
animate-accordion-up | Height auto to 0 (collapse) | Accordion |
animate-collapsible-down | Height 0 to auto (expand) | Collapsible |
animate-collapsible-up | Height auto to 0 (collapse) | Collapsible |
animate-caret-blink | Pulsing cursor blink | InputOTP |
Reduced motion
tw-animate-css automatically respects prefers-reduced-motion: reduce by setting animation duration to near-zero. Our custom keyframes use CSS variables that can be overridden the same way. No additional work needed in consuming code.
Guidelines
- Entrances decelerate (
ease-enter) — content arrives and settles. - Exits accelerate (
ease-exit) — content gathers speed as it leaves. - Keep it under 300ms — anything longer feels sluggish. Use
duration-medium(300ms) only for sheet/drawer slides. - Fade + scale = polish — combine
fade-in-0 zoom-in-95for dropdown/popover entrances. Pure fades feel flat. - Match direction to origin — a dropdown opening below should
slide-in-from-top. A sheet opening right shouldslide-in-from-right. - No animation on hover states — use
transition-colors duration-150instead. Hover feedback must be instant.