Button with motion: it presses in on a spring, a new label crosses over while the button glides to its new width, and loading pops the spinner in.
$ pnpm dlx shadcn@latest add https://dashral.com/r/motion-button.jsonInstallation
$ pnpm dlx shadcn@latest add https://dashral.com/r/motion-button.jsonAdds components/dashral/motion/button.tsx, the Button it builds on (in components/dashral/), motion and the motion files every motion component shares. With the registry in your components.json:
$ pnpm dlx shadcn@latest add @dashral/motion-buttonUsage
The same as Button, imported from components/dashral/motion/: swap the import and it moves.
import { Button } from "@/components/dashral/motion/button"
<Button loading={saving} loadingText="Saving…" onClick={save}>
Save changes
</Button>Examples
Styles, sizes and states
Every button does something: Save goes Saving… then Saved, Delete asks once, Add item pops a count in, ••• turns into ×, Copy pops a tick in, Upload works before it says it's done.
Hold to confirm
holdToConfirm: a click does nothing; held, a fill sweeps across, and when it's full the button pops, the delete (or payment) runs and a toast says so. Let go early and it drains back.
Follow
morph: the text label changes letter by letter: Follow becomes Following (and Unfollow under the pointer), the follower count turning on wheels.
Hold styles
holdStyle: fill, radial from the press, a ring round the edge, a bar along the foot, or a countdown on odometer wheels. Hold each until it pops.
A form that saves
Edit the name and press Save: Saving… pops in with the spinner, then Saved rises in as the button glides to its width.
Compared with Default
Presses in to 97% in CSS. A new label snaps the width.
Motion
Presses on a spring, deeper for an icon button and shallower for a wide one. A new label rises in while the width glides to it; the spinner and loadingText pop in.
How the two editions differ in general, and which to use where: Motion.
API reference
Every prop of Button, and every attribute of the element it renders. What the Motion edition adds is marked Motion.
Button
Also takes every prop of <button>(opens in a new tab).
| Prop | Type | Default |
|---|---|---|
holdToConfirm | boolean | number
| false |
holdStyle | "fill" | "radial" | "ring" | "bar" | "countdown"
| "fill" |
morph | boolean
| false |
variant | "solid" | "soft" | "outline" | "ghost" | "link"How much it stands out. | "solid" |
color | "primary" | "neutral" | "info" | "success" | "warning" | "destructive" | "highlight"What it means. | "primary" (outline, ghost: "neutral") |
glow | booleanA soft colored shadow under a solid button. | false |
size | "xs" | "sm" | "default" | "lg" | "icon-xs" | "icon-sm" | "icon" | "icon-lg" | "inline"Height and padding; each icon size is square, as tall as the size it's named after; inline has neither, for a link-style button in a sentence. | "default" |
loading | booleanShows a spinner in place of the label, which stays (hidden) so the button keeps its width, and ignores clicks, but stays focusable. | false |
loadingText | ReactNodeBeside the spinner while loading, e.g. "Saving…"; the button is as wide as the wider of it and the label. Not with asChild. | — |
className | stringExtra classes, merged with cn() so yours win. | — |
No asChild: for a link that looks like a button, use the default Button.
Motion
- Presses in on a spring while held, as deep as its size calls for: an icon button a little more, a wide one a little less.
- A new label fades in rising 4px while the old one fades out; with morph, a label that's only text changes letter by letter instead: the letters both have glide to their new places, new ones rise out of a soft blur and the rest fade (Follow to Following keeps Follow and grows "ing"). Either way the button glides to the new width (--duration-considered, no overshoot) instead of snapping.
- loading pops the spinner, and loadingText, in over the label with a small overshoot; the button keeps its width.
- With holdToConfirm, held, a fill sweeps from start to end at an even pace; full, the button pops (a quick swell and back) and onClick runs, submitting its form if it's a submit button; let go early and the fill drains back on a glide. Space and Enter hold it too; screen readers hear "(press and hold)" after the label; phones tick at the start and buzz when it runs.
- Disabled, loading, or opening a menu, popover or tooltip, it doesn't press in. With reduced motion (the OS's, or data-motion="reduce" on <html>) everything only fades.