Motion
betaContinuous transitions for changing labels, icons, and local layouts. Keeps the live controls and focus in place.
Preview
Project updates
Wrapping controls
DayMon
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/motion
Use it for
- Use MotionSwap with a meaningful value when a label or icon changes state.
- Use Motion around a local group whose items are added, removed, or rearranged. Keep keys stable. Nested lists use the none axis so their parent alone owns the occupied size.
- Use useMotionLayout(ref) on an existing positioned element when an extra wrapper would change its semantics.
- The latest state is immediately usable; only the visual transition takes time.
Avoid it for
- Wrapping the whole page or nesting boundaries that animate the same geometry.
- Animating each keystroke, pointer drag, or slider scrub. These track the input directly.
- Remounting controls to replay an entrance. Initial content renders at rest.
Props
- useMotionLayout
Apply local layout motion to an existing positioned element.
(ref: RefObject<HTMLElement | null>, options?: { axis?: "height" | "both" | "none"; items?: boolean }) => void
- MotionControl
Add size continuity to one existing control without a wrapper.
({ children: ReactElement }) => ReactElement
- motionSettings
Read the motion tokens in scope at node, for script animation. duration is the layout time and fade the fast time, in ms. easing is the ease-out curve. travel is the distance in px. Motion off makes the numbers 0; reduced motion makes duration and travel 0.
(node: Element) => { duration: number; fade: number; easing: string; travel: number }
- axis
Resize height, both dimensions, or use none for an items-only boundary inside a resizing parent.
"height" | "both" | "none" · default "height"
- items
Motion also moves its direct children and fades additions and removals.
boolean · default true
- value
MotionSwap's meaningful state identifier; a change fades or rolls its contents, as effect sets.
string | number | boolean
- effect
MotionSwap: roll compact chart readouts vertically; keep the default fade for other labels. Reduced motion keeps only the fade.
"fade" | "roll" · default "fade"
- children
The current content, immediately available to keyboard and assistive technology.
ReactNode
- className
Layout classes on the boundary. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| useMotionLayout | (ref: RefObject<HTMLElement | null>, options?: { axis?: "height" | "both" | "none"; items?: boolean }) => void | - | Apply local layout motion to an existing positioned element. |
| MotionControl | ({ children: ReactElement }) => ReactElement | - | Add size continuity to one existing control without a wrapper. |
| motionSettings | (node: Element) => { duration: number; fade: number; easing: string; travel: number } | - | Read the motion tokens in scope at node, for script animation. duration is the layout time and fade the fast time, in ms. easing is the ease-out curve. travel is the distance in px. Motion off makes the numbers 0; reduced motion makes duration and travel 0. |
| axis | "height" | "both" | "none" | "height" | Resize height, both dimensions, or use none for an items-only boundary inside a resizing parent. |
| items | boolean | true | Motion also moves its direct children and fades additions and removals. |
| value | string | number | boolean | - | MotionSwap's meaningful state identifier; a change fades or rolls its contents, as effect sets. |
| effect | "fade" | "roll" | "fade" | MotionSwap: roll compact chart readouts vertically; keep the default fade for other labels. Reduced motion keeps only the fade. |
| children | ReactNode | - | The current content, immediately available to keyboard and assistive technology. |
| className | string | - | Layout classes on the boundary. Later classes win. |
Accessibility
- Live controls stay mounted, preserving focus and drafts. Outgoing visuals are inert, unnamed, and hidden from screen readers.
- Motion off is immediate. Reduced motion removes spatial transitions and keeps short fades.
- Without browser animation support, all content updates immediately.
Examples
A local boundary
<Motion>Content</Motion>An existing control
<MotionControl><button type="button">Save</button></MotionControl>An existing element
function LocalList() { const ref = useRef<HTMLUListElement>(null); useMotionLayout(ref); return <ul ref={ref} className="relative"><li>Design</li></ul>; }A result label
<MotionSwap value={saved}>{saved ? "Saved" : "Save changes"}</MotionSwap>A rolling chart readout
<MotionSwap value={day} effect="roll">{day}</MotionSwap>A script fade on the theme's times
const { fade, easing } = motionSettings(node); node.animate([{ opacity: 0 }, { opacity: 1 }], { duration: fade, easing });A changing list
<Motion className="flex flex-col gap-2">{items.map(item => <div key={item.id}>{item.name}</div>)}</Motion>