The Motion editions to try (odometer numbers, hold to confirm, rubber bands, drags and sweeps), Default and Motion side by side, one set of durations and curves for both, and how reduced motion is handled.
Every component moves with the same few durations and curves, so the kit feels like one thing. They're CSS variables in the theme, and Tailwind's transition-* utilities use them by default.
Try the Motion editions
The Motion editions go past animation: numbers you can watch travel, controls you can drag, hold or sweep, and limits you can feel. Each card is the real component; its page has more to try.
Odometer numbers
Each digit turns as far as it travelled. Hold + and it speeds up; at 12 it strains.
See it on its pageHold to confirm
For what can't be undone: a fill sweeps across while you hold, then it pops and runs.
See it on its pageA rubber band at the ends
Drag past an end and the track stretches after you; the bubble leans as you go.
See it on its pageA thumb you can drag
It follows your finger, goes over past the middle and holds back past the end.
See it on its pageSweep to select
Press one box and run the pointer down the list: each takes the same state.
See it on its pageToasts that stack
They fold behind the newest and spread when you hover. Swipe one away.
See it on its pageA tooltip that follows
Across a wide trigger it trails the pointer on a soft spring, its words changing as it goes.
See it on its pageDefault and Motion
Every component moves out of the box, in CSS: the Default edition, in components/dashral/. Some also come as a Motion edition, in components/dashral/motion/: the same component, the same props and the same look, with motion/react on top for the movement CSS can't do on its own. Switching is one import:
import { Tabs } from "@/components/dashral/tabs" // Default
import { Tabs } from "@/components/dashral/motion/tabs" // MotionWith a Motion edition so far: Button, Tabs, Switch, Accordion, Checkbox, Dialog, Tooltip, Input, Textarea, Segmented Control, Radio Group, Badge, Toast, Number Input, Slider, Dropdown Menu, Select, Popover, Progress, Sidebar, Alert Dialog, Input OTP, Time Picker, Toggle, Toggle Group, Kbd. Each one's page says what it adds over the Default, with Default | Motion at the top to try both.
| Default | Motion | |
|---|---|---|
| What moves it | CSS transitions and keyframes | motion/react: springs, measured layout |
| Added to your bundle | Nothing | The motion package, on the client |
| How it feels | Set durations and curves: calm and even | Springs that can be caught mid-way and settle with a touch of overshoot |
| What it can move | What CSS knows the end of: a slide, a fade, a set height | Also what CSS can't: a width or height following new content, one element passing between parents, things leaving, numbers on odometer wheels |
| Under your finger | Presses and hovers | Also drags and swipes that follow the finger, rubber bands at the ends, press-and-hold to confirm, sweeps across a list, and a buzz on phones |
| Props | — | The same: swap the import |
Which to use
- Default for dense dashboards with dozens of components on a page, where the bundle matters, and where calm, even movement is the point.
- Motion where the page should feel crafted (a landing page, onboarding, settings, pricing) and where content changes under the component, so a width or height that glides really shows.
- Mix them freely: they share the tokens below, so a Motion tab next to a Default button still feels like one kit.
Tokens
| Token | Value | For |
|---|---|---|
| --duration-fast | 160ms | Hovers, presses, color changes. The default for every transition. |
| --duration-standard | 240ms | Menus, popovers, toggles. |
| --duration-considered | 480ms | Sliding highlights (with --ease-standard), charts drawing in, panels, counting numbers. |
| --ease-standard | cubic-bezier(0.2, 0, 0, 1) | Things already on screen moving or changing. The default curve. |
| --ease-enter | cubic-bezier(0, 0, 0, 1) | Arriving: fast, then settling. |
| --ease-exit | cubic-bezier(0.3, 0, 1, 1) | Leaving: gathering speed. |
| --ease-spring | linear(…) | A spring: about 8% past the end, then back. Presses, toggles, bars. |
The curves
Each card draws its curve, with the start and the end dashed so an overshoot shows; click one to send its dot across, or play a group to compare. The CSS curves move the Default edition, the springs the Motion edition.
CSS curves
The Default edition, over --duration-considered (480ms).
Springs
The Motion edition, from lib/motion-tokens.ts, over the same second.
Using them
The curves are Tailwind utilities (ease-standard, ease-enter, ease-exit, ease-spring); durations go through a variable:
<span className="transition-transform duration-(--duration-standard) ease-spring" />
/* or in CSS */
.panel {
transition: translate var(--duration-standard) var(--ease-spring);
}A spring in CSS
--ease-spring is a real spring — a little overshoot, then settling — written with CSS linear(), which plots the curve as points. It runs on the browser's compositor like any CSS transition: no animation library, no JavaScript per frame, nothing added to your bundle.
Motion tokens in JavaScript
motion/react takes numbers, not CSS variables, so the Motion editions share lib/motion-tokens.ts: the same durations (in seconds) and curves as the theme, and five springs. Use them in your own animations to move like the rest of the kit.
import { duration, ease, spring } from "@/components/dashral/lib/motion-tokens"
<motion.div
animate={{ opacity: 1, y: 0 }}
transition={{ duration: duration.standard, ease: ease.enter }}
/>
<motion.span layout transition={spring.move} />| Spring | Feel | For |
|---|---|---|
| spring.press | 0.16s, no bounce | A press and its release: quick, and it never wobbles under the finger. |
| spring.move | 0.4s, a touch of bounce | Something moving to a new place: a tab's highlight, a switch's thumb. |
| spring.appear | 0.24s, a hint of bounce | A menu, popover, tooltip or dialog arriving: as quick as --duration-standard. |
| spring.glide | 0.48s, no bounce | Sizes following new content: a label's width, a panel's height. |
| spring.pop | 0.48s, small bounce | Something arriving that should feel alive: an icon, a spinner. Like --ease-spring. |
Reduced motion
When the system asks for reduced motion, the theme takes the bounce out of --ease-spring and shortens --duration-standard and --duration-considered to 160ms. Components also drop movement that travels — sliding, drawing in, counting — and keep the end state, focus and feedback. Do the same in your own code:
<div className="transition-transform motion-reduce:transition-none" />To let people turn motion down in your app, whatever their system says, set data-motion="reduce" on <html>. The theme and every motion-reduce: and motion-safe: class follow it, and so do the components that move from JavaScript (counting numbers, carousels, smooth scrolling). In your own scripts, ask reducedMotion() from @/components/dashral/lib/motion, which checks both.
// A "Reduce motion" switch in your settings:
const root = document.documentElement
if (on) root.dataset.motion = "reduce"
else delete root.dataset.motion
// In a script that moves something:
import { reducedMotion } from "@/lib/motion"
el.scrollIntoView({ behavior: reducedMotion() ? "auto" : "smooth" })The Motion editions listen too: useReducedMotion() from @/components/dashral/lib/motion-reduced hears both the system and data-motion="reduce", and they drop springs, slides and stretches for a plain fade, keeping every state change.
Guidelines
- Animate
transform,translate,scaleandopacity: they don't make the browser lay the page out again. - Motion explains a change — where something came from or went. If it explains nothing, leave it out.
- Don't animate things people repeat quickly, like moving through a list with the arrow keys.
- Keep it short: most things finish within 240ms.