Motion
Motion in the Mukoko design system is purposeful and restrained. Animations communicate state changes and guide attention — they never exist for decoration.
Principles
- Functional, not decorative — every animation must communicate something (a state change, a transition, a response to user input)
- Fast — most transitions complete in 150-200ms; users should never wait for an animation
- Consistent — the same type of change uses the same animation everywhere
- Respectful — honor
prefers-reduced-motionfor users who need it
Timing
| Duration | CSS class | Usage |
|---|---|---|
| 100ms | duration-100 | Micro-interactions (hover, focus) |
| 150ms | duration-150 | State changes (toggle, checkbox) |
| 200ms | duration-200 | Standard transitions (color, opacity) |
| 300ms | duration-300 | Layout shifts (accordion, collapsible) |
| 500ms | duration-500 | Page-level transitions (sheet, drawer) |
Easing
| Easing | CSS class | Usage |
|---|---|---|
ease-out | ease-out | Elements entering (fade in, slide in) |
ease-in | ease-in | Elements exiting (fade out, slide out) |
ease-in-out | ease-in-out | Continuous motion (loading spinners) |
Standard transition pattern:
<div className="transition-colors duration-150 ease-out hover:bg-muted">
Hover me
</div>Enter/exit patterns
Fade
The simplest transition. Used for tooltips, popovers, and content that appears in place:
{/* Tailwind transition */}
<div className="transition-opacity duration-200 ease-out data-[state=open]:opacity-100 data-[state=closed]:opacity-0">
Content
</div>Slide
Used for sheets, drawers, and sidebars that enter from an edge:
{/* Sheet sliding from bottom */}
<SheetContent side="bottom" className="transition-transform duration-300 ease-out">
Content
</SheetContent>Scale
Used for dialogs and modals. Scale from 95% to 100% with a simultaneous fade:
<div className="transition-all duration-200 ease-out data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95">
Modal content
</div>Collapse
Used for accordions and collapsible sections. Animate height from 0 to auto:
<AccordionContent>
{/* Radix handles the height animation */}
Content that expands and collapses
</AccordionContent>Radix UI primitives handle these animations internally with CSS keyframes.
Common animation patterns
Hover lift
Cards and interactive elements that lift slightly on hover:
<Card className="transition-all duration-150 hover:shadow-md hover:-translate-y-0.5">
Content
</Card>Loading spinner
The Spinner component uses a continuous rotation:
<Spinner className="size-5" />Skeleton pulse
Loading placeholders pulse with a subtle opacity animation:
<Skeleton className="h-4 w-48" />Arrow slide
Navigation arrows that slide on hover to indicate direction:
<ArrowRight className="size-4 transition-transform duration-150 group-hover:translate-x-1" />Reduced motion
Respect the prefers-reduced-motion media query. Users enable this for medical reasons (vestibular disorders, seizure conditions) or personal preference.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}In Tailwind, use the motion-safe and motion-reduce variants:
{/* Only animate when motion is OK */}
<div className="motion-safe:transition-transform motion-safe:duration-200 motion-safe:hover:-translate-y-1">
Card
</div>
{/* Alternative for reduced motion */}
<div className="motion-reduce:transition-none">
Content
</div>What to keep with reduced motion
Even with reduced motion enabled:
- Opacity changes are acceptable (instant, no physical motion)
- Color changes are acceptable (hover states, focus states)
- Layout shifts should still happen, just without animation
What to remove:
- Translate/scale/rotate animations
- Sliding transitions
- Parallax effects
- Auto-playing carousels