animate-ui
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAnimate UI
Animate UI
Animate UI animates the primitives components are built on. Its package, , ships each Radix UI, Base UI and React Aria primitive with exactly its library's API and Motion inside, so swapping the import is the whole change.
@animate-ui/reactIts motion is springy and alive, but coherent: nearly everything moves on a spring, each motion role has one value (the tokens), and tweens pick their curve by what the element does. It borrows Emil Kowalski's practical rules (trigger-anchored origins, ease-out for what enters and leaves, press feedback, a light blur to bridge swaps; see NOTICE.md) without his strictness: springs may overshoot a little, popups can start from 0.7, and the keyboard animates like the pointer.
Animate UI 会为组件所基于的 primitives 添加动画。其包 提供每个 Radix UI、Base UI 和 React Aria primitive,API 与对应库完全一致,并在内部集成 Motion,因此只需替换 import 即可完成全部改动。
@animate-ui/react它的 motion 富有弹性且充满活力,但整体协调一致:几乎所有内容都使用 spring,每种 motion role 只有一个对应值(tokens),而 tweens 会根据元素的行为选择 curve。它借鉴 Emil Kowalski 的实用规则(以 trigger 为锚点的 origins、进入和离开时使用 ease-out、press feedback、用轻微 blur 衔接替换;见 NOTICE.md),但没有那么严格:springs 可以略微 overshoot,popups 可以从 0.7 开始,键盘动画的表现也与 pointer 相同。
1. Pick the path
1. 选择路径
| The request is about… | Do this |
|---|---|
A shadcn/ui component (the project has | A. Run the CLI |
| A component hand-built on Radix UI, Base UI or React Aria | B. Swap the import |
| Custom, plain markup, another library, or a primitive Animate UI doesn't cover | C. Animate it with the rules |
An Animate UI v1 project ( | |
| An app already on Animate UI: faster, slower, calmer | § Pace |
| Reviewing existing motion, finding slow or janky animations | |
| One rule to fix in existing motion (reduced motion, a loop) | C, step 8 and that rule only |
| An animation to reproduce: a link, a video, a GIF, a CSS or JS snippet | |
| A feel in words ("snappier", "plus doux", "bouncy") or CSS for a token | |
| An interaction by name: a loading button, a list, a swipe, a banner… | C, from |
| An icon to animate, from any library or SVG | |
Before writing motion by hand, check : entrances, scroll reveals, counters, text effects, buttons, backgrounds and icons already ship, and using one beats rebuilding it.
references/catalog.md| 请求涉及…… | 应执行的操作 |
|---|---|
shadcn/ui 组件(项目包含 | A. 运行 CLI |
| 直接基于 Radix UI、Base UI 或 React Aria 手工构建的组件 | B. 替换 import |
| 自定义组件、纯 markup、其他库,或 Animate UI 未覆盖的 primitive | C. 按规则添加动画 |
Animate UI v1 项目( | |
| 已经使用 Animate UI 的应用:更快、更慢或更平缓 | § Pace |
| 审查现有 motion、查找缓慢或卡顿的动画 | |
| 修复现有 motion 中的一条规则(reduced motion、loop) | C,仅执行第 8 步和对应规则 |
| 复现一个动画:链接、视频、GIF、CSS 或 JS snippet | |
| 用文字描述感觉(“snappier”“plus doux”“bouncy”)或为 token 指定 CSS | |
| 按名称指定交互:loading button、list、swipe、banner 等 | C,从 |
| 为任意库或 SVG 中的图标添加动画 | |
在手动编写 motion 前,先检查 :其中已经提供 entrances、scroll reveals、counters、text effects、buttons、backgrounds 和 icons,直接使用现成实现优于重新构建。
references/catalog.md2. Install
2. 安装
Paths A, B and C import and (C only when it writes Motion code; a CSS or Tailwind-only request adds no package); add them with the project's package manager, read from its lockfile: → , → , / → , otherwise . Run the CLI the same way (, , , ; the commands below use pnpm). The primitive library the components already use (, , ) stays installed: builds on it. Neither the CLI nor an import swap installs anything. warns when is neither in nor in ; warns when a package it needs () isn't in . Where installing isn't possible, add the entries to 's by hand and give the install command in the output (its other peer dependencies are optional): and (the version needs; an existing entry that allows 14 stays as it is, and included).
@animate-ui/reactmotionpnpm-lock.yamlpnpm addyarn.lockyarn addbun.lockbun.lockbbun addnpm installpnpm dlxyarn dlxbun xnpxradix-ui@base-ui/reactreact-aria-components@animate-ui/reactanimate@animate-ui/reactpackage.jsonnode_modulesmigrate@base-ui/reactpackage.jsonpackage.jsondependencies"@animate-ui/react": "^2.0.0""motion": "^14.0.0"@animate-ui/reactmotion"latest""*"Tailwind's version matters in two places (centering classes beside Motion's , and whether is gated to fine pointers): 4 in or in the CSS is v4; a with 3 is v3; class syntax only v4 has (bare variants, values like ) means v4 too. When nothing tells, take the form that works on both: margins for centering (, a wrapper that already centers stays), for hover, and a hover border with an explicit color (a bare is gray in v3, the text color in v4).
transformhover:tailwindcsspackage.json@import "tailwindcss"tailwind.config.*tailwindcssdata-open:(--var)h-(--x)inset-0 m-auto h-fit[@media(hover:hover)_and_(pointer:fine)]:hover:border路径 A、B 和 C 都会用到 和 (仅当 C 需要编写 Motion 代码时才需要 ;如果请求只涉及 CSS 或 Tailwind,则无需添加包)。使用项目的 package manager 添加这些包,并根据 lockfile 判断命令: → , → ,/ → ,否则使用 。运行 CLI 时采用相同方式(、、、;以下命令使用 pnpm)。组件当前使用的 primitive library(、、)需要保持安装,因为 建立在它们之上。CLI 和 import 替换本身都不会安装任何依赖。 会在 既不在 中、也不在 中时发出警告; 会在所需包(如 )不在 中时发出警告。如果无法安装,请手动将以下条目添加到 的 ,并在输出中提供安装命令(其他 peer dependencies 均为可选): 和 (这是 所需的版本;如果已有的 条目允许使用 14,则保持原样,包括 和 )。
@animate-ui/reactmotionmotionpnpm-lock.yamlpnpm addyarn.lockyarn addbun.lockbun.lockbbun addnpm installpnpm dlxyarn dlxbun xnpxradix-ui@base-ui/reactreact-aria-components@animate-ui/reactanimate@animate-ui/reactpackage.jsonnode_modulesmigrate@base-ui/reactpackage.jsonpackage.jsondependencies"@animate-ui/react": "^2.0.0""motion": "^14.0.0"@animate-ui/reactmotion"latest""*"Tailwind 的版本会在两个地方产生影响:Motion 的 旁边的居中 classes,以及 是否只对精细指针生效。如果 中有 4,或 CSS 中包含 ,则视为 v4;如果存在带 3 的 ,则视为 v3;只有 v4 才有的 class 语法(裸 variants、 值,如 )同样表示 v4。如果无法确定,请采用同时兼容两种版本的形式:使用 margins 实现居中(,已有居中 wrapper 的组件保持不变),使用 实现 hover,并为 hover border 指定明确颜色(裸 在 v3 中是灰色,在 v4 中是文字颜色)。
transformhover:package.jsontailwindcss@import "tailwindcss"tailwindcsstailwind.config.*data-open:(--var)h-(--x)inset-0 m-auto h-fit[@media(hover:hover)_and_(pointer:fine)]:hover:border3. Gate: how much motion
3. Gate:决定 motion 的幅度
The gate (§ Gate, under C) decides how much a component moves, and when to stop:
- An explicit request is always honored. Asked to animate something, animate it: the gate only picks the form, the shortest one when the component is used all day (a command palette on ⌘K gets a quick fade and slight scale, never a wait), and the output says why in one line. Never answer an explicit request with no motion.
- An open request ("make the page feel alive", "what do you think of the motion") may end with less motion, or none where motion would only add a wait. "Review it, then fix it" on something that already moves is explicit: keep it moving, in its right form.
- Ask the level when it isn't given. Never ask on A, B, a migration, § Pace or a review's fixes: the swap is the whole answer (even when the request names enter and exit), and a review's fixes keep the element's motion in its right form. A level or feel word anywhere in the request answers it, even beside praise or a named interaction. "Cycle", "fade" or "drag to close" names the effect: subtle, with that effect leading. Otherwise a request to animate something that names no level, feel or reference ("anime ma liste", "add an animation to this button") gets one question before any code: which level, subtle, more or expressive (, § Levels), each with what it would look like on this component in a few words, the one the gate suggests marked as recommended. A link, a video or "like an Animate UI sheet" answers it too. When nobody can answer (a non-interactive run), take subtle and say so in the output. A feel word ("vivant", "calm", "snappy") sets the feel (
references/feel.md) at the subtle level, unless the word is about how much ("plus animé", "bouncy", "expressive"). The recipes and the catalog's defaults are written at the subtle level. "Nice", "nicely", "cool" alone are praise, not a feel, and an interaction named alone (a swipe, a reorder, a skeleton) names no level: ask.feel.md - On A the request names the components ("all" runs all of them, leaves some out); on B, the one asked. Neither shortens the motion: the primitives bring their own tokens. Migrating (
--exclude) and § Pace skip it.references/migrate.md
Gate(C 中的 § Gate)用于决定组件应移动多少,以及何时停止:
- 始终执行明确请求。 用户要求为某项内容添加动画时,就应当添加动画:Gate 只会选择形式。如果组件全天都在使用,则选择最短的形式(例如通过 ⌘K 打开的 command palette 只做快速 fade 和轻微 scale,绝不能让用户等待),并在输出中用一行说明原因。绝不能以完全没有 motion 来回应明确请求。
- 开放式请求(“让页面更有活力”“你觉得这些 motion 怎么样”)可以少加 motion;如果添加 motion 只会增加等待感,也可以不加。对于已有 motion 的内容,“审查后修复”属于明确请求:保留它的 motion,并采用正确的形式。
- 未指定级别时应询问。 在 A、B、迁移、§ Pace 或 review 的修复场景中绝不询问:替换本身就是完整答案(即使请求明确提到 enter 和 exit),review 的修复应让元素以正确形式保留原有 motion。请求任何位置出现级别词或感觉词,都已经回答了这个问题,即使它与赞美或指定交互同时出现。“Cycle”“fade”或“drag to close”指明的是效果:采用 subtle,并让该效果成为主要表现。如果请求没有指定级别、感觉或参考(“anime ma liste”“add an animation to this button”),则在编写任何代码前只问一个问题:选择哪个级别——subtle、more 还是 expressive(,§ Levels)?分别用几句话说明每个级别在该组件上的效果,并将 Gate 建议的选项标为 recommended。提供链接、视频或“像 Animate UI sheet 一样”也算作回答。如果无法回答(例如非交互式运行),则采用 subtle,并在输出中说明。感觉词(“vivant”“calm”“snappy”)会设定感觉(
references/feel.md),默认采用 subtle 级别;但如果该词表达的是 motion 的量(“plus anim锓bouncy”“expressive”),则不采用此默认值。Recipes 和 catalog 的默认值均以 subtle 级别编写。“Nice”“nicely”“cool”单独出现只是赞美,不算感觉词;仅指定交互名称(swipe、reorder、skeleton)也不代表级别,此时应询问。feel.md - 在 A 中,请求需要指定组件(“all”表示运行全部组件,用于排除部分组件);在 B 中,则处理指定的那个组件。两者都不会缩短 motion:primitives 自带相应的 tokens。迁移(
--exclude)和 § Pace 不需要询问级别。references/migrate.md
A. shadcn/ui components: run the CLI
A. shadcn/ui 组件:运行 CLI
bash
pnpm add @animate-ui/react motion
pnpm dlx @animate-ui/cli@latest animate <component> --dry-run # preview
pnpm dlx @animate-ui/cli@latest animate <component> # or: animate all<component>dropdown-menu.tsxall --exclude sidebar,calendarcomponents.jsonradix-ui@radix-ui/react-*@base-ui/react[/…]react-aria-componentsvaulinput-otp@shadcn/react/questionnaire@animate-ui/reactdata-starting-styledata-ending-styledata-enteringdata-exitingh-(--radix-accordion-content-height)@animate-ui/reactOn Tailwind 3 it also recenters modals with margins () instead of the translate classes Motion's transform would cancel; it reads the version from , , a config file, or the file's own class syntax. What it leaves on purpose: , , and the like (Motion animates through inline styles, so neither can fight it, and the origin variables are what Motion grows from), and plain Tailwind transitions such as or a trigger's (color and hover feedback, not the enter or exit Motion now plays; the moving indicators copy and hide the selected look themselves).
inset-0 m-auto h-fitpackage.jsoncomponents.jsonanimate-noneanimate-none!origin-(--transform-origin)transition-colorstransition-allbash
pnpm add @animate-ui/react motion
pnpm dlx @animate-ui/cli@latest animate <component> --dry-run # preview
pnpm dlx @animate-ui/cli@latest animate <component> # or: animate all<component>dropdown-menu.tsxall --exclude sidebar,calendarcomponents.jsonradix-ui@radix-ui/react-*@base-ui/react[/…]react-aria-componentsvaulinput-otp@shadcn/react/questionnaire@animate-ui/reactdata-starting-styledata-ending-styledata-enteringdata-exitingh-(--radix-accordion-content-height)@animate-ui/react在 Tailwind 3 中,它还会使用 margins()重新居中 modals,而不是使用会被 Motion 的 transform 抵消的 translate classes;它会从 、、配置文件或文件自身的 class 语法中读取版本。它有意保留以下内容:、、 等(Motion 通过 inline styles 执行动画,因此这些样式不会与 Motion 冲突,而 origin variables 正是 Motion 放大时的起点),以及普通 Tailwind transitions,例如 或 trigger 的 (它们负责颜色和 hover feedback,而不是由 Motion 播放的 enter 或 exit;移动指示器会自行复制并隐藏选中态样式)。
inset-0 m-auto h-fitpackage.jsoncomponents.jsonanimate-noneanimate-none!origin-(--transform-origin)transition-colorstransition-allB. Built on Radix UI, Base UI or React Aria: swap the import
B. 基于 Radix UI、Base UI 或 React Aria 构建:替换 import
Add the packages (§ Install), then swap:
tsx
import { Dialog, Slot } from 'radix-ui'; // before
import { Dialog, Slot } from '@animate-ui/react/primitives/radix-ui'; // after (re-exports all of radix-ui)
import * as DialogPrimitive from '@radix-ui/react-dialog'; // before
import * as DialogPrimitive from '@animate-ui/react/primitives/radix-ui/dialog'; // after
import { Dialog } from '@base-ui/react/dialog'; // before
import { Dialog } from '@animate-ui/react/primitives/base-ui/dialog'; // after
import { Dialog, Field } from '@base-ui/react'; // before
import { Dialog, Field } from '@animate-ui/react/primitives/base-ui'; // after (re-exports all of Base UI)
import { Popover, Button } from 'react-aria-components'; // before
import { Popover, Button } from '@animate-ui/react/primitives/react-aria'; // after (re-exports all of React Aria)Mirror the library's import style: an umbrella import (, , ) keeps its specifiers and only changes source; a per-package or subpath import (, , → , , → ) uses the per-primitive path. The whole import moves, even when only one part was asked for: parts Animate UI doesn't animate are re-exported unchanged. A Base UI subpath Animate UI doesn't animate () stays on Base UI.
radix-ui@base-ui/reactreact-aria-components@radix-ui/react-*@base-ui/react/*vaulradix-ui/drawerinput-otp@shadcn/react/questionnaire@animate-ui/react/primitives/questionnaire@base-ui/react/fieldThen remove the component's own CSS animation classes so they don't run on top of Motion: tw-animate utilities (, , , , , , and the beside them), Base UI's / transitions, React Aria's / (and /), and the height keyframes (/, ; Base UI's too, while Radix's can stay: the animated content sets that variable to ). Everything else stays: parts, props, data attributes, /, render props, positioning classes, and the items' own highlighted or focus background (, ): the gliding highlight takes it. A modal centered with keeps them under Tailwind v4 (the property composes with Motion's ); under Tailwind v3, or when the version can't be told (§ Install), they'd fight it, so center it with margins instead of a transform: replace with (its width classes stay). Animated parts that enter and leave (contents, overlays, indicators) accept two optional props, and (Motion s); parts that only move (a switch thumb, a slider, a progress bar) accept . The exceptions are Animate UI's own drop-ins (, ): they take exactly their package's props, neither of those ('s , and for the questionnaire's checks, still apply). On a , drop shadcn/ui's Animated Items example's tw-animate classes (, , , ): they'd run on top of the step's entrance.
animate-inanimate-outfade-*zoom-*slide-in-from-*slide-out-to-*duration-*data-starting-style:*data-ending-style:*data-entering:*data-exiting:*entering:exiting:animate-accordion-downupanimate-collapsible-*h-(--accordion-panel-height)h-(--radix-accordion-content-height)autoforceMountkeepMounteddata-highlighted:bg-*focus:bg-accent-translate-x-1/2 -translate-y-1/2translatetransformtop-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2inset-0 m-auto h-fittransitionexitTransitionTransitiontransitioninput-otp@shadcn/react/questionnaireAnimateUIProvidertempocheckAnimationQuestionnaireItemdata-active:animate-indata-active:fade-in-0data-active:slide-in-from-bottom-2data-active:duration-300添加依赖包(§ 安装),然后替换:
tsx
import { Dialog, Slot } from 'radix-ui'; // before
import { Dialog, Slot } from '@animate-ui/react/primitives/radix-ui'; // after (re-exports all of radix-ui)
import * as DialogPrimitive from '@radix-ui/react-dialog'; // before
import * as DialogPrimitive from '@animate-ui/react/primitives/radix-ui/dialog'; // after
import { Dialog } from '@base-ui/react/dialog'; // before
import { Dialog } from '@animate-ui/react/primitives/base-ui/dialog'; // after
import { Dialog, Field } from '@base-ui/react'; // before
import { Dialog, Field } from '@animate-ui/react/primitives/base-ui'; // after (re-exports all of Base UI)
import { Popover, Button } from 'react-aria-components'; // before
import { Popover, Button } from '@animate-ui/react/primitives/react-aria'; // after (re-exports all of React Aria)沿用原库的 import 风格:umbrella import(、、)保留其 specifiers,只修改来源;按包或子路径导入(、、 → 、、 → )则使用对应的 per-primitive path。即使只要求处理其中一部分,整个 import 也会一起迁移:Animate UI 不提供动画的部分会原样重新导出。Animate UI 不提供动画的 Base UI 子路径()仍保留在 Base UI。
radix-ui@base-ui/reactreact-aria-components@radix-ui/react-*@base-ui/react/*vaulradix-ui/drawerinput-otp@shadcn/react/questionnaire@animate-ui/react/primitives/questionnaire@base-ui/react/field然后移除组件自身的 CSS 动画 classes,避免它们叠加在 Motion 上:tw-animate utilities(、、、、、,以及与其相邻的 )、Base UI 的 / transitions、React Aria 的 /(以及 /),以及高度 keyframes(/、;Base UI 的 也需移除,而 Radix 的 可以保留,因为动画内容会将该变量设置为 )。其他内容全部保留:parts、props、data attributes、/、render props、positioning classes,以及条目自身的 highlighted 或 focus background(、),这些样式由滑动的 highlight 接管。使用 居中的 modal 在 Tailwind v4 下可以保留这些 classes( property 会与 Motion 的 组合);在 Tailwind v3 下,或无法判断版本时(§ 安装),二者会发生冲突,因此应改用 margins 居中:将 替换为 ,宽度 classes 保持不变。会进入和离开的动画 parts(contents、overlays、indicators)支持两个可选 props: 和 (Motion s);只发生移动的 parts(switch thumb、slider、progress bar)支持 。例外是 Animate UI 自己的 drop-ins(、):它们只接受各自原有包的 props,不支持上述两个 props( 的 ,以及 questionnaire 的 checks 使用的 仍然有效)。对于 ,请移除 shadcn/ui 的 Animated Items 示例中的 tw-animate classes(、、、),否则它们会叠加在步骤入场动画上。
animate-inanimate-outfade-*zoom-*slide-in-from-*slide-out-to-*duration-*data-starting-style:*data-ending-style:*data-entering:*data-exiting:*entering:exiting:animate-accordion-downupanimate-collapsible-*h-(--accordion-panel-height)h-(--radix-accordion-content-height)autoforceMountkeepMounteddata-highlighted:bg-*focus:bg-accent-translate-x-1/2 -translate-y-1/2translatetransformtop-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2inset-0 m-auto h-fittransitionexitTransitionTransitiontransitioninput-otp@shadcn/react/questionnaireAnimateUIProvidertempocheckAnimationQuestionnaireItemdata-active:animate-indata-active:fade-in-0data-active:slide-in-from-bottom-2data-active:duration-300Pace
Pace
To make every animation faster or slower at once, wrap the app in (, a client component: in a Next.js root layout, put it inside around ) and set : durations and delays are multiplied by it, springs keep their shape on the new time scale, reduced-motion fades included. "A little" or no amount ("speed it up", "feels sluggish") is one step, slower or faster; "a lot" is or ; asked again, take one more step from the current value ( → ); stay within –. Without the provider the defaults apply ( 1). "Faster" or "sluggish" about the whole app is this; about one component, . It reaches every Animate UI primitive and any code that reads the tokens through or ; the app's own motion with hand-typed values (Motion props, Tailwind , CSS transitions) doesn't follow it: name those files in the output, and offer to move them onto the tokens.
AnimateUIProvider@animate-ui/react/provider<body>{children}tempo1.150.851.30.71.151.30.52tempofeel.mduseTempoTransition()useMotionTokens()duration-*如需统一加快或减慢所有动画,请使用 (,一个 client component:在 Next.js root layout 中,将它放在 内并包裹 )并设置 :durations 和 delays 会乘以该值,springs 在新的时间尺度下保持原有形态,reduced-motion fades 也包含在内。“稍微”或未指定幅度(“speed it up”“feels sluggish”)时只调整一档, 表示更慢, 表示更快;幅度很大时使用 或 ;再次提出要求时,在当前值基础上再调整一档( → );始终保持在 – 范围内。如果没有 provider,则使用默认值( 1)。如果“faster”或“sluggish”针对整个应用,应使用此方案;如果只针对某个组件,则使用 。它会影响每个 Animate UI primitive,以及所有通过 或 读取 tokens 的代码;应用中手动指定值的 motion(Motion props、Tailwind 、CSS transitions)不会跟随它:应在输出中列出相关文件,并提议将它们迁移到 tokens。
AnimateUIProvider@animate-ui/react/provider<body>{children}tempo1.150.851.30.71.151.30.52tempofeel.mduseTempoTransition()useMotionTokens()duration-*C. Anything else: animate it with the rules
C. 其他内容:按规则添加动画
- Know the technique. Motion's documentation and examples (motion.dev) show how most interactions are built; when a tool can read them, learn the technique there, then write it with Animate UI's tokens and rules. Present the result as the user's animation, not as someone's example.
- Catalog first: if has it (a counter, an entrance, a scroll reveal, a text effect, a button effect), use that. Whenever an Animate UI component does the job (a word cycling, tabs, a copy button, a pressed button, a counter), use it rather than writing the motion by hand, and use it whole, with its animated parts: Tabs take
references/catalog.mdfor the list andTabsHighlightwith aTabsContentsper value for what changes under it, never a list over a panel swapped by hand. When its layout gets in the way (Rotating Text is a block), change the layout or the props around it (TabsContent, analignstyle) instead of replacing it.inline-block - Gate (below), then classify the component and start from the matching recipe: (the table below) for overlays, menus, controls, cards, toasts and pages;
references/recipes/<kind>.mdfor interactions asked by name (a status button, a list that adds and removes, a drag to reorder, a card opening into a view, a skeleton, a counting badge, a field in error, a swipe to delete, a filtered grid, form steps, a banner).references/interactions/ - If it renders a Radix UI, Base UI or React Aria overlay, prefer composing Animate UI's primitive over re-wiring presence yourself. A hand-rolled overlay stays hand-rolled unless the user asks: animating it doesn't mean moving it onto a library (that would change its focus, modality and dismissal), so wire Motion into it as it is, the way shows for plain markup, and mention in the output what a primitive would add. A drag or swipe asked on a hand-built sheet counts as asking: move it onto
references/base-libraries.md(@animate-ui/react/primitives/radix-ui/drawer:import { Drawer }withDrawer.Root,direction,Drawer.Portal,Drawer.Overlay,Drawer.Content,Drawer.Handle,Drawer.Title), addDrawer.Descriptionwhen it's missing, and say in the output what changes (focus is trapped inside, Escape closes it, the page stops scrolling behind it).radix-ui - Motion runs in client components: put on the file that writes Motion code, and keep a server page server-side by moving the animated part into its own client component. Content that enters on mount starts hidden in the server HTML; keep that for what is below the fold or decorative, never for the page's main text on first paint. When an explicit request animates content likely in the first viewport, do it and say in the output that it's hidden until hydration. Markup that differs under reduced motion or at a screen size reads them with hooks that give the server's value while hydrating (
'use client', auseShouldReduceMotion()with a server snapshot), never Motion'suseSyncExternalStoreoruseReducedMotion()during render: otherwise React throws the server HTML away and renders the page again.window.matchMedia - When the user describes a feel ("snappy", "calme", "more bounce"), read it with before picking values. For CSS without Motion (a stylesheet, Tailwind, a server component), print a token's timing with
references/feel.md(the script ships with this skill) from the project's folder, so it loads the project'snode <skill>/scripts/css-timing.mjs <token>; without the package, write the tween tokens' values from@animate-ui/reactastokens.md.cubic-bezier - Take every value from the tokens ():
references/tokens.md. Pass them throughimport { spring, transition, scale, blur, shift, ease, useTempoTransition, ReducedMotionConfig, useShouldReduceMotion } from '@animate-ui/react/utils/motion'(or read them fromuseTempoTransition()) souseMotionTokens()'sAnimateUIProviderapplies. No hand-typed spring for a role a token covers (a recipe's own distances and scales stay as written), no ease-in (but a dismissed dialog'stempo, and a menubar's next content fading in ontransition.dismiss), no ease-in-out on an entrance. Animated icons are the exception: their values live in the icon file (transition.handoffIn).references/icons.md - Check every rule in (quality rules are non-negotiable; style rules are defaults), then verify (§ Verify).
references/rules.md
- 了解 technique。 Motion 的文档和示例(motion.dev)展示了大多数交互的构建方式;当工具能够读取这些内容时,应先在那里学习 technique,再使用 Animate UI 的 tokens 和规则编写实现。最终结果应呈现为用户自己的动画,而不是直接照搬他人的示例。
- 优先使用 catalog。 如果 中已有对应实现(counter、entrance、scroll reveal、text effect、button effect),应直接使用。只要 Animate UI 组件能够完成工作(word cycling、tabs、copy button、pressed button、counter),就应使用该组件,而不是手动编写 motion,并完整使用它及其动画 parts:Tabs 应为列表使用
references/catalog.md,为切换后的内容使用TabsHighlight,并为每个值使用一个TabsContents,不要手动用列表替换 panel。如果它的布局妨碍使用(Rotating Text 是 block),应调整组件周围的布局或 props(TabsContent、inline-block style),而不是替换它。align - 执行 Gate(见下文),然后判断组件类型,并从匹配的 recipe 开始:(见下表),用于 overlays、menus、controls、cards、toasts 和 pages;对于按名称指定的交互,使用
references/recipes/<kind>.md,例如 status button、可添加和删除条目的 list、拖拽排序、展开为视图的 card、skeleton、counting badge、error field、swipe to delete、filtered grid、form steps 或 banner。references/interactions/ - 如果组件渲染 Radix UI、Base UI 或 React Aria overlay,优先组合 Animate UI 的 primitive,而不是自行重新实现 presence 机制。手工实现的 overlay 仍应保持手工实现,除非用户明确要求:为它添加动画并不意味着将其迁移到某个 library,因为这会改变焦点管理、模态行为和关闭机制;应按照 为纯 markup 所展示的方式,将 Motion 接入现有实现,并在输出中说明使用 primitive 会增加哪些能力。如果用户要求为手工构建的 sheet 添加 drag 或 swipe,就视为明确要求迁移:改用
references/base-libraries.md(@animate-ui/react/primitives/radix-ui/drawer:import { Drawer }搭配Drawer.Root、direction、Drawer.Portal、Drawer.Overlay、Drawer.Content、Drawer.Handle、Drawer.Title),缺少Drawer.Description时将其添加为依赖,并在输出中说明变化(焦点会被限制在内部、Escape 可以关闭、页面不会在后方滚动)。radix-ui - Motion 在 client components 中运行:在编写 Motion 代码的文件中添加 ,并通过将动画部分拆分到独立的 client component 中,让 server page 保持 server-side。挂载时进入的内容在 server HTML 中会先处于隐藏状态;仅对首屏以下或装饰性内容使用这种方式,不要用于首次绘制时页面中的主要文字。如果明确请求为可能出现在首屏中的内容添加动画,则应执行,并在输出中说明它会隐藏至 hydration 完成。对于 reduced motion 或不同屏幕尺寸下 markup 会发生变化的内容,应使用能够在水合期间提供 server 值的 hooks(
'use client',或带 server snapshot 的useShouldReduceMotion())读取状态;绝不要在 render 期间使用 Motion 的useSyncExternalStore或useReducedMotion(),否则 React 会丢弃 server HTML 并重新渲染页面。window.matchMedia - 当用户描述一种感觉(“snappy”“calme”“more bounce”)时,应先阅读 ,再选择数值。对于不使用 Motion 的 CSS(stylesheet、Tailwind、server component),从项目目录运行
references/feel.md(该脚本随此 skill 提供),以加载项目的node <skill>/scripts/css-timing.mjs <token>;如果项目中没有该包,则根据@animate-ui/react写出 tween tokens 的tokens.md值。cubic-bezier - 所有数值都必须来自 tokens():
references/tokens.md。通过import { spring, transition, scale, blur, shift, ease, useTempoTransition, ReducedMotionConfig, useShouldReduceMotion } from '@animate-ui/react/utils/motion'传入这些值,或通过useTempoTransition()读取它们,使useMotionTokens()的AnimateUIProvider能够生效。对于 token 已覆盖的角色,不得手动编写 spring(recipe 自身规定的 distances 和 scales 保持原样),不得使用 ease-in(但被关闭的 dialog 的tempo,以及 menubar 下一项内容淡入时使用的transition.dismiss除外),进入动画不得使用 ease-in-out。动画图标是例外:其数值保存在图标文件中(transition.handoffIn)。references/icons.md - 检查 中的每条规则(质量规则不可妥协,样式规则作为默认值),然后执行验证(§ Verify)。
references/rules.md
Gate: should it animate?
Gate:是否应该添加动画?
| How often it's used | Decision |
|---|---|
| Constantly (press, tooltips, menus, tabs, toggles, hover highlights) | Animate, quickly: |
| Occasionally (popovers, selects, dialogs, sheets, toasts, disclosures) | The role's token: surfaces may take their time ( |
| Rarely (effects, text, numbers, icons, onboarding, success) | Their own motion: own springs, more bounce, longer rhythms (icons: |
| Toggled from the keyboard all day (command palette, global shortcuts) | The shortest form: a quick fade and a slight scale from |
Keyboard navigation animates like the pointer. Name the purpose in one word: feedback (it answers an action), spatial (where it comes from or goes), state (what changed), continuity (it's the same thing, moved), delight (a rare moment worth a flourish: a page's entrance, a success). On an open request, if no word fits, don't animate.
| 使用频率 | 决策 |
|---|---|
| 持续使用(press、tooltips、menus、tabs、toggles、hover highlights) | 快速添加动画: |
| 偶尔使用(popovers、selects、dialogs、sheets、toasts、disclosures) | 使用该角色的 token:surfaces 可以稍慢(进入时使用 |
| 很少使用(effects、text、numbers、icons、onboarding、success) | 使用专属 motion:独立 springs、更明显的 bounce、更长的节奏(图标参见 |
| 全天可通过键盘切换(command palette、global shortcuts) | 使用最短形式:在 |
键盘导航的动画效果应与 pointer 相同。用一个词说明目的:feedback(回应一次操作)、spatial(说明对象来自哪里或去向何方)、state(说明什么发生了变化)、continuity(同一个对象发生了位移)、delight(偶尔值得炫技的时刻,例如页面入场或成功反馈)。对于开放式请求,如果找不到合适的词,就不要添加动画。
Kinds of component
组件类型
Recipes and the gate table are for C; on B the primitive brings its own motion.
| Kind | Examples | Recipe |
|---|---|---|
| Anchored overlay | Popover, Tooltip, Hover Card, Preview Card | |
| Anchored list | Dropdown Menu, Context Menu, Menubar, Select, Combobox, Navigation Menu | |
| Centered overlay | Dialog, Alert Dialog | |
| Edge overlay | Sheet, Drawer | |
| Disclosure | Accordion, Collapsible | |
| Moving indicator | Tabs, Toggle Group, Pagination, Navigation Menu indicator | |
| Steps of a flow | Questionnaire | |
| Pressable | Button, Toggle, Checkbox, Switch, Radio | |
| Pressable surface | Clickable card, tile, list row | |
| Centered list opened by a shortcut | Command palette, spotlight search | |
| Messages stacking at an edge, hand-built | Toasts, snackbars, notifications fixed to a screen edge | |
| Page content | Hero, sections, feature grids, stats | |
| Two states swapping in place | Copy → copied, sun → moon, a changing label (rules B9) | |
| Gesture or stack the library drives with CSS variables (Base UI Drawer, Toast) | |
Recipes 和 Gate 表格适用于 C;在 B 中,primitive 自带相应的 motion。
| 组件类型 | 示例 | Recipe |
|---|---|---|
| 锚定 overlay | Popover、Tooltip、Hover Card、Preview Card | |
| 锚定列表 | Dropdown Menu、Context Menu、Menubar、Select、Combobox、Navigation Menu | |
| 居中 overlay | Dialog、Alert Dialog | |
| 边缘 overlay | Sheet、Drawer | |
| 展开或折叠内容 | Accordion、Collapsible | |
| 移动指示器 | Tabs、Toggle Group、Pagination、Navigation Menu indicator | |
| 流程步骤 | Questionnaire | |
| 可按压组件 | Button、Toggle、Checkbox、Switch、Radio | |
| 可按压表面 | Clickable card、tile、list row | |
| 通过快捷键打开的居中列表 | Command palette、spotlight search | |
| 手工构建、在边缘堆叠的消息 | Toasts、snackbars、固定在屏幕边缘的 notifications | |
| 页面内容 | Hero、sections、feature grids、stats | |
| 原位切换的两种状态 | Copy → copied、sun → moon、变化的 label(规则 B9) | |
| 由 library 通过 CSS variables 驱动的手势或堆叠(Base UI Drawer、Toast) | |
The rules in short
规则摘要
- Reduced motion ships with the animation: around the motion tree,
ReducedMotionConfigto neutralizeuseShouldReduceMotion()strings,transformfor animations that play on mount. Fades and blurs stay, movement goes.<ReducedMotionConfig remount> - Transform, opacity and filter (height only where content must push what's below). Anchored popups grow from their trigger (library origin variables), from 0.7 / 0.8 / 0.9 by kind; modals stay centered.
- Press scales to 0.97 on a spring (0.985 for a whole card or row); the pressed element never scales on hover. Hover changes color, background, border or shadow; inside a clipped card the image may zoom and the card may lift a few pixels.
- Tweens: ease-out for what enters and leaves, a softer ease-out for what the eye watches change (disclosures unfolding, overlays, menus opening, sheets sliding in), ease-in-out for what moves on screen, for color; never ease-in, but a closed drawer speeds off its edge on a short one (
ease), a dismissed dialog or sheet speeds away on a steady push (transition.drawerOut), and a menubar's next content fades in behind the one leaving on a slow start (transition.dismiss). A light blur bridges a swap.transition.handoffIn - Nothing stretches: a box resized with gets
layouton its children (or its height animated instead), so text, icons and images keep their proportions. A changing text fades, blurs or slides, never scales; Animate UI's texts (Rotating, Morphing, Rolling Text, Sliding Number) for the expressive level.layout - The exit replays the entrance (a dismissed dialog, drawer or sheet leaves at once, quicker, speeding away), its transition inside the target (an exiting element keeps its last props). Open from the start, it still animates.
exit - Keep the library element as the animated element (Radix , Base UI
asChild, React Ariarender/render) somotion.createattributes and styles still apply.data-* - Warm tooltips show at once (and the one they replace goes at once).
- A menubar's popover moves to the next menu and resizes (: it starts gently and eases in without going past, keeping its speed from menu to menu) instead of closing and reopening, its content changing in place: the content stays where it is on screen while the popover slides over it, the previous items leaving
spring.morph(6px) away from where it goes as they fade out (shift.handoff), the next ones arriving from there ontransition.handoffOutand fading in behind them (spring.handoff): from File to Edit, File's leave to the left, Edit's come from the right. One popover on screen at every frame, each menu's content once (a menu coming back takes its leaving items back from where they are), and the triggers share one highlight gliding from trigger to trigger (internal/menu-handoff.ts, internal/item-highlight.tsx).transition.handoffIn - The keyboard animates like the pointer; hover only on hover-capable fine pointers; re-triggering retargets, never restarts; loops pause off screen.
- Reduced motion 必须随动画一同提供:在 motion tree 外层使用 ,使用
ReducedMotionConfig中和useShouldReduceMotion()字符串,对于挂载时播放的动画使用transform。Fades 和 blurs 保留,movement 停止。<ReducedMotionConfig remount> - 优先使用 transform、opacity 和 filter;只有在内容必须推开下方元素时才动画 height。锚定 popups 从其 trigger 处扩展(使用 library 的 origin variables),根据类型从 0.7/0.8/0.9 开始;modals 保持居中。
- Press 使用 spring 缩放到 0.97(整张 card 或 row 使用 0.985);被按压的元素绝不在 hover 时缩放。Hover 只改变 color、background、border 或 shadow;在被裁剪的 card 内,image 可以缩放,card 可以上移几像素。
- Tweens:进入和离开时使用 ease-out;眼睛持续关注的变化使用更柔和的 ease-out(disclosures 展开、overlays、menus 打开、sheets 滑入);屏幕上移动的元素使用 ease-in-out;color 使用 ;绝不使用 ease-in,但关闭的 drawer 会沿短促路径加速离开边缘(
ease),被关闭的 dialog 或 sheet 会以稳定推力快速离开(transition.drawerOut),menubar 的下一项内容会在离开项后方以较慢的起始过程淡入(transition.dismiss)。轻微 blur 可用于衔接替换。transition.handoffIn - 禁止拉伸:使用 调整大小的 box,其 children 也应使用
layout(或改为动画 height),确保 text、icons 和 images 保持比例。变化的 text 应 fade、blur 或 slide,绝不 scale;expressive 级别使用 Animate UI 的 text 组件(Rotating、Morphing、Rolling Text、Sliding Number)。layout - Exit 重现 Enter 的过程(被关闭的 dialog、drawer 或 sheet 会立即离开,且速度更快、加速离场),其 transition 应放在 target 内部(正在退出的元素保留最后一组 props)。即使元素从一开始就可见,也仍然播放动画。
exit - 保持 library element 作为被动画化的元素(Radix 、Base UI
asChild、React Ariarender/render),确保motion.createattributes 和 styles 仍然生效。data-* - Warm tooltips 应立即显示,被其替换的 tooltip 也应立即消失。
- Menubar 的 popover 会移动到下一个菜单并调整大小(:它会柔和开始并平滑进入,不会越过目标,在菜单之间保持速度),而不是关闭后重新打开;内容在原位变化:popover 在屏幕上滑动时,内容保持在原位置,之前的 items 使用
spring.morph(6px)离开目标方向并淡出(shift.handoff),接下来的 items 从同一位置通过transition.handoffOut到达并在后方淡入(spring.handoff):从 File 切换到 Edit 时,File 向左离开,Edit 从右侧进入。每一帧屏幕上只能有一个 popover,每个菜单的内容只出现一次(菜单返回时,离开的 items 会从原位置返回),triggers 共享一个在 trigger 之间滑动的 highlight(internal/menu-handoff.ts、internal/item-highlight.tsx)。transition.handoffIn - 键盘动画效果应与 pointer 相同;hover 仅适用于支持 hover 的精细指针;重复触发时应重新定位目标,而不是重新开始;离开屏幕的 loop 应暂停。
Verify
验证
With a browser, look at frames in the middle of each animation, not only at rest: nothing passes over a label it shouldn't, the words around a change don't move, text keeps its baseline, and every part that changes state animates (a chevron that flips at once is a miss). An element handed from one layer to another (a shared gliding out of an intro into the header) stays above anything still fading on top of it: raise its new layer over the fading one, or it shows through the veil (a white logo turning grey mid-glide).
layoutId- Run the type-check before touching anything, even for a one-line swap: its output is the before.
- Every quality item of the checklist in passes; style deviations are named.
references/rules.md - Reduced motion (OS setting or Playwright ): things still appear and disappear, nothing moves.
emulateMedia({ reducedMotion: 'reduce' }) - Trigger twice quickly: it retargets. The keyboard path animates like the pointer one. Positioning and still work.
className - Slow it down (DevTools Animations at 10%) to check the origin, the overshoot and the curve.
Without a browser (an agent's shell, CI), check what can be read. On A and B: the type-check passes with no new error (compare its output before and after), and the diff only swapped imports, removed animation classes, replaced a modal's translate centering where § B asks for it, and added the package entries. The primitives bring their own enter and exit, overlays included. On C: the type-check passes with no new error; every value comes from a token or the recipe; every element that moves has a reduced-motion path ( around it, transform strings neutralized); every has its transition inside it and an above it; nothing animates , (outside disclosures), , or ; and say in the output that it wasn't run in a browser. For CSS without Motion, the checks at the end of .
ReducedMotionConfigexitAnimatePresencewidthheighttopleftmarginfeel.md使用 browser 时,应查看每个动画中间阶段的 frames,而不只是静止状态:不应有任何内容遮挡不该遮挡的标签,变化周围的文字不应移动,text 应保持基线,每个状态发生变化的 part 都应有动画(立即翻转的 chevron 属于遗漏)。当元素从一个 layer 交接到另一个 layer 时(例如共享的 从 intro 滑入 header),它应始终位于仍在淡出的内容之上:应提高新 layer 的层级,否则元素会透过遮罩显示出来,例如白色 logo 在滑动中途变成灰色。
layoutId- 在修改任何内容之前先运行 type-check,即使只改动一行 import:其输出就是修改前的基线。
- 检查清单中的每个质量项都必须通过;样式偏差需要说明。
references/rules.md - Reduced motion(OS 设置或 Playwright ):内容仍会正常出现和消失,但不应发生移动。
emulateMedia({ reducedMotion: 'reduce' }) - 快速触发两次:动画应重新定位到新目标。键盘路径的动画效果应与 pointer 路径一致。Positioning 和 仍应正常工作。
className - 将速度降至 10%(DevTools Animations),检查 origin、overshoot 和 curve。
如果没有 browser(例如 Agent 的 shell 或 CI 环境),则检查所有能够读取的内容。对于 A 和 B:type-check 不应产生新的错误(比较修改前后的输出),diff 中只应包含 import 替换、动画 classes 的移除、§ B 要求的 modal translate 居中替换,以及 package 条目新增。Primitives 自带 enter 和 exit,包括 overlays。对于 C:type-check 不应产生新的错误;每个数值都来自 token 或 recipe;每个发生移动的元素都有 reduced-motion 路径(外层使用 ,并中和 transform 字符串);每个 的 transition 都位于其内部,并且上方有 ;不得动画 、(disclosures 除外)、、 或 ;并在输出中说明未在 browser 中运行。对于不使用 Motion 的 CSS,执行 末尾的检查。
ReducedMotionConfigexitAnimatePresencewidthheighttopleftmarginfeel.mdOutput
输出
The change (CLI run, import swap or code), then in a few lines:
- A, B: what the CLI or the swap changed, the packages to add, what was left on purpose, and the type-check before and after. Nothing to remove is a complete answer.
- C: the level (asked, or subtle by default), the gate result (and why, when it shortened or skipped motion), the catalog item or recipe used, the tokens, and any style default changed and why.
- Feel: the word, the step or row taken, the token or spring written, and the role kept.
- Migrate: the manual steps done and left, and the files deleted or kept.
- Reproduce: what was read or measured, the tokens it became or the values kept, and what the reference didn't show.
- Icon: the file and where it went, what the icon does and why it means that, its trigger and why, the level, the usages replaced, the original files left, and any technique or rule of adapted or set aside.
icons.md - Review: the findings (file:line, why, fix), the facts dismissed and why, and the audit's before/after when fixes were made.
- Pace: the tempo set and where.
先展示改动(CLI 运行结果、import 替换或代码),然后用几行说明:
- A、B:CLI 或替换修改了什么、需要添加哪些 packages、哪些内容被有意保留,以及修改前后的 type-check 结果。无需移除任何内容也是完整答案。
- C:使用的级别(用户指定,或默认采用 subtle)、Gate 结果(如果缩短或跳过了 motion,说明原因)、使用的 catalog item 或 recipe、tokens,以及修改了哪些样式默认值及其原因。
- Feel:感觉词、采用的档位或行、写入的 token 或 spring,以及保留的角色。
- Migrate:已完成和保留的手动步骤,以及删除或保留的文件。
- Reproduce:读取或测量了什么、转换成了哪些 tokens 或保留了哪些数值,以及参考内容未展示什么。
- Icon:文件及其目标位置、图标执行什么操作及其含义、触发方式及原因、级别、被替换的 usages、保留的原始文件,以及对 中哪些 technique 或规则进行了调整或搁置。
icons.md - Review:发现的问题(file:line、原因、修复方式)、排除的问题及原因,以及完成修复时 audit 的前后对比。
- Pace:设置的 tempo 及其位置。