Skip to content

Motion

Movement with restraint

Menu highlights, tabs and sidebar changes respond immediately. Keep motion for occasional changes such as opening a dialog or revealing a chart. Those movements use the shared duration and easing tokens.

Frequent actions

Hover and focus highlights have no transition. Sidebar width and the active indicator change immediately. Tooltips wait 500ms for the first label, then neighbouring labels appear without animation within the shared provider's 300ms skip window.

Live

These are the real animations, not screenshots. Replay to watch them again.

Hover lift

Hover to raise 2px on the soft hover shadow.

duration-base · ease-standard

Transactions
3,041,892

useCountUp · 600ms ease-out

Gate 4
Pavilion
Rides

stagger 50ms · 280ms ease-out

Live

dot pulse · ambient loop

Presets

Six canonical presets cover almost every case. Each is a motion-library variant object with initial, animate, and exit states.

enterGeneric show. Opacity + a 4px lift. base + out.
exitGeneric hide. Opacity only. fast + in.
fadeOpacity only. The reduced-motion-safe baseline.
slideOpacity + an 8px translate. base in, fast out.
popOpacity + scale 0.96 to 1. Snappy spring in, fast fade out.
staggerA container that cascades children by the 50ms stagger step.

Consuming the motion source

The @ludo/motion item is plain data. No DOM, no React, no React Native imports, so web and native read the same tokens. Add it with npx shadcn@latest add @ludo/motion.

In JavaScript

Read durations and easings, or hand a preset straight to the motion library.

overview.tsxtsx
import { DURATIONS, EASINGS, PRESETS } from "@/lib/motion";

// Plain data: read tokens directly.
const fast = DURATIONS.fast;       // 0.15 (seconds)
const outCurve = EASINGS.out;      // [0.16, 1, 0.3, 1]

// Or hand a preset to the motion library as variants.
<motion.div
  variants={PRESETS.enter}
  initial="initial"
  animate="animate"
  exit="exit"
/>;

In CSS

Most motion needs no library at all. The durations and easings are Tailwind utilities, and staggered entrances work through a single class.

list.tsxtsx
/* Frequent hover and focus highlights are immediate. */
<button className="hover:bg-accent focus-visible:ring-2" />

/* Staggered children, no library needed. */
<ul className="stagger-children">
  <li>Gate 4</li>
  <li>Pavilion</li>
  <li>Rides</li>
</ul>

Reduced motion

The policy is tone down, do not kill. The theme ships a global prefers-reduced-motion block that collapses durations to roughly 1ms and neutralises transforms, while opacity and colour still change. Ambient loops pause. You do not wire anything up.

/* Ships in the theme. No setup required. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
  /* Transforms are neutralised; opacity and colour still change. */
}

For the motion library, wrap the app root in <MotionConfig reducedMotion="user">. For React Native, check AccessibilityInfo.isReduceMotionEnabled() and pass ReduceMotion.System to springs.