Skip to content
txbrendan/ui

Motion

beta

Continuous transitions for changing labels, icons, and local layouts. Keeps the live controls and focus in place.

Preview

Select a project first

Project updates

Everything is up to date.
alex@example.comsam@example.com

Wrapping controls

DayMon

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. 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

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>