Motion & Transition

Two-tier motion system

Shavin's motion system follows the same two-tier architecture as colors and radii. Foundation tokens are the raw cubic-bezier curves, durations, distances, and stagger intervals — you never use them directly. Semantic animation utilities bundle the easing, duration, and keyframes into purpose-driven CSS classes. Reach for the .animate-* class, not the raw tokens.

--ease-out--ease-in-out--ease-drawer--dur-*--dist-*--stagger3 easing + 5 duration + 4 distance + 1 stagger9 semantic utilities

Foundation easing tokens

Three curated cubic-bezier curves cover every component motion in the design system. Click a thumbnail to inspect it — watch the ball replay the easing, drag the duration, and copy the exact CSS. DS-marked curves are shipped as tokens; prefer them over ad-hoc curves. linear is shown as a reference baseline — not for UI motion.

ease-out
cubic-bezier(0.23, 1, 0.32, 1)
Preview
Duration
transition: transform 1200ms var(--ease-out);

Principles

Animate only transform & opacity

These two are composited on the GPU — they never trigger layout or paint. Animating width, height, top or margin thrashes the main thread and drops frames. Move and fade; don't reflow.

Ease-out for enter, never ease-in

Entering elements should be fast off the mark and settle gently — that's ease-out. Ease-in delays the frames the eye is watching and reads as sluggish at the identical duration.

Easing matters more than duration

A thoughtful curve at 200ms beats a linear tween at any length. Reach for the DS tokens (--ease-out, --ease-in-out, --ease-drawer) before touching the number.

Exit faster than enter

Dismissal is a decision already made. Play exits at roughly 60% of the enter duration so the UI gets out of the way instead of making the user wait.

Motion must be interruptible

A user can re-open what they just closed. Animations transition from their current state — no waiting for a frame to finish, no janky commit mid-flight.

Animate from the origin

Popovers grow from their trigger; modals scale from center; a swipe follows the finger. Motion that starts where the eye already is feels physical, not decorative.

Purposeful, not decorative

Every animation earns its place: it gives feedback, guides attention, or shows a relationship between states. If it does none of those, cut it.

Respect reduced motion

prefers-reduced-motion collapses all motion tokens to zero in one media query — durations to 0ms, distances to 0px, stagger to 0ms. Every animation, transition, and entrance preset collapses automatically. Functional loops (spinner, skeleton) keep running. Components need zero conditional code.

(hold to see the press feedback)

Semantic animation utilities

These CSS classes are the semantic layer — each bundles a keyframe, duration, and easing token into a purpose-driven name. Use them instead of writing raw animation or transition declarations.

UtilityPurposeDurationEasing
.animate-risePopover / tooltip / hover-card entervar(--dur-fast)--ease-out
.animate-rise-fastQuick tooltip re-open (skips delay)var(--dur-instant)--ease-out
.animate-modal-inModal dialog enter (scale + fade)var(--dur-base)--ease-out
.animate-overlayDialog / spotlight backdrop fadevar(--dur-base)ease
.animate-accordion-downAccordion content expandvar(--dur-base)--ease-out
.animate-accordion-upAccordion content collapsevar(--dur-base) × 0.8--ease-out
.animate-progress-indeterminateLoading bar indeterminate looploopease-in-out
.animate-pulse-dotStatus light pulse (functional loop)loopease-in-out
.animate-caret-blinkCode editor caret blinkloopsteps(2)

Durations

The more often a surface is seen, the faster it should animate. Keep UI motion under 300ms — a 200ms dropdown feels more responsive than a 400ms one.

SurfaceDurationNotes
Press feedbackvar(--dur-instant)active:scale on buttons — 100ms
Tooltip / hover shiftvar(--dur-fast)fires often — keep it snappy — 160ms
Panels & modalsvar(--dur-base)popover, menu, dialog, accordion — 240ms
Large surfacesvar(--dur-slow)sidebar, sheet, drawer — 400ms
Entrances & revealsvar(--dur-enter)hero, stagger sequences — 560ms
Exit≈ 60% of enterfaster — the user already decided to dismiss

Duration, distance & stagger tokens

Motion durations, entrance travel distances, and stagger intervals are tokenized at the foundation level. Every semantic utility and page-level preset references these — never hardcode ms or px values in components. All collapse to zero under reduced-motion.

Duration tokens

--dur-instant100ms
--dur-fast160ms
--dur-base240ms
--dur-slow400ms
--dur-enter560ms

Distance tokens

--dist-xs4px
--dist-sm8px
--dist-md16px
--dist-lg24px

Stagger token

--stagger60ms

Interval between sequentially revealed elements. Multiply by index: calc(var(--stagger) * N)

Tailwind utilities: duration-instant, duration-fast, duration-base, duration-slow, duration-enter. Pair with ease-out / ease-in-out / ease-drawer.

Reduced motion

Reduced motion means gentler, not off. Shavin uses a token-level safety net — instead of overriding each animation individually, all motion tokens collapse to zero in one media query. Durations become 0ms, distances become 0px, stagger becomes 0ms — every animation, transition, and entrance preset collapses automatically. Functional loops (Spinner, Skeleton, caret-blink) keep running because they're keyframe-based, not transition-duration based. Components need zero conditional code.

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-instant: 0ms; --dur-fast: 0ms; --dur-base: 0ms;
    --dur-slow: 0ms; --dur-enter: 0ms; --stagger: 0ms;
    --dist-xs: 0px; --dist-sm: 0px; --dist-md: 0px; --dist-lg: 0px;
  }
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
Configure Theme
Theme
Accent Color
Custom
Gray Family
Appearance
Radius
Scaling
Panel Style
Heading Font
Body Font