Dashral Pro is on the wayGet 30% off at launch

Dashral

Motion

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 page

Hold to confirm

For what can't be undone: a fill sweeps across while you hold, then it pops and runs.

See it on its page

A 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 page

A thumb you can drag

It follows your finger, goes over past the middle and holds back past the end.

See it on its page
8

Counts on wheels

A count spins to its new number, and reads 99+ past its max.

See it on its page

Sweep to select

Press one box and run the pointer down the list: each takes the same state.

See it on its page

Toasts that stack

They fold behind the newest and spread when you hover. Swipe one away.

See it on its page

A 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 page

Default 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:

example.tsx
import { Tabs } from "@/components/dashral/tabs" // Default
import { Tabs } from "@/components/dashral/motion/tabs" // Motion

With 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.

DefaultMotion
What moves itCSS transitions and keyframesmotion/react: springs, measured layout
Added to your bundleNothingThe motion package, on the client
How it feelsSet durations and curves: calm and evenSprings that can be caught mid-way and settle with a touch of overshoot
What it can moveWhat CSS knows the end of: a slide, a fade, a set heightAlso what CSS can't: a width or height following new content, one element passing between parents, things leaving, numbers on odometer wheels
Under your fingerPresses and hoversAlso 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

TokenValueFor
--duration-fast160msHovers, presses, color changes. The default for every transition.
--duration-standard240msMenus, popovers, toggles.
--duration-considered480msSliding highlights (with --ease-standard), charts drawing in, panels, counting numbers.
--ease-standardcubic-bezier(0.2, 0, 0, 1)Things already on screen moving or changing. The default curve.
--ease-entercubic-bezier(0, 0, 0, 1)Arriving: fast, then settling.
--ease-exitcubic-bezier(0.3, 0, 1, 1)Leaving: gathering speed.
--ease-springlinear(…)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:

example.tsx
<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.

example.tsx
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} />
SpringFeelFor
spring.press0.16s, no bounceA press and its release: quick, and it never wobbles under the finger.
spring.move0.4s, a touch of bounceSomething moving to a new place: a tab's highlight, a switch's thumb.
spring.appear0.24s, a hint of bounceA menu, popover, tooltip or dialog arriving: as quick as --duration-standard.
spring.glide0.48s, no bounceSizes following new content: a label's width, a panel's height.
spring.pop0.48s, small bounceSomething 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:

example.tsx
<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.

example.tsx
// 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, scale and opacity: 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.