# Reduced motion and accessibility (/docs/accessibility) ## Reduced motion [#reduced-motion] When the system setting `prefers-reduced-motion: reduce` is on, every cut becomes a short fade: * nothing moves, scales or blurs * fades last between about 120 and 160 ms * the page doesn't sink behind sheets and drawers * no ghosts, and no toasts flying from the button you pressed * [ValueCut](/docs/helpers/value-cut) swaps the value instantly * [LoadCut](/docs/helpers/load-cut) still waits before showing a skeleton, so it doesn't flash, then fades You don't need to do anything for this. It's on for everyone who asks their system for less motion. To check it, open Chrome DevTools, then **Rendering**, then **Emulate CSS media feature prefers-reduced-motion**. ## Focus and keyboard [#focus-and-keyboard] Keyframery never moves focus and never adds focusable elements. Opening, closing and focus trapping stay with Base UI or Radix, exactly as in stock shadcn. Keyboard presses count as the start of a cut just like clicks, so a dialog opened with Enter grows from the focused button. ## Screen readers [#screen-readers] * Ghosts, the copies Keyframery animates when something leaves, are `aria-hidden` and ignore the pointer. * ValueCut's rolling digits are `aria-hidden`. A visually hidden copy always holds the current value. Add `announce` to read changes out with `aria-live="polite"`. * LoadCut sets `aria-busy` while loading. ## Questions ### Does Keyframery respect prefers-reduced-motion? Yes. When the system asks for reduced motion, every cut becomes a short fade of about 120 to 160 ms: nothing moves, scales or blurs. # Use with AI tools (/docs/ai-tools) Keyframery works with any coding agent. Claude Code gets the most: a plugin. Every other agent connects to the same MCP server, and any assistant can read the docs as plain text. ## Claude Code plugin [#claude-code-plugin] The plugin teaches Claude when to use each cut and connects it to the Keyframery MCP server. In Claude Code, run: ```bash /plugin marketplace add keyframery/keyframery /plugin install keyframery@keyframery ``` Then ask for what you want, for example "add Keyframery to this app" or "animate the inbox list". ## MCP server [#mcp-server] The server is at `https://keyframery.com/mcp`. It is read-only, needs no account, and never sees your code. | Tool | What it does | | ------------- | --------------------------------------------------------------------------- | | `list_kinds` | The six kinds of change, with the cut, install command and snippet for each | | `search_docs` | Searches the docs and returns matching pages | | `get_doc` | Returns one docs page as Markdown, such as `helpers/list-cut` | | `make_theme` | Builds the `` line and CSS for a chosen speed, easing or cut | ### Claude Code [#claude-code] Without the plugin: ```bash claude mcp add --transport http keyframery https://keyframery.com/mcp ``` ### Claude [#claude] On the web or desktop: open Settings, then Connectors, add a custom connector, and paste `https://keyframery.com/mcp`. ### Codex [#codex] The Codex CLI and the IDE extension share one setting: ```bash codex mcp add keyframery --url https://keyframery.com/mcp ``` Or add it to the config file yourself: ```toml title="~/.codex/config.toml" [mcp_servers.keyframery] url = "https://keyframery.com/mcp" ``` ### Cursor [#cursor] ```json title=".cursor/mcp.json" { "mcpServers": { "keyframery": { "url": "https://keyframery.com/mcp" } } } ``` ### VS Code (GitHub Copilot) [#vs-code-github-copilot] ```json title=".vscode/mcp.json" { "servers": { "keyframery": { "type": "http", "url": "https://keyframery.com/mcp" } } } ``` ### Gemini CLI [#gemini-cli] ```bash gemini mcp add --scope user --transport http keyframery https://keyframery.com/mcp ``` ### Any other MCP client [#any-other-mcp-client] Add a streamable HTTP server with the address `https://keyframery.com/mcp`. A client that only runs local servers can reach it through `npx -y mcp-remote https://keyframery.com/mcp`. ## llms.txt [#llmstxt] The docs are also published as plain text: | URL | What it has | | ---------------------------------- | --------------------------------------------------------------------------------------------- | | [`/llms.txt`](/llms.txt) | an index of every page, with one line each | | [`/llms-full.txt`](/llms-full.txt) | every page in full, in one file | | any docs page + `.mdx` | that page as Markdown, for example [`/docs/helpers/list-cut.mdx`](/docs/helpers/list-cut.mdx) | Paste `https://keyframery.com/llms-full.txt` into a chat, or add a line to your project rules. Codex and most other agents read `AGENTS.md`; Claude Code reads `CLAUDE.md`. ```md title="AGENTS.md" This project uses Keyframery for motion (https://keyframery.com/llms-full.txt). Use ListCut for lists that change, ValueCut for numbers and statuses that change, LoadCut for skeletons, and MatchCut for a card that opens into a detail view. Don't hand-write enter or exit animations for shadcn components. ``` ## Questions ### Can Claude Code add animations to my app? Yes. Install the plugin with `/plugin marketplace add keyframery/keyframery` and `/plugin install keyframery@keyframery`, then ask Claude to animate your app. It knows which cut fits each change and reads the docs through the MCP server. ### Does the MCP server see my code? No. It is read-only and needs no account. It receives only what your tool sends it, such as a search query or a docs path, and never reads your code. ### Which coding agents does it work with? Any MCP client. This page has the setup for Claude Code, Claude, Codex, Cursor, VS Code (GitHub Copilot) and Gemini CLI, and the docs are also plain text at keyframery.com/llms-full.txt. # What's automatic (/docs/automatic) With `` in your root layout, Keyframery finds shadcn's own parts on the page and animates them. You don't import anything, and nothing in `components/ui` changes. ## Dialogs, alert dialogs and the command menu [#dialogs-alert-dialogs-and-the-command-menu] A dialog grows out of the button that opened it, and closes back into it. Alert dialogs and the command menu do the same. Reference: [Dialog](/docs/components/dialog), [Alert dialog](/docs/components/alert-dialog), [Command](/docs/components/command). ## Sheets and drawers [#sheets-and-drawers] A sheet slides in from its edge while the page steps back behind it. A drawer keeps its own swipe, and the page steps back the same way. Reference: [Sheet](/docs/components/sheet), [Drawer](/docs/components/drawer). ## Tabs [#tabs] The tab indicator moves first and the new panel follows a beat later, while the height eases to fit. Reference: [Tabs](/docs/components/tabs). ## Toasts [#toasts] A toast flies out of the button you pressed, so you can see which action it's about. It works with Sonner and Base UI toasts, and you call `toast()` as usual. Reference: [Toast and Sonner](/docs/components/toast). ## Menus, popovers and the rest [#menus-popovers-and-the-rest] Popover, dropdown menu, context menu, menubar, select, combobox, hover card, tooltip, navigation menu, accordion and collapsible already open from their trigger. Keyframery gives them the same speed and easing as everything else. Reference: [Tuned components](/docs/components/tuned). ## What stays still [#what-stays-still] Buttons, inputs, checkboxes and other controls that don't change the picture stay as they are. Motion there would only be noise. To animate lists, numbers, loading states or a card that opens into a page, use the four small components under **Animate your app** in the sidebar. ## Questions ### Do I need to change my shadcn components? No. `` finds shadcn's own parts by their `data-slot` names and animates them. Nothing in `components/ui` changes. ### Which shadcn/ui components animate automatically? Dialog, alert dialog, command, sheet, drawer, tabs and toasts get their own cuts. Popover, dropdown menu, context menu, menubar, select, combobox, hover card, tooltip, navigation menu, accordion and collapsible keep their motion and get Keyframery's speed and easing. # Base UI vs Radix (/docs/base-ui-vs-radix) shadcn/ui ships each component on two bases: Base UI and Radix. Both name their parts with the same `data-slot` attributes, such as `dialog-content` and `tabs-trigger`. Keyframery finds parts by those names, so the same `` works on either base. Every cut is tested on both, in Chromium, WebKit and Firefox. ## What differs [#what-differs] | | Base UI | Radix | | ------------------- | ------------------------------------------- | ----------------------------------------------- | | Open state on parts | `data-open` / `data-closed` | `data-state="open"` / `"closed"` | | Exit animations | the popup stays mounted until its exit ends | `Presence` keeps it mounted until its exit ends | | Drawer | `drawer-popup`, Base UI's own drawer | `drawer-content`, built on Vaul | | Toast | Base UI `Toast`, plus sonner | sonner | Keyframery reads both forms of the open state, waits for the exit like the library does, and handles both drawers and both toasts. ## What you write is the same [#what-you-write-is-the-same] Menus, variables and attributes are identical on both bases. A project can even switch base later without touching its Keyframery setup. # Changelog (/docs/changelog) ## 0.1.0 (8 October 2026) [#010-8-october-2026] The first release. * ``: rack focus, punch-in and fade for dialog, alert dialog and command; slide-sink, slide and fade for sheet; slide-sink for drawer; J-cut, whip and fade for tabs; cut on action for sonner and Base UI toasts * Eleven tuned components: popover, dropdown-menu, context-menu, menubar, select, combobox, hover-card, tooltip, navigation-menu, accordion and collapsible * Helpers: MatchCut, ListCut, ValueCut and LoadCut * Seven `--kf-*` variables, per-element `data-cut`, `data-cut-pace` and `data-cut-origin`, and the `keyframery:cut` event * Reduced motion: every cut becomes a short fade * Tested on Base UI and Radix, in Chromium, WebKit and Firefox. See [Compatibility](/docs/compatibility) for versions. # Compatibility (/docs/compatibility) Every shadcn component works with `` mounted: no errors, and focus and accessibility intact. The components where a screen changes get a cut. The rest are left alone on purpose, because motion there would be noise. | Component | With Keyframery | Why | | ---------------- | ------------------ | ------------------------------------------------ | | dialog | cut: rack focus | opens on top of the page | | alert-dialog | cut: rack focus | opens on top of the page | | command | cut: rack focus | its dialog opens on top | | sheet | cut: slide-sink | opens on top of the page | | drawer | cut: slide-sink | opens on top of the page | | tabs | cut: J-cut | switches to a neighbour | | sonner | cut: cut on action | a result of what you pressed | | toast | cut: cut on action | a result of what you pressed | | popover | tuned | already opens from its trigger | | dropdown-menu | tuned | already opens from its trigger | | context-menu | tuned | already opens from the pointer | | menubar | tuned | already opens from its trigger | | select | tuned | already opens from its trigger | | combobox | tuned | already opens from its trigger | | hover-card | tuned | already opens from its trigger | | tooltip | tuned | already opens from its trigger | | navigation-menu | tuned | already opens from its trigger | | accordion | tuned | already expands in place | | collapsible | tuned | already expands in place | | card | helper: MatchCut | opens into its detail | | table | helper: ListCut | rows are added, removed and moved | | item | helper: ListCut | rows are added, removed and moved | | message | helper: ListCut | messages arrive | | bubble | helper: ListCut | messages arrive | | message-scroller | helper: ListCut | messages arrive | | badge | helper: ValueCut | its count changes | | skeleton | helper: LoadCut | becomes content | | calendar | planned | months switch like tabs | | carousel | planned | slides switch like tabs | | questionnaire | planned | steps switch like tabs | | pagination | planned | pages switch like tabs | | attachment | planned | files are added and removed | | alert | planned | dismissing it should fold it away | | field | planned | errors should arrive with a cut | | empty | planned | the empty state arrives | | sidebar | planned | labels collapse and expand | | aspect-ratio | no cut | static | | avatar | no cut | static | | breadcrumb | no cut | static | | button | no cut | the press is the action, not a change of picture | | button-group | no cut | static | | checkbox | no cut | its own check is enough | | direction | no cut | not visual | | form | no cut | not visual | | input | no cut | typing isn't a change of picture | | input-group | no cut | static | | input-otp | no cut | typing isn't a change of picture | | kbd | no cut | static | | label | no cut | static | | marker | no cut | static | | native-select | no cut | the browser draws it | | progress | no cut | it already animates its value | | radio-group | no cut | its own dot is enough | | resizable | no cut | follows your pointer | | scroll-area | no cut | follows your scroll | | separator | no cut | static | | slider | no cut | follows your pointer | | spinner | no cut | already animates | | switch | no cut | its own thumb is enough | | textarea | no cut | typing isn't a change of picture | | toggle | no cut | its own state is enough | | toggle-group | no cut | its own state is enough | | chart | no cut | Recharts animates itself | ## Tested versions [#tested-versions] | Package | Versions | | -------------- | ----------------------------------------------------- | | shadcn CLI | 4.21.3 | | @base-ui/react | 1.8 | | radix-ui | 1.7 | | sonner | 2.0.8 | | vaul | 1.1.2 | | cmdk | 1.1.1 | | Next.js | 16.3 | | React | 19.3 | | Tailwind CSS | 4 | | Browsers | Chromium, WebKit (Safari) and Firefox, current stable | # Alert Dialog animation (/docs/components/alert-dialog) ## What it fixes [#what-it-fixes] A confirmation should read as the answer to the button you pressed. With Keyframery, an alert dialog grows out of the Delete button and closes back into it, so you never lose track of what you're confirming. ## Install [#install] ```bash npx shadcn add alert-dialog ``` Nothing else, as long as `` is in your layout. ## Choose a cut [#choose-a-cut] Alert dialogs follow the `dialog` menu: ```tsx ``` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `dialog (on )` | `"rack-focus" \| "punch-in" \| "fade" \| "none"` | `"rack-focus"` | The cut for dialogs, alert dialogs and the command menu. | | `data-cut (on AlertDialogContent)` | `"rack-focus" \| "punch-in" \| "fade" \| "none"` | | The cut for this alert dialog. | | `data-cut-origin (on AlertDialogContent)` | `"center"` | | center starts from the middle. | # Command menu animation (/docs/components/command) ## What it fixes [#what-it-fixes] `CommandDialog` is a dialog, so it gets the same rack focus: it grows out of the button you pressed, or from the centre when you open it with a shortcut. The search on this site runs on it. Press ⌘K to try it. ## Install [#install] ```bash npx shadcn add command ``` ## Choose a cut [#choose-a-cut] The command menu follows the `dialog` menu: ```tsx ``` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `dialog (on )` | `"rack-focus" \| "punch-in" \| "fade" \| "none"` | `"rack-focus"` | The cut for dialogs, alert dialogs and the command menu. | A keyboard shortcut counts as a press only when a focused element receives it. A global ⌘K listener has no opener, so the menu opens from the centre. # Dialog animation (/docs/components/dialog) ## What it fixes [#what-it-fixes] A stock dialog appears in the middle of the screen, unconnected to what you pressed. With Keyframery it starts out toward the button, slightly small and soft, and sharpens in place. That's a rack focus: the camera pulls focus from the page to the dialog. Closing reverses it, back into the button. ## Install [#install] Dialog is shadcn's own component. With [``](/docs/installation) in your layout, there's nothing else to add: ```bash npx shadcn add dialog ``` ## Choose a cut [#choose-a-cut] ```tsx // every dialog, alert dialog and command menu // this dialog only // grow from the centre instead of the button ``` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `dialog (on )` | `"rack-focus" \| "punch-in" \| "fade" \| "none"` | `"rack-focus"` | The cut for every dialog, alert dialog and command menu. | | `data-cut (on DialogContent)` | `"rack-focus" \| "punch-in" \| "fade" \| "none"` | | The cut for this dialog. Beats the prop. | | `data-cut-origin (on DialogContent)` | `"center"` | | center starts a rack focus from the middle instead of the opener. | | `data-cut-pace` | `number` | | Duration multiplier for this dialog, or for everything inside an ancestor. | | Cut | Opening | Closing | | ------------ | -------------------------------------------------------------- | ------------------------------ | | `rack-focus` | from 35% toward the opener, at 0.94 scale and 8px blur, 340 ms | back toward the opener, 220 ms | | `punch-in` | from the centre, 0.85 to 1.02 to 1, 260 ms | to 0.96 and fade, 160 ms | | `fade` | opacity only, 200 ms | 150 ms | A dialog opened from code, with no press, has no opener, so a rack focus starts from the centre. ## Questions ### Why doesn't my shadcn dialog animate from the button? Check that `` is rendered once in your root layout, and that your `DialogContent` has `data-slot="dialog-content"`. Components added by shadcn CLI 3 and later have it; run `npx shadcn add dialog` to update an older copy. ### How do I disable the animation on one shadcn dialog? Add `data-cut="none"` to its `DialogContent` to keep shadcn's own motion there. For no motion at all, also remove the `animate-in` and `animate-out` classes from `DialogContent` in your `components/ui`. ### Does it work on Base UI and Radix? Yes. Keyframery reads `data-open` on Base UI and `data-state` on Radix, and every cut is tested on both in Chromium, Firefox and WebKit. # Drawer animation (/docs/components/drawer) ## What it fixes [#what-it-fixes] Drawers already have good motion: they follow your finger. What they're missing is depth. With `slide-sink`, the page behind steps back as the drawer rises, the way iOS sheets do. The drawer's own swipe and spring are never touched. It works with both drawers: Base UI's and the Radix base's Vaul drawer. ## Install [#install] ```bash npx shadcn add drawer ``` ## Choose a cut [#choose-a-cut] ```tsx // keep the page still behind drawers ``` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `drawer (on )` | `"slide-sink" \| "none"` | `"slide-sink"` | Whether the page steps back behind drawers. | | `data-cut (on the drawer content)` | `"slide-sink" \| "none"` | | none keeps the page still for this drawer. | # Sheet animation (/docs/components/sheet) ## What it fixes [#what-it-fixes] A stock sheet slides over a page that doesn't react, so the two look flat against each other. With `slide-sink`, the page behind scales to 0.975, dims slightly and rounds its corners while the sheet arrives. Now the sheet reads as on top. When it closes, the page comes forward again. The part of the page that steps back is the top-level element in `` that holds the button you pressed. A sheet opened from inside another sheet doesn't sink the page twice. ## Install [#install] ```bash npx shadcn add sheet ``` ## Choose a cut [#choose-a-cut] ```tsx ``` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `sheet (on )` | `"slide-sink" \| "slide" \| "fade" \| "none"` | `"slide-sink"` | The cut for every sheet. | | `data-cut (on SheetContent)` | `"slide-sink" \| "slide" \| "fade" \| "none"` | | The cut for this sheet. | | `data-cut-pace` | `number` | | Duration multiplier. | | Cut | Opening | Closing | | ------------ | -------------------------------------------------- | ---------------------------- | | `slide-sink` | from its edge over 460 ms, and the page steps back | 260 ms, and the page returns | | `slide` | the same, without the page stepping back | same | | `fade` | opacity only, 200 ms | 150 ms | ## Questions ### Can the page stay still behind the sheet? Yes. Use ``: the sheet slides in from its edge without the page stepping back. # Tabs animation (/docs/components/tabs) ## What it fixes [#what-it-fixes] Stock tabs swap their panel in a single frame, and the frame snaps to the new height. With the J-cut, the active pill whips across first, the old panel leaves, and the new panel enters 70 ms later. Meanwhile the frame's height morphs smoothly. Your eye follows the pill, and the panel arrives where you're already looking. ## Install [#install] ```bash npx shadcn add tabs ``` ## Choose a cut [#choose-a-cut] Put `data-cut` on the `Tabs` root: ```tsx … ``` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `tabs (on )` | `"j-cut" \| "whip" \| "fade" \| "none"` | `"j-cut"` | The cut for every tabs component. | | `data-cut (on Tabs)` | `"j-cut" \| "whip" \| "fade" \| "none"` | | The cut for this tabs component. | | `data-cut-pace (on Tabs)` | `number` | | Duration multiplier. | | Cut | What plays | | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `j-cut` | the pill whips over (200 ms), the old panel leaves (180 ms), the new panel enters 70 ms later (300 ms), and the height morphs (320 ms) | | `whip` | no lead, 160 ms, with a 4px motion blur on both panels and the height morph | | `fade` | a 180 ms crossfade and the height morph | ## Questions ### How do I animate the shadcn/ui tabs indicator? Render `` once. The tab indicator then slides to the new tab, the new panel follows a beat later, and the height eases to fit. Your `Tabs` don't change. ### Can the tabs switch faster? Yes. `` switches in 160 ms with a short motion blur, or put `data-cut="whip"` on one `Tabs` root. # Toast and Sonner animation (/docs/components/toast) ## What it fixes [#what-it-fixes] A stock toast slides in from the corner, far from the button that caused it. With cut on action, it starts at the button, small and slightly blurred, and flies to its place. You see which action the toast is about without reading it. If nothing was pressed in the last second, as with a toast from a websocket message, the library's own entrance plays. It works with sonner on both bases, and with Base UI's own `Toast`. ## Install [#install] ```bash npx shadcn add sonner ``` ## Choose a cut [#choose-a-cut] ```tsx ``` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `toast (on )` | `"cut-on-action" \| "none"` | `"cut-on-action"` | The cut for sonner and Base UI toasts. | The flight takes 480 ms × pace, from 0.4 scale and a 6px blur. The toast's exit is the library's own. ## Questions ### Do I need to change my toast() calls? No. Call `toast()` as usual. Each toast flies out of the element you pressed, with Sonner and with Base UI toasts. # Popover, menu and select animations (/docs/components/tuned) These components already open from their trigger in stock shadcn. Keyframery doesn't replace their motion. It retimes their own keyframes to 180 ms × pace with `--kf-ease`, so they move at the same pace as everything else, and it makes sure closing returns to the trigger. | Component | | | | ---------- | ------------- | --------------- | | popover | dropdown-menu | context-menu | | menubar | select | combobox | | hover-card | tooltip | navigation-menu | | accordion | collapsible | | ## Install [#install] Nothing beyond ``. ## Options [#options] They have no menu, and they follow the [variables](/docs/customize#css-variables). | Prop | Type | Default | Description | | --- | --- | --- | --- | | `data-cut="none"` | `"none"` | | Stock timing for this element or everything inside it. | | `data-cut-pace` | `number` | | Duration multiplier. | # Speed, easing and cuts (/docs/customize) There are three levels. Use the first one that does what you need. | You want to | Use | | -------------------------------------------------------- | -------------------------------------- | | change speed or easing everywhere, or in one section | [CSS variables](#css-variables) | | pick a different cut for dialogs, sheets, tabs or toasts | [`` props](#cuts-props) | | change one element, or turn motion off in one area | [attributes](#one-element-or-one-area) | The [Theme page](/theme) builds the variables and props for you, with a live preview. ## CSS variables [#css-variables] | Variable | Default | What it does | | ---------------- | -------------------------------- | -------------------------------------------------------------------------- | | `--kf-pace` | `1` | multiplies every duration (2 is half speed) | | `--kf-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | easing for entrances | | `--kf-ease-exit` | `cubic-bezier(0.5, 0, 0.75, 0)` | easing for exits | | `--kf-travel` | `0.35` | how far toward the opener a dialog starts (0 starts from the centre) | | `--kf-blur` | `8px` | the blur a dialog starts from | | `--kf-depth` | `0.94` | the scale a dialog starts from | | `--kf-hold` | `300ms` | how long [LoadCut](/docs/helpers/load-cut) waits before showing a skeleton | For the whole app, put them in your global stylesheet: ```css title="app/globals.css" :root { --kf-pace: 1.2; --kf-blur: 4px; } ``` Keyframery declares its defaults with zero specificity, so your `:root` always wins, wherever it sits in the file. For one section, set them on any element. They cascade, and dialogs and menus opened from inside the section use them too: ```tsx
…
``` ## `` props [#cuts-props] ```tsx ``` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `dialog` | `"rack-focus" \| "punch-in" \| "fade" \| "none"` | `"rack-focus"` | The cut for dialog, alert dialog and command. | | `sheet` | `"slide-sink" \| "slide" \| "fade" \| "none"` | `"slide-sink"` | The cut for sheet. | | `drawer` | `"slide-sink" \| "none"` | `"slide-sink"` | The cut for drawer. Its own swipe motion is always kept. | | `tabs` | `"j-cut" \| "whip" \| "fade" \| "none"` | `"j-cut"` | The cut for tabs. | | `toast` | `"cut-on-action" \| "none"` | `"cut-on-action"` | The cut for sonner and Base UI toasts. | | `pace` | `number` | `1` | Multiplies every duration. Sets --kf-pace on , so it beats your CSS. | | `enabled` | `boolean` | `true` | false plays shadcn's own motion everywhere. | `none` means "leave the library's own motion alone". ### What each cut does [#what-each-cut-does] Durations are multiplied by `--kf-pace`. | Cut | Opening | Closing | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | | `rack-focus` | starts 35% of the way toward the button that opened it, at 0.94 scale and 8px blur, then sharpens in place over 340 ms while the backdrop fades in | returns toward the button over 220 ms | | `punch-in` | from the centre: scale 0.85, past 1.02, to 1, over 260 ms | scales to 0.96 and fades over 160 ms | | `fade` | opacity only, 200 ms | 150 ms | | `slide-sink` | the panel travels in from its edge over 460 ms, while the page behind scales to 0.975, dims slightly and rounds its corners | 260 ms, and the page returns | | `slide` | like `slide-sink`, without the page stepping back | same | | `j-cut` | the tab pill whips to the new tab, the old panel leaves, and the new one enters 70 ms later while the frame's height morphs | | | `whip` | no lead, 160 ms, with a 4px motion blur on both panels | | | tabs `fade` | a 180 ms crossfade with the height morph | | | `cut-on-action` | the toast flies from the element you pressed, from 0.4 scale and a 6px blur, over 480 ms | the library's own exit | The eleven [tuned components](/docs/components/tuned) have no menu. They follow the variables. ## One element or one area [#one-element-or-one-area] | Attribute | Put it on | Effect | | -------------------------- | ------------------------------------------------------------ | ----------------------------------------- | | `data-cut=""` | a shadcn part, for example `` | use that cut here | | `data-cut="none"` | a part, or any ancestor like `
` | stock shadcn motion inside | | `data-cut-pace="1.5"` | a part or any ancestor | sets `--kf-pace` there | | `data-cut-origin="center"` | a dialog's content | open from the centre, not from the button | ```tsx …
{/* everything in here, including the dialogs it opens, uses shadcn's own motion */}
… ``` For tabs, put `data-cut` on the `Tabs` root. The four components under **Animate your app** take a `cut` prop instead, for example ``, and they also respect `data-cut="none"` and `data-cut-pace` on their ancestors. ### Which setting wins [#which-setting-wins] From weakest to strongest: 1. Keyframery's default cut 2. the `` prop for that group 3. the element's own `data-cut` The order of your stylesheets doesn't matter. A named cut, such as `data-cut="fade"`, applies only to the element it's written on. Only `data-cut="none"` reaches the elements inside, including dialogs that render in a portal. See [Portals](/docs/how-it-works#portals). ## Turn it off [#turn-it-off] ```tsx ``` Every component plays shadcn's own motion. To turn it off in one area only, put `data-cut="none"` on it. ## Listen to cuts [#listen-to-cuts] Every cut fires a `keyframery:cut` event on `document`: ```ts document.addEventListener("keyframery:cut", (e) => { const { cut, component, phase, ms } = (e as CustomEvent).detail // e.g. { cut: "rack-focus", component: "dialog", phase: "enter", ms: 340 } }) ``` It's handy for analytics and in tests. See [Turning motion off in tests](/docs/guides/testing). ## Questions ### How do I slow down or speed up every animation? Set `--kf-pace` in your global CSS, for example `:root { --kf-pace: 1.2; }`. It multiplies every duration, so 2 is half speed. `` does the same. ### How do I turn the animation off for one dialog? Add `data-cut="none"` to its `DialogContent`, and that dialog keeps shadcn's own motion. On a section, `data-cut="none"` covers everything inside it, including the dialogs it opens. ### Can a dialog open from the centre instead of the button? Yes. Add `data-cut-origin="center"` to one dialog's content, or use `` for every dialog. # Loading data after a click (/docs/guides/data-loading) There are two moments to cover: the first load, when a skeleton becomes content, and every refetch after it, when values change in place. ## First load: LoadCut [#first-load-loadcut] Pass the library's loading flag straight to `loading`: ```tsx import { useQuery } from "@tanstack/react-query" import { LoadCut } from "@/components/keyframery/load-cut" import { Skeleton } from "@/components/ui/skeleton" export function Revenue() { const { data, isPending } = useQuery({ queryKey: ["revenue"], queryFn: fetchRevenue }) return ( }> {data && } ) } ``` With SWR, use `isLoading`. With `fetch` in an effect, use your own `loading` state. Make the skeleton roughly the size of the content. LoadCut morphs the height between them, but the closer they are, the calmer the change. ## Refetches: ValueCut [#refetches-valuecut] After the first load, don't show the skeleton again for a refetch. Keep the old values on screen and let them change in place: ```tsx ``` ## Changing a filter [#changing-a-filter] When a filter changes and the old content no longer applies (a different date range, say), set `loading` while the new data loads. If it comes back within 300 ms, the skeleton never shows. The [dashboard on the home page](/) does exactly this. # Detail pages on their own route (/docs/guides/detail-routes) [MatchCut](/docs/helpers/match-cut) works across a client-side route change, because it only needs the same `id` to leave and arrive within a second. ```tsx title="app/orders/page.tsx" import Link from "next/link" import { MatchCut } from "@/components/keyframery/match-cut" export default async function Orders() { const orders = await getOrders() return (
    {orders.map((o) => (
  • {o.customer}
  • ))}
) } ``` ```tsx title="app/orders/[id]/page.tsx" import { MatchCut } from "@/components/keyframery/match-cut" export default async function Order({ params }: { params: Promise<{ id: string }> }) { const { id } = await params const order = await getOrder(id) return ( ) } ``` `MatchCut` is a client component, and the pages around it can stay server components. ## Things to know [#things-to-know] * **Navigate on the client.** A `` or `router.push` keeps the page alive, so the match can play. A full page load starts fresh and plays nothing. * **Back works too.** Going back mounts the card again with the same `id`, so the detail shrinks into it. * **Slow routes.** If the detail page takes longer than a second to render, there's nothing left to match, and it simply appears. Add a `loading.tsx` that renders the same `MatchCut` with a skeleton inside, so the match plays at once and the content dissolves in. # Live data (/docs/guides/live-data) Live data changes without anyone pressing anything, so the cuts that need a press step aside on their own. ## Lists [#lists] Wrap the list in [ListCut](/docs/helpers/list-cut) and key items by their server id: ```tsx {events.map((e) => ( {e.title} ))} ``` * Items that arrive from the server rise in, because nobody pressed anything in the last 800 ms. * Items you add yourself fly from the button you pressed, even if the server echoes them back later, as long as the id is stable. * Reorders from the server glide. Don't give an optimistic item a temporary id that changes when the server answers. ListCut would see one item removed and another added. Generate the id on the client and send it to the server. ## Numbers [#numbers] Use [ValueCut](/docs/helpers/value-cut) for counters that tick. Add `announce` only for values someone is waiting on, like an order status. A ticking counter read aloud every second is noise. ## Toasts [#toasts] A toast fired from a websocket message has no press, so it plays the library's own entrance instead of flying from a button. # Performance (/docs/guides/performance) ## What runs [#what-runs] * **One** `MutationObserver` that watches for shadcn parts appearing and changing state * **Two** listeners in the capture phase, `pointerdown` and `keydown`, that remember the last press * Animations of `transform`, `opacity` and `filter` only, which the browser runs off the main thread where it can * Ghosts, the copies of leaving elements, which are removed as soon as their animation ends or is cancelled Nothing runs while nothing changes. ## Size [#size] The layer is about 7 KB gzipped once your bundler minifies it: `cuts.tsx`, its stylesheet and the engine. Each helper adds about 1.5–2.5 KB. ## Lists [#lists] ListCut measures its items once per render, in a layout effect. Only items in or near the viewport animate, so a list of thousands stays fast. Key items by a stable id, and avoid re-rendering the whole list on every keystroke elsewhere on the page. ## Slow devices [#slow-devices] Blur is the most expensive part of a rack focus. If you target low-end phones, turn it off: ```css :root { --kf-blur: 0px; } ``` # Turning motion off in tests (/docs/guides/testing) ## Turn Keyframery off [#turn-keyframery-off] The simplest way is `enabled={false}` while testing: ```tsx title="app/layout.tsx" ``` Then every component plays shadcn's own motion, exactly as before you added Keyframery. ## Or test with reduced motion [#or-test-with-reduced-motion] Playwright can ask for reduced motion, and then every cut is a fade of about 150 ms: ```ts title="playwright.config.ts" export default defineConfig({ use: { reducedMotion: "reduce" }, }) ``` Wait for the state you expect, such as a dialog being visible, never for a fixed time. ## Assert that a cut played [#assert-that-a-cut-played] Listen to `keyframery:cut` in the page: ```ts await page.addInitScript(() => { window.__cuts = [] document.addEventListener("keyframery:cut", (e) => window.__cuts.push(e.detail)) }) await page.getByRole("button", { name: "Edit profile" }).click() await expect.poll(() => page.evaluate(() => window.__cuts.map((c) => c.cut))).toContain("rack-focus") ``` ## Unit tests [#unit-tests] In jsdom or happy-dom, there are no real animations. Keyframery's parts catch the error and step aside, so components render and behave as stock shadcn. You don't need to mock anything. # List animations (/docs/helpers/list-cut) In a stock list, a new item appears from nowhere, a deleted one vanishes and everything below jumps up, and a reorder teleports every row. `ListCut` gives each of those changes a motion you can follow. ## Install [#install] ```bash npx shadcn add @keyframery/list-cut ``` If your project doesn't know `@keyframery` yet, do the [Quick start](/docs/installation) first. ## Use [#use] Replace your list and its items with `ListCut` and `ListCut.Item`, and give each item the same `id` you use as its `key`: ```tsx import { ListCut } from "@/components/keyframery/list-cut" {messages.map((m) => ( {m.text} ))} ``` It works with any data source, because it only compares ids between renders. For a table, use `ListCut as="tbody"` with `ListCut.Item as="tr"`. ## You should see [#you-should-see] * **Add an item** with a button: it flies out of that button into its place. * **Add an item** with no button involved, for example from the server: it rises in. * **Remove an item:** it folds away, and the items below slide up a moment later. * **Reorder:** every item glides to its new place. ## Options [#options] | Prop | Type | Default | Description | | --- | --- | --- | --- | | `as` | `React.ElementType` | `"div"` | The list element. | | `pace` | `number` | | Duration multiplier for this list. | | `cut` | `"none"` | | none renders changes without motion. | `ListCut.Item`: | Prop | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string \| number` | required | The item's stable id. | | `as` | `React.ElementType` | `"div"` | The item element. | Both pass any other props, such as `className` or `aria-label`, to their element. ## Good to know [#good-to-know] * The first render never animates. * Only items in or near the viewport animate, so long lists stay fast. * Timing: an added item flies in over 480 ms; a removed one folds away and the items below follow 60 ms later; a moved one glides over 320 ms. * A removed table row folds inside a one-row table that keeps your column widths. In film, a new item flying from the button is a **cut on action**, and a removed item leaving before the rest move up is an **L-cut**. ## Questions ### How do I animate adding and removing items in a React list? Wrap the list in `ListCut` and each item in `ListCut.Item` with the same `id` you use as its `key`. Added items fly in from the button that added them, removed ones fold away, and moved ones glide. ### Does ListCut work with tables? Yes. Use `ListCut as="tbody"` with `ListCut.Item as="tr"`. A removed row folds inside a one-row table that keeps your column widths. ### Does it work with data from the server? Yes. ListCut only compares ids between renders, so any data source works. Items that arrive while nothing was pressed rise in. # Skeleton to content transitions (/docs/helpers/load-cut) A stock skeleton blinks on for a moment even when the data is fast, then snaps to content of a different height, and the page jumps. `LoadCut` fixes both. ## Install [#install] ```bash npx shadcn add @keyframery/load-cut ``` If your project doesn't know `@keyframery` yet, do the [Quick start](/docs/installation) first. ## Use [#use] Wrap the content, and pass your loading flag and skeleton: ```tsx import { LoadCut } from "@/components/keyframery/load-cut" import { Skeleton } from "@/components/ui/skeleton" }> ``` ## You should see [#you-should-see] * **Fast data:** no skeleton at all. The content just appears, and nothing on the page jumps. * **Slow data:** the skeleton shows, then dissolves into the content while the height eases from one to the other. ## Options [#options] | Prop | Type | Default | Description | | --- | --- | --- | --- | | `loading` | `boolean` | required | true while the data is loading. | | `skeleton` | `React.ReactNode` | required | What to show while loading. | | `hold` | `number` | `--kf-hold (300)` | How long to wait before showing the skeleton, in ms. | | `minShow` | `number` | `400` | Once shown, the skeleton stays at least this long, in ms. | | `pace` | `number` | | Duration multiplier. | | `cut` | `"dissolve" \| "none"` | `"dissolve"` | none swaps the skeleton and content without the dissolve. | | `className` | `string` | | Classes for the wrapper. | ## Good to know [#good-to-know] * LoadCut waits `--kf-hold` (300 ms) before it shows the skeleton. During the wait the space is reserved but empty, so nothing jumps. * Once shown, the skeleton stays at least 400 ms, so it never flashes, then dissolves over 240 ms. * LoadCut sets `aria-busy` while loading. In film, one shot fading into the next is a **dissolve**. ## Questions ### How do I stop a skeleton from flashing when data loads fast? Wrap the content in `LoadCut`. It waits 300 ms before showing the skeleton, so fast data never shows one, and once shown the skeleton stays at least 400 ms. ### Does it prevent layout shift? Yes. While it waits, LoadCut reserves the space, and the height eases from the skeleton to the content instead of jumping. # Cards that open into a page (/docs/helpers/match-cut) When a card opens into its detail view, a stock app swaps one screen for the other. With `MatchCut`, the detail grows out of the card, and going back shrinks it into the card again. ## Install [#install] ```bash npx shadcn add @keyframery/match-cut ``` If your project doesn't know `@keyframery` yet, do the [Quick start](/docs/installation) first. ## Use [#use] Wrap the small version and the big version in `MatchCut`, with the same `id`: ```tsx import { MatchCut } from "@/components/keyframery/match-cut" // app/orders/page.tsx // app/orders/[id]/page.tsx ``` It works for a conditional render on one page and for a client-side route change. See [Detail pages on their own route](/docs/guides/detail-routes). ## You should see [#you-should-see] Click a card: the detail grows out of the card's box, while a copy of the card fades out above it. Go back: the detail shrinks into the card. ## Options [#options] | Prop | Type | Default | Description | | --- | --- | --- | --- | | `id` | `string` | required | The same id on the small and the big version. | | `as` | `React.ElementType` | `"div"` | The element to render. | | `pace` | `number` | | Duration multiplier for this match. | | `cut` | `"match-cut" \| "none"` | `"match-cut"` | none renders without matching. | | `className` | `string` | | Classes for the element. | | `style` | `React.CSSProperties` | | Styles for the element. | ## Good to know [#good-to-know] * A MatchCut that mounts within a second of another one with the same `id` leaving grows out of it. * The new element animates from the old box to its own over 420 ms: position, scale and corner radius. The copy of the old one fades out by 40% of the way. * If the old and the new element sit at the same spot on the page, nothing plays. In film, this is a **match cut**: a shape in one shot becomes the same shape in the next. ## Questions ### What is a shared element transition? It's when one element on screen becomes another, such as a card growing into its detail view. MatchCut does it in React: give the small and the big version the same `id`. ### Does it work across Next.js routes? Yes. It works for a conditional render on one page and for a client-side route change in the Next.js App Router. Going back shrinks the detail into the card again. # Animated numbers and statuses (/docs/helpers/value-cut) When a stock number changes, it just changes, and nobody notices. `ValueCut` rolls only the digits that changed, in the direction of the change, and gives the value a small punch. Text, like an order status, crossfades instead. ## Install [#install] ```bash npx shadcn add @keyframery/value-cut ``` If your project doesn't know `@keyframery` yet, do the [Quick start](/docs/installation) first. ## Use [#use] Put `ValueCut` where the value is rendered: ```tsx import { ValueCut } from "@/components/keyframery/value-cut" ``` ## You should see [#you-should-see] When the value changes, only the digits that changed roll up or down, and the whole value briefly grows and settles. A status crossfades with a small slide. ## Options [#options] | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `number \| string` | required | The number or text to show. | | `format` | `Intl.NumberFormatOptions` | | Intl.NumberFormat options, for numbers. | | `locale` | `string` | `the browser's` | The locale for number formatting. | | `announce` | `boolean` | `false` | Read changes out to screen readers (aria-live="polite"). | | `pace` | `number` | | Duration multiplier. | | `cut` | `"punch-in" \| "none"` | `"punch-in"` | none swaps the value instantly. | | `className` | `string` | | Classes for the wrapper span. | ## Good to know [#good-to-know] * Numbers use tabular figures, so the digits don't shift sideways. * The punch scales the value to 1.06 and back over 300 ms. Text crossfades with a 6px slide. * The first render never animates, and with reduced motion the value swaps instantly. In film, a quick push toward the subject is a **punch-in**. ## Questions ### How do I animate a number counter in React? Render the number with ``. When it changes, only the digits that changed roll, in the direction of the change. ### Can it format currency and compact numbers? Yes. Pass `Intl.NumberFormat` options, for example `format={{ style: "currency", currency: "USD" }}` with `locale="en-US"`, or `format={{ notation: "compact" }}`. ### Do screen readers hear the change? With `announce`, yes: ValueCut reads changes out politely through `aria-live`. # How it works (/docs/how-it-works) ## The six kinds of change [#the-six-kinds-of-change] Every change on a screen is one of six kinds, and Keyframery has one cut for each. See them playing on [the Cuts page](/cuts). | Kind of change | Example | Cut | Where it comes from | | ---------------------------------- | ------------------------------------ | ------------------------------------- | ----------------------------------- | | Something opens on top | a dialog, a sheet, a menu, a toast | rack focus, slide-sink, cut on action | ``, automatic | | You switch to a neighbour | a tab | J-cut, whip | ``, automatic | | A thing opens into its bigger self | a card becomes its detail page | match cut | [MatchCut](/docs/helpers/match-cut) | | A list changes | a message is sent, a row is archived | cut on action, L-cut, glide | [ListCut](/docs/helpers/list-cut) | | A value changes in place | a price, a count, a status | punch-in | [ValueCut](/docs/helpers/value-cut) | | A placeholder becomes real | a skeleton turns into a chart | dissolve | [LoadCut](/docs/helpers/load-cut) | The first two need no code, because shadcn already has the parts and Keyframery finds them. The other four are changes shadcn has no component for, so each one is a small component you wrap around your own markup. ## What the layer does [#what-the-layer-does] `` renders nothing and never changes your components' props. When it mounts: 1. It sets `data-kf` on ``, which switches on the cut stylesheet. 2. **It remembers presses.** A listener in the capture phase records the element you last pressed with a pointer or key, with its box and the time. That's where the next cut starts: a dialog grows from it, and a toast flies from it. 3. **It watches for parts.** One `MutationObserver` notices when a shadcn part appears or changes state, by its `data-slot` name and its open state (`data-open` on Base UI, `data-state` on Radix). 4. **It plays the cut.** * *CSS cuts*, for dialogs, sheets and the eleven tuned components: a stylesheet, active only under `html[data-kf]`, replaces the library's keyframes. For a rack focus, Keyframery measures the opener and the dialog and writes the distance as `--kf-dx` and `--kf-dy`, so the dialog starts out toward the button. * *JS cuts*, for tabs, toasts and the page sinking behind a sheet: these use the Web Animations API, because they animate things the library doesn't, like the tab pill, the old panel leaving and the toast's flight path. 5. **It reads your settings at play time.** Every cut reads the `--kf-*` variables from the element it animates, so a variable set on a section applies to everything in it. Then it fires a `keyframery:cut` event. It's safe with server rendering: on the server it renders nothing, and the page looks exactly like stock shadcn until it mounts. ### When something goes wrong [#when-something-goes-wrong] Each part catches its own errors and falls back to doing nothing, so the library's stock motion plays and nothing else breaks. When `` unmounts, the page is stock shadcn again. ## Exits [#exits] Base UI and Radix both keep a closing part mounted until its exit animation ends. Keyframery's exit keyframes replace the stock ones and hold the last frame, so the part stays invisible until the library removes it. Nothing is removed early, and nothing flashes back. A rack focus closes back into the element that opened it, even if that element has scrolled since. ## Portals [#portals] Dialogs, sheets, menus and toasts render in a portal at the end of ``, outside the section they were opened from. So when you open one, Keyframery copies the opener's settings onto it: 1. If the opener is inside `data-cut="none"`, the portaled part gets `data-cut="none"` too, unless it has its own `data-cut`. 2. The opener's `data-cut-pace` and its `--kf-*` values are copied over. So `
` slows down the dialogs opened from inside it, even though they render somewhere else. Named cuts are never copied, because a tabs cut means nothing to a dialog. If code opens a dialog without a press, from a timer for example, the page's own settings apply and a rack focus starts from the centre. ## Questions ### Does Keyframery work with server rendering? Yes. On the server `` renders nothing, so the page looks exactly like stock shadcn until it mounts. ### What happens if something goes wrong? Each part catches its own errors and falls back to doing nothing, so the library's stock motion plays and nothing else breaks. # Introduction (/docs) Keyframery adds animations to the shadcn/ui components you already have. Render one `` in your root layout, and your dialogs, sheets, tabs and toasts start moving from the button you pressed, and back into it. Switch the preview to **Stock** to see shadcn's own motion, then back to **Keyframery**. ## What you get [#what-you-get] * **Motion with no code changes.** Dialogs, alert dialogs, sheets, drawers, tabs, toasts, the command menu and eleven more components animate as soon as `` is in your layout. Nothing in `components/ui` changes. See [what's automatic](/docs/automatic). * **Four small components** for the changes shadcn has no component for: [cards that open into a page](/docs/helpers/match-cut), [lists](/docs/helpers/list-cut), [numbers and statuses](/docs/helpers/value-cut) and [loading states](/docs/helpers/load-cut). * **Control like colours.** Speed, easing and the cut each component uses are CSS variables and props. See [Customize](/docs/customize). Like the rest of shadcn, it's code you own: the CLI copies it into your project. Remove `` and you have stock shadcn again. ## Start here [#start-here] 1. [Quick start](/docs/installation): three steps, about two minutes. 2. Using Claude Code or another AI tool? See [Use with AI tools](/docs/ai-tools). ## Why "cuts"? [#why-cuts] In film, a cut is how one shot becomes the next. A jump cut, where something suddenly appears in a new place, is jarring, and most interface changes are jump cuts. Keyframery replaces them with the cuts an editor would choose. [See each one](/cuts). # Quick start (/docs/installation) You need a project with shadcn/ui set up, so `components.json` exists. Base UI and Radix both work. ### Register Keyframery and install it [#register-keyframery-and-install-it] ```bash npx shadcn registry add "@keyframery=https://keyframery.com/r/{name}.json" npx shadcn add @keyframery/cuts ``` The first command adds Keyframery's registry to `components.json`, once per project. The second copies `components/keyframery/cuts.tsx`, its stylesheet and a small engine in `lib/keyframery/` into your project. No npm packages are added. **You should see** `"@keyframery"` under `registries` in `components.json`, and a new `components/keyframery/` folder. ### Render `` once, at the root [#render-cuts--once-at-the-root] **Next.js** ```tsx title="app/layout.tsx" import { Cuts } from "@/components/keyframery/cuts" export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` **Vite** ```tsx title="src/main.tsx" import { StrictMode } from "react" import { createRoot } from "react-dom/client" import { Cuts } from "@/components/keyframery/cuts" import App from "./App" import "./index.css" createRoot(document.getElementById("root")!).render( , ) ``` **React Router** ```tsx title="app/root.tsx" import { Outlet } from "react-router" import { Cuts } from "@/components/keyframery/cuts" export default function App() { return ( <> ) } ``` **You should see** a `data-kf` attribute on the `` element in your browser's dev tools. `` sets it while it runs. ### Open a dialog [#open-a-dialog] Open any dialog in your app. **You should see** it grow out of the button you pressed, and close back into it. Tabs now slide, sheets push the page back, and toasts fly from the button that made them. ## If the dialog still pops into the middle [#if-the-dialog-still-pops-into-the-middle] * Make sure `` is rendered once, in the root layout or entry file. * Check that your dialog's content element has `data-slot="dialog-content"`. Components added by shadcn CLI 3 and later have it; older copies don't. Run `npx shadcn add dialog` again to update yours. * If the CLI answered `Unknown registry "@keyframery"`, run the first command of step 1 again, or add the registry to `components.json` by hand: ```json title="components.json" { "registries": { "@keyframery": "https://keyframery.com/r/{name}.json" } } ``` ## Next [#next] * See [what's automatic](/docs/automatic) now that `` is running. * Animate [lists](/docs/helpers/list-cut), [numbers and statuses](/docs/helpers/value-cut), [loading states](/docs/helpers/load-cut) and [cards that open into a page](/docs/helpers/match-cut). Each one installs by name, for example `npx shadcn add @keyframery/list-cut`. * To try the layer without registering first, `npx shadcn add https://keyframery.com/r/cuts.json` installs it by URL. That works for `cuts` only, because the other components depend on it. ## Questions ### Do I need Framer Motion or another animation library? No. Keyframery adds motion with CSS and the Web Animations API. The CLI copies ``, its stylesheet and a small engine into your project, and adds no npm packages. ### Why does the CLI say Unknown registry "@keyframery"? Your project hasn't registered Keyframery yet. Run `npx shadcn registry add "@keyframery=https://keyframery.com/r/{name}.json"` once, then install again. ### Does it work with Vite and React Router? Yes. Render `` once in `src/main.tsx` for Vite or `app/root.tsx` for React Router, the same way as in a Next.js root layout.