# Motion

Animate items that appear, leave, move or replace one another, and numbers that change, with no animation code.

Task list that adds and removes rows

[Complete TSX file](/examples/motion/default.tsx)

```tsx
"use client";

import { PlusIcon, XIcon } from "lucide-react";
import { useRef, useState } from "react";
import { Button } from "@/siteplane/components/button";
import { Card } from "@/siteplane/components/card";
import { MotionItem, MotionList } from "@/siteplane/components/motion";

type Task = { id: number; title: string };

const titles = [
  "Review the launch checklist",
  "Send the invoice",
  "Write the release notes",
  "Book the team offsite",
  "Update the pricing copy",
];

export default function Example() {
  const [tasks, setTasks] = useState<Task[]>(() =>
    titles.slice(0, 3).map((title, index) => ({ id: index + 1, title })),
  );
  const nextId = useRef(4);

  const add = () => {
    const id = nextId.current++;
    setTasks((current) => [
      { id, title: titles[(id - 1) % titles.length]! },
      ...current,
    ]);
  };

  return (
    <div className="grid w-full max-w-md gap-4">
      <Button className="justify-self-start" onClick={add} size="sm">
        <PlusIcon /> Add task
      </Button>
      <MotionList aria-label="Tasks" gap={2}>
        {tasks.map((task) => (
          <MotionItem key={task.id}>
            <Card
              className="flex-row items-center justify-between gap-3 p-3"
              variant="subtle"
            >
              <span className="truncate text-sm">{task.title}</span>
              <Button
                aria-label={`Remove ${task.title}`}
                onClick={() =>
                  setTasks((current) =>
                    current.filter((item) => item.id !== task.id),
                  )
                }
                shape="square"
                size="sm"
                variant="ghost"
              >
                <XIcon />
              </Button>
            </Card>
          </MotionItem>
        ))}
      </MotionList>
    </div>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/motion
```

With npm: `npx shadcn@4.18.0 add @siteplane/motion`. [Registry JSON](/registry/motion.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/card @siteplane/alert @siteplane/input @siteplane/separator @siteplane/toggle-group
```

## Usage

```tsx
import { MotionItem, MotionList } from "@/siteplane/components/motion";

const tasks = [
  { id: 1, title: "Review the launch checklist" },
  { id: 2, title: "Send the invoice" },
];

<MotionList aria-label="Tasks" gap={2}>
  {tasks.map((task) => (
    <MotionItem key={task.id}>{task.title}</MotionItem>
  ))}
</MotionList>;
```

Change the array and the list animates. A new item expands and fades in, and a removed item fades out and collapses while the items around them glide to their new place.

```tsx
import { MotionNumber } from "@/siteplane/components/motion";

<MotionNumber
  format={{ currency: "USD", maximumFractionDigits: 0, style: "currency" }}
  value={24960}
/>;
```

Change `value` and the digits roll to the new figure.

## Composition

```text
MotionList
└── MotionItem            one per entry, with a stable key
MotionReveal              one block, shown with open
MotionSwap                content keyed by swapKey
MotionNumber              a figure that rolls to its new value
useTweenedValues          numbers that move toward a target, for chart geometry
useTweenedRows            keyed rows whose numbers come, move and go
useFollowValue            a number that glides after a moving target
```

## Add, remove and reorder

`MotionList` renders a `ul` and `MotionItem` an `li`; pass `render` to use another element, or a component that passes its `ref` on to its element. Give every item a stable `key` such as an id, never the index: the key tells the list which item has left. Sorting or shuffling the array makes each item glide to its new place.

Set the space between items with `gap`, in steps of the spacing scale. It is between items only: none above the first item or below the last. It opens with an item that is added after another and closes when an item becomes the last, together with that item’s own expand or collapse. A CSS `gap` on the list would leave a hole that closes only after the item has gone.

Sorting and shuffling a list

[Complete TSX file](/examples/motion/reorder.tsx)

```tsx
"use client";

import { ArrowDownAZIcon, ShuffleIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/siteplane/components/button";
import { Card } from "@/siteplane/components/card";
import { MotionItem, MotionList } from "@/siteplane/components/motion";

const initial = [
  { id: 1, title: "Pricing copy" },
  { id: 2, title: "Launch checklist" },
  { id: 3, title: "Team offsite" },
  { id: 4, title: "Invoice" },
  { id: 5, title: "Release notes" },
];

export default function Example() {
  const [tasks, setTasks] = useState(initial);

  return (
    <div className="grid w-full max-w-md gap-4">
      <div className="flex gap-2">
        <Button
          onClick={() =>
            setTasks((current) =>
              [...current].sort((a, b) => a.title.localeCompare(b.title)),
            )
          }
          size="sm"
          variant="outline"
        >
          <ArrowDownAZIcon /> Sort A–Z
        </Button>
        <Button
          onClick={() =>
            setTasks((current) => [...current].sort(() => Math.random() - 0.5))
          }
          size="sm"
          variant="outline"
        >
          <ShuffleIcon /> Shuffle
        </Button>
      </div>
      <MotionList aria-label="Tasks" gap={2}>
        {tasks.map((task) => (
          <MotionItem key={task.id}>
            <Card className="p-3" variant="subtle">
              <span className="text-sm">{task.title}</span>
            </Card>
          </MotionItem>
        ))}
      </MotionList>
    </div>
  );
}
```

## Dividers between items

`separator` draws a divider, or a line that connects the items, in the space between two items. It appears and disappears with that space. Position it against the item (the item needs to be `relative`) or lay it out inside the space.

Dividers that come and go with the space between rows

[Complete TSX file](/examples/motion/divided.tsx)

```tsx
"use client";

import { PlusIcon, XIcon } from "lucide-react";
import { useRef, useState } from "react";
import { Button } from "@/siteplane/components/button";
import { MotionItem, MotionList } from "@/siteplane/components/motion";
import { Separator } from "@/siteplane/components/separator";

type Task = { id: number; title: string };

const titles = [
  "Review the launch checklist",
  "Send the invoice",
  "Write the release notes",
  "Book the team offsite",
  "Update the pricing copy",
];

export default function Example() {
  const [tasks, setTasks] = useState<Task[]>(() =>
    titles.slice(0, 3).map((title, index) => ({ id: index + 1, title })),
  );
  const nextId = useRef(4);

  const add = () => {
    const id = nextId.current++;
    setTasks((current) => [
      ...current,
      { id, title: titles[(id - 1) % titles.length]! },
    ]);
  };

  return (
    <div className="grid w-full max-w-md gap-4">
      <Button className="justify-self-start" onClick={add} size="sm">
        <PlusIcon /> Add task
      </Button>
      <MotionList
        aria-label="Tasks"
        gap={4}
        separator={
          <Separator
            className="absolute inset-x-0 bottom-2"
            role="presentation"
          />
        }
      >
        {tasks.map((task) => (
          <MotionItem className="relative" key={task.id}>
            <div className="flex items-center justify-between gap-3">
              <span className="truncate text-sm">{task.title}</span>
              <Button
                aria-label={`Remove ${task.title}`}
                onClick={() =>
                  setTasks((current) =>
                    current.filter((item) => item.id !== task.id),
                  )
                }
                shape="square"
                size="sm"
                variant="ghost"
              >
                <XIcon />
              </Button>
            </div>
          </MotionItem>
        ))}
      </MotionList>
    </div>
  );
}
```

## Wrapping sets

Use `layout="flow"` for chips, tags and tiles that wrap. A set item fades and scales slightly, and its siblings glide to their new place by position, and the height of the list follows when a row appears or goes, so what is below it glides instead of jumping. Lay the list out and space it with your own classes, such as `flex flex-wrap gap-2`. A list that is `display: contents` has no height of its own: pass the element that wraps its items as `container`.

Chips that wrap and glide

[Complete TSX file](/examples/motion/flow.tsx)

```tsx
"use client";

import { PlusIcon, XIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/siteplane/components/button";
import { MotionItem, MotionList } from "@/siteplane/components/motion";

const topics = ["Design", "Research", "Launch", "Pricing", "Docs", "Support"];

export default function Example() {
  const [chips, setChips] = useState(topics.slice(0, 4));
  const unused = topics.filter((topic) => !chips.includes(topic));

  return (
    <div className="grid w-full max-w-md gap-4">
      <Button
        className="justify-self-start"
        disabled={unused.length === 0}
        onClick={() => setChips((current) => [...current, unused[0]!])}
        size="sm"
      >
        <PlusIcon /> Add topic
      </Button>
      <MotionList
        aria-label="Topics"
        className="flex flex-wrap gap-2"
        layout="flow"
      >
        {chips.map((chip) => (
          <MotionItem key={chip}>
            <Button
              aria-label={`Remove ${chip}`}
              onClick={() =>
                setChips((current) => current.filter((item) => item !== chip))
              }
              size="sm"
              variant="outline"
            >
              {chip}
              <XIcon />
            </Button>
          </MotionItem>
        ))}
      </MotionList>
    </div>
  );
}
```

## Show and hide a block

`MotionReveal` shows or hides one block with `open`. It expands with a fade and collapses with a fade. While it is closed it renders nothing, so it adds no space to the layout around it. Put the space above the content as a margin inside the block, so it opens and closes with it.

A message that expands and collapses

[Complete TSX file](/examples/motion/reveal.tsx)

```tsx
"use client";

import { InfoIcon } from "lucide-react";
import { useState } from "react";
import {
  Alert,
  AlertDescription,
  AlertTitle,
} from "@/siteplane/components/alert";
import { Button } from "@/siteplane/components/button";
import { MotionReveal } from "@/siteplane/components/motion";

export default function Example() {
  const [open, setOpen] = useState(false);

  return (
    <div className="w-full max-w-md">
      <Button
        aria-expanded={open}
        onClick={() => setOpen((current) => !current)}
        size="sm"
        variant="outline"
      >
        {open ? "Hide message" : "Show message"}
      </Button>
      <MotionReveal open={open}>
        <Alert className="mt-3" variant="info">
          <InfoIcon aria-hidden="true" />
          <AlertTitle>Draft saved</AlertTitle>
          <AlertDescription>
            Your changes are stored. Publish when you are ready.
          </AlertDescription>
        </Alert>
      </MotionReveal>
      <p className="mt-3 text-muted-foreground text-sm">
        The content below glides with the message instead of jumping.
      </p>
    </div>
  );
}
```

A field error is the most common use. [FieldError](/docs/components/field/#custom-validation) reveals its message this way by itself, so the space under the field opens with the message and the form below moves smoothly. Leave the field empty, or type an incomplete address and move on, to see it.

A field error under an input

[Complete TSX file](/examples/motion/field-error.tsx)

```tsx
"use client";

import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Input } from "@/siteplane/components/input";

export default function Example() {
  return (
    <Field className="w-full max-w-sm" name="email" validationMode="onBlur">
      <FieldLabel>Email</FieldLabel>
      <Input placeholder="name@company.com" required type="email" />
      <FieldError>Enter an email address such as name@company.com.</FieldError>
    </Field>
  );
}
```

## Replace content

`MotionSwap` fades content that replaces other content through, such as the steps of a flow or the states of a panel. Change `swapKey` with the content: the old content fades out quickly and the new content fades in after it, so the two are never legible on top of each other. The container’s height follows the new content over the whole swap, so nothing below it jumps, and it keeps following content that builds a frame later, such as a collection or an editor.

Steps that fade through and resize

[Complete TSX file](/examples/motion/swap.tsx)

```tsx
"use client";

import { useState } from "react";
import { Button } from "@/siteplane/components/button";
import { Card } from "@/siteplane/components/card";
import { MotionSwap } from "@/siteplane/components/motion";

const steps = [
  {
    body: "Choose a name for the workspace. You can change it later.",
    title: "Name your workspace",
  },
  {
    body: "Invite people by email. Everyone you invite gets editor access, and you can change roles from the members page at any time. People who have not joined yet show as pending until they accept.",
    title: "Invite your team",
  },
  { body: "All set.", title: "Done" },
];

export default function Example() {
  const [step, setStep] = useState(0);
  const current = steps[step]!;

  return (
    <div className="grid w-full max-w-md gap-4">
      <MotionSwap swapKey={step}>
        <Card className="gap-1 p-4" variant="subtle">
          <p className="text-muted-foreground text-xs">
            Step {step + 1} of {steps.length}
          </p>
          <p className="font-medium text-sm">{current.title}</p>
          <p className="text-muted-foreground text-sm">{current.body}</p>
        </Card>
      </MotionSwap>
      <div className="flex gap-2">
        <Button
          disabled={step === 0}
          onClick={() => setStep((value) => value - 1)}
          size="sm"
          variant="outline"
        >
          Back
        </Button>
        <Button
          disabled={step === steps.length - 1}
          onClick={() => setStep((value) => value + 1)}
          size="sm"
        >
          Next
        </Button>
      </div>
    </div>
  );
}
```

## Rolling numbers

`MotionNumber` shows a figure that changes in place, such as a total or a rate. When `value` changes, each digit rolls like an odometer wheel to its new digit, upward when the number rises and downward when it falls. Characters that come and go, such as a sign, a separator or a new leading digit, open and close their width, so everything beside them glides. A number is formatted with `Intl.NumberFormat`: pass `format` for currency, percent, compact notation, a sign or a unit, and `locales` for another language. A string is shown as it is written and its digits roll, so a preformatted value works too.

Revenue and change that roll to new values

[Complete TSX file](/examples/motion/number.tsx)

```tsx
"use client";

import { useState } from "react";
import { Card } from "@/siteplane/components/card";
import { MotionNumber } from "@/siteplane/components/motion";
import {
  ToggleGroup,
  ToggleGroupItem,
} from "@/siteplane/components/toggle-group";

const periods = [
  { change: 0.024, days: "7", label: "7 days", revenue: 5920, visitors: 3008 },
  {
    change: 0.091,
    days: "30",
    label: "30 days",
    revenue: 24960,
    visitors: 12480,
  },
  {
    change: -0.032,
    days: "90",
    label: "90 days",
    revenue: 76640,
    visitors: 38336,
  },
];

export default function Example() {
  const [days, setDays] = useState("30");
  const period = periods.find((item) => item.days === days)!;

  return (
    <div className="grid w-full max-w-md gap-4">
      <ToggleGroup
        aria-label="Period"
        onValueChange={(next) => {
          if (next[0]) setDays(next[0]);
        }}
        size="sm"
        value={[days]}
        variant="outline"
      >
        {periods.map((item) => (
          <ToggleGroupItem key={item.days} value={item.days}>
            {item.label}
          </ToggleGroupItem>
        ))}
      </ToggleGroup>
      <Card className="gap-1 p-4" variant="subtle">
        <p className="text-muted-foreground text-xs">Booking revenue</p>
        <p className="font-semibold text-3xl">
          <MotionNumber
            format={{
              currency: "USD",
              maximumFractionDigits: 0,
              style: "currency",
            }}
            value={period.revenue}
          />
        </p>
        <p className="text-muted-foreground text-sm">
          <MotionNumber
            format={{
              maximumFractionDigits: 1,
              signDisplay: "always",
              style: "percent",
            }}
            value={period.change}
          />{" "}
          on the period before, from <MotionNumber value={period.visitors} />{" "}
          visitors
        </p>
      </Card>
    </div>
  );
}
```

The first render and the server show the final text, and nothing animates on mount. A change that arrives while the digits still roll carries on from where they are. `MotionNumber` sets tabular figures (`tabular-nums`) itself, so the digits keep their places; a font without tabular figures works too, because the width of each place follows the roll. In right-to-left text a number still reads from left to right: `MotionNumber` sets `dir="ltr"` on its element, and the number as a whole sits where the surrounding text puts it.

## Chart values

A chart that animates writes no animation loop of its own. `useTweenedValues` takes the numbers the data says now, such as the heights of bars or the points of a line, and returns the numbers to draw this frame, moving toward each new target with the shared tokens. At rest it returns your array itself, so you can tell that nothing moves, and a fresh array with the same numbers starts nothing. `useTweenedValue` does the same for one number. `useTweenedRows` follows keyed rows: a row that is new grows from 0, a row that is gone shrinks to 0 and then leaves the list, which is how the Pie Chart glides.

Bars that glide to new data

[Complete TSX file](/examples/motion/values.tsx)

```tsx
"use client";

import { ShuffleIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/siteplane/components/button";
import { useTweenedValues } from "@/siteplane/components/motion";

export default function Example() {
  const [values, setValues] = useState([34, 52, 41, 68, 57, 80]);
  // The heights glide to the new values; the chart writes no animation loop.
  const heights = useTweenedValues(values);

  return (
    <div className="grid w-full max-w-xl gap-4">
      <Button
        className="justify-self-start"
        onClick={() =>
          setValues((current) =>
            current.map(() => 15 + Math.round(Math.random() * 75)),
          )
        }
        size="sm"
      >
        <ShuffleIcon /> New data
      </Button>
      <svg
        aria-label="Bookings by week"
        className="h-40 w-full"
        role="img"
        viewBox="0 0 240 100"
      >
        {heights.map((height, index) => (
          <rect
            fill="var(--chart-brand)"
            height={height}
            key={index}
            rx="3"
            width="28"
            x={index * 40 + 6}
            y={100 - height}
          />
        ))}
      </svg>
    </div>
  );
}
```

Not every number should grow from 0. Pass `absent` to `useTweenedRows` for the number a row has while it is not there: a new row starts from it and a leaving row ends at it. Return the row’s own number, such as its place on an axis, and a row appears and disappears where it stands while the rows that stay glide to their new place; the Bar Chart places its categories this way. With `keys`, `useTweenedValues` takes `starts` instead: one number per value, in the order of the target, where a value with a new key starts.

## Follow motion

`useFollowValue` makes a number glide after a target that moves, such as the crosshair of a chart that follows the pointer from one item to the next. It returns a motion value: bind it to a `followed` element and the element moves frame by frame while your component does not render again. The glide is a critically damped spring, so it never overshoots, and when the target changes on the way the value keeps its speed instead of stopping at each item. At rest, on the server and in the first render, the value is the target itself.

```tsx
import { followed, useFollowValue } from "@/siteplane/components/motion";

function Crosshair({ height, x }: { height: number; x: number }) {
  const at = useFollowValue(x);
  return (
    <followed.line stroke="currentColor" x1={at} x2={at} y1={0} y2={height} />
  );
}
```

A glide takes `--motion-duration-follow` (280 ms) by default. A card that trails the mark, as the tooltip of a chart does, passes `duration: FOLLOW_TRAIL_DURATION`, the `--motion-duration-follow-trail` token (520 ms), so it visibly lags behind. Set `active` to `false` while the mark is hidden: it then sits at its target, appears in its place when it is shown again and glides on from there.

`useFollowPoint` follows an x and a y. `useFollowDerived` makes a value that moves with another, such as the height of a dot that rides a curve while its x glides. A part that has to pass the numbers on as props, such as the anchor of a floating panel, uses `useFollowFrames` to render again on each frame in which they move.

## Reduced motion

Every part is instant when the system asks for reduced motion, when `html` carries `data-motion-reduce="true"`, or inside `SiteplaneMotionProvider preset="reduced"`. Items then appear and disappear in one step, in the same frame; the final state is the same. A number shows its new value at once, the value tweens return their targets, and a follower jumps to its target. The parts render the same markup on the server and while hydrating, and follow the preference right after, so reduced motion never causes a hydration mismatch.

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| MotionList.layout | "stack" \| "flow" | "stack" | Stack is for vertical lists: items expand and collapse. Flow is for wrapping sets: items fade and scale, siblings glide by position. |
| MotionList.gap | number | unset | Space between the items of a stack, in steps of the spacing scale (3 is 12 px). None above the first or below the last item; it opens and closes with the items. |
| MotionList.separator | ReactNode | unset | Drawn in the space between two items, such as a divider or a connecting line, and shown with it. Needs gap. |
| MotionList.container | RefObject<HTMLElement> | the list | Flow only: the element whose height follows when a row of items appears or goes. Pass the element that wraps the items when the list is display: contents. |
| MotionList.render | ReactElement \| render function | ul | Replaces the list element. |
| MotionItem.key | React.Key | required | A stable key, such as an id. The key tells the list which item has left. |
| MotionItem.render | ReactElement | li | Replaces the item element with an element or a component element that passes its ref on. It is the clipping box while the item changes: give it no padding or border. |
| MotionReveal.open | boolean | required | Shows the block. Closing collapses it; nothing is rendered while it is closed. |
| MotionReveal.render | ReactElement | div | Replaces the block element. It is the clipping box while the block changes: give it no padding or border. |
| MotionSwap.swapKey | React.Key | required | Identifies the content. A new key fades the old content out and the new content in after it. |
| MotionSwap.render | ReactElement \| render function | div | Replaces the container element. |
| MotionNumber.value | number \| string | required | The value to show. A number is formatted with locales and format; a string is shown as it is and its digits roll. |
| MotionNumber.locales | Intl.LocalesArgument | "en-US" | The locale a number is formatted in, as Intl.NumberFormat takes it. Fixed by default, so the server and the client always agree. |
| MotionNumber.format | Intl.NumberFormatOptions | unset | Options for a number: currency, percent, compact notation, sign display, units. |
| MotionNumber.render | ReactElement \| render function | span | Replaces the element. |
| useTweenedValues | (target: readonly number[], options?: TweenOptions) => readonly number[] | none | Moves a list of numbers toward its target and returns what to draw this frame; the target itself at rest. A list that changes its length shows at once unless options.keys name the values. |
| useTweenedValue | (target: number, options?: { container }) => number | none | The same for one number. |
| useTweenedRows | (rows, read: (row) => number, options?: { container, absent }) => TweenedRow[] | none | Moves the number of each keyed row (rows have an id) toward the one read gives. A new row starts from its absent number; a row that is gone moves to it and then leaves. Returns the rows in order with their value so far and whether they are leaving. |
| useTweenedRows.absent | (row) => number | 0 | The number a row has while it is not there: where a new row starts and where a leaving row ends. Return the row’s own number to let it appear and disappear where it stands. |
| TweenOptions.container | RefObject<Element> | document root | The element whose motion tokens set the timing: the chart. |
| TweenOptions.keys | readonly string[] | unset | Names the values when their number changes, so each follows its own key; a value with a new key starts at 0 or at its number in starts. |
| TweenOptions.starts | readonly number[] | 0 for each | Where each value with a new key starts, in the order of the target. A value that should appear where it is starts at its own target. |
| useFollowValue | (target: number, options?: FollowValueOptions) => MotionValue<number> | none | Glides a number after its target with the follow spring and returns it as a motion value to bind to a followed element. At rest it is the target itself. |
| useFollowPoint | (target: { x: number; y: number }, options?: FollowValueOptions) => { x, y } | none | The same for a point: two motion values. |
| useFollowDerived | (source: MotionValue<number>, derive: (value: number) => number) => MotionValue<number> | none | A motion value that is derive of the source and moves with it. A derive that changes while the source stands still moves it too. |
| useFollowFrames | (values: readonly MotionValue<number>[]) => void | none | Renders the component again on every frame in which one of the values moves, for a part that has to pass the numbers on as props. Bind the values to an element instead wherever you can. |
| followed | { circle, div, line, path, rect } | none | Motion elements that take motion values as attributes or style. A path has no position attribute: style={{ x, y }} translates it. |
| FollowValueOptions.duration | `--motion-duration-${string}` | FOLLOW_DURATION | The token that sets how long a glide takes. Pass FOLLOW_TRAIL_DURATION for a card that trails a mark. |
| FollowValueOptions.active | boolean | true | Whether the follower is in use. One that is not sits at its target; when it becomes active it appears in its place and glides on from there. |
| FollowValueOptions.container | RefObject<Element> | document root | The element whose motion tokens set the timing: the chart. |
| FOLLOW_DURATION | "--motion-duration-follow" | 280ms | The token of a mark that follows the pointer, such as a crosshair, a dot or an axis label. |
| FOLLOW_TRAIL_DURATION | "--motion-duration-follow-trail" | 520ms | The token of a card that trails the marks, so it visibly lags behind them. |
| followSpring | (seconds: number) => FollowSpring | none | The spring of a glide that takes about seconds, as options for Motion’s animate: critically damped, so it never overshoots, and it keeps the speed of a value that is sent to a new target on the way. |
| followMode | ({ active, reduced, seconds, wasActive }) => FollowMode | none | glide while the follower is active and was active before, motion is not reduced and the token gives it time; jump otherwise. |

All parts pass the remaining props to their element. Durations and easings come from the shared [motion tokens](/docs/customization/motion/#motion-tokens): presence uses `--motion-duration-expand` and `--motion-ease-standard`. A swap takes `--motion-duration-base` in all: the old content fades out within `--motion-duration-micro` (`--motion-ease-soft-in-out`), the new content fades in during the last `--motion-duration-micro` (`--motion-ease-standard`), and the height follows over the whole swap. Flow items scale from `--motion-presence-scale`. Rolling numbers and chart values take `--motion-duration-value` (380 ms by default, the same as `--motion-duration-linger`) with `--motion-ease-standard`. Followers take `--motion-duration-follow` (280 ms) or `--motion-duration-follow-trail` (520 ms) and no easing token: the spring sets its own curve. The tokens are read when an animation starts, so a token set on a container changes the motion inside it. The browser plays the motion: items and reveals as Web Animations, the glide of a list as a translate on the compositor, and a height that follows as a CSS transition. Each starts in the frame after the one that shows the change, so a slow first frame does not cut off the start of the motion. Each part exports its props type, such as MotionListProps or MotionNumberProps, and the hooks export TweenOptions, TweenedRow, FollowValueOptions, FollowMode and FollowSpring.

[Read the installed source](/source/motion).

## Usage notes

A leaving item is inert and hidden from assistive technology while it fades, so focus inside it is lost when it leaves. Move focus to a neighboring item before removing one.

An item, a reveal or a swap container clips its content only while it changes height. At rest nothing is clipped, so focus rings and shadows at the edge stay whole.

A `MotionNumber` is read once, as its final text, and the rolling glyphs are hidden from assistive technology. It sets no live region, so a change is not announced by itself: put `aria-live="polite"` on the element around it when it should be. Only the Latin digits of a string roll; any other character fades in and out. A number without `locales` is formatted in `en-US`, so pass the locale of your page for another language.

A list measures positions only when its keys change, so it can re-render often, for example while progress updates. A leaving item stays right after the item it followed until it has collapsed, so adding an item before it never makes it glide across a neighbor. Items must be direct children of the list: a fragment around several items counts as one. For several hundred rows, virtualize the list instead.
