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
useCountUp · 600ms ease-out
stagger 50ms · 280ms ease-out
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.
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.
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.
/* 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.