mcpField
A
animate-ui

animate-ui

github.com/prototyperai设计与 UI781 次安装23 周前更新
代码仓库

关于 animate-ui

为 Prototyper UI 组件提供动画规范的设计技能,面向实现和评审界面的开发者。它汇总可复制的 Tailwind 类、时长与缓动规则,并涵盖无障碍和性能约束。

主要功能

  1. 01提供常见组件动画模式
  2. 02定义缓动曲线与时长等级
  3. 03规范覆盖层定位与缩放
  4. 04限制可动画属性与过渡写法
  5. 05提供减少动态效果的无障碍规则

适用场景

  1. 01为弹窗和菜单添加进出场动画
  2. 02统一按钮及控件的按压反馈
  3. 03检查动画性能与可中断性
  4. 04为动态效果补充减少动态设置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/prototyperai/prototyper-ui --skill animate-ui

安装后在 Claude Code 中输入/animate-ui确认已加载。

详细介绍

Prototyper UI — 动画约定

Prototyper UI 组件的所有动画决策。先介绍模式,再列出速查表,接着说明规则,最后介绍各组件的细节。


1. 复制粘贴模式

以下五种模式涵盖了约 95% 的动画工作。请原样复制类字符串。

模式 A:定位浮层

适用于:Popover、Menu、Tooltip、Select、Combobox、Context Menu、HoverCard、Menubar。

className={cn(
  // Enter
  "data-open:animate-in data-open:duration-150",
  "data-open:[animation-timing-function:var(--ease-out-fluid)]",
  "data-open:fade-in-0 data-open:zoom-in-95",
  // Exit
  "data-closed:animate-out data-closed:duration-100",
  "data-closed:[animation-timing-function:var(--ease-in-quart)]",
  "data-closed:fade-out-0 data-closed:zoom-out-95",
  // Direction-aware slide (2 = 0.5rem)
  "data-[side=bottom]:slide-in-from-top-2",
  "data-[side=top]:slide-in-from-bottom-2",
  "data-[side=left]:slide-in-from-right-2",
  "data-[side=right]:slide-in-from-left-2",
  // Origin from trigger position
  "origin-(--transform-origin)",
  // Performance
  "data-entering:will-change-[opacity,transform]",
  "data-exiting:will-change-[opacity,transform]",
  // Accessibility
  "motion-reduce:animate-none motion-reduce:transition-none",
)}
模式 B:对话框(居中)
// Backdrop
className={cn(
  "data-open:animate-in data-closed:animate-out",
  "data-closed:fade-out-0 data-open:fade-in-0",
  "duration-200",
  "bg-black/10 supports-backdrop-filter:backdrop-blur-xs",
  "motion-reduce:animate-none motion-reduce:transition-none",
)}

// Content — note the slight overshoot scale
className={cn(
  "data-open:animate-in data-closed:animate-out",
  "data-closed:fade-out-0 data-open:fade-in-0",
  "data-closed:zoom-out-[0.98] data-open:zoom-in-[1.02]",
  "duration-200 ease-out-fluid",
  "data-entering:will-change-[opacity,transform]",
  "data-exiting:will-change-[opacity,transform]",
  "motion-reduce:animate-none motion-reduce:transition-none",
)}

Dialog 使用 zoom-in-[1.02](轻微过冲)和 zoom-out-[0.98],比浮层的 zoom-in-95/zoom-out-95 更微妙。

模式 C:侧边面板(按方向)
const sheetVariants = cva(
  // Base: transition + split enter/exit duration
  "transition ease-out-fluid " +
    "data-open:animate-in data-open:duration-300 " +
    "data-closed:animate-out data-closed:duration-200 " +
    "data-entering:will-change-[opacity,transform] " +
    "data-exiting:will-change-[opacity,transform] " +
    "motion-reduce:animate-none motion-reduce:transition-none",
  {
    variants: {
      side: {
        top: "data-open:slide-in-from-top data-closed:slide-out-to-top",
        bottom:
          "data-open:slide-in-from-bottom data-closed:slide-out-to-bottom",
        left: "data-open:slide-in-from-left data-closed:slide-out-to-left",
        right: "data-open:slide-in-from-right data-closed:slide-out-to-right",
      },
    },
  },
);

Sheet 会滑动 100%(从边缘到边缘),而不是像定位浮层那样使用 slide-*-2。

模式 D:按压反馈
"motion-safe:active:scale-[0.97]"; // Standard interactive elements
"motion-safe:active:scale-[0.98]"; // Menu items, tab triggers
"motion-safe:active:scale-[0.95]"; // Small controls
"motion-safe:active:scale-100"; // Explicitly disabled (ghost/link buttons)

始终添加 motion-safe: 前缀,绝不单独使用 active:scale-*。Checkbox 使用 motion-safe:group-active:scale-[0.95](group 变体)。

缩放值组件
scale-[0.97]Button(默认)、Switch(轨道)、Toggle、Toolbar(Button、Link)、Select(触发器)
scale-[0.98]Menu 项(Item、SubTrigger、CheckboxItem、RadioItem)、Tabs(触发器)
scale-[0.95]Checkbox(控件)、RadioGroup(项)、NumberField(Increment、Decrement)
scale-100Button 的 ghost 变体、Button 的 link 变体
模式 E:状态过渡
"transition-[color,background-color,border-color,box-shadow,opacity] duration-150 ease-smooth";
"motion-reduce:transition-none";

适用于:Button、Checkbox、Combobox(输入框/标签/移除标签)、Field(FieldGroup)、Input、InputGroup、Menu 项、NumberField、RadioGroup、Select(触发器)、Tabs(触发器)、TextField、Textarea、Toggle、Toolbar(button/link/input)、NavigationMenu(trigger/link)、Accordion(trigger)。


2. 缓动 Token

定义于 apps/docs/registry/prototyper-tokens.css:

Token值使用场景
ease-smoothcubic-bezier(0.4, 0, 0.2, 1)状态过渡、屏幕上的变化
ease-out-fluidcubic-bezier(0.32, 0.72, 0, 1)进入的元素:浮层、侧边面板、开关/滑块滑块
ease-in-quartcubic-bezier(0.895, 0.03, 0.685, 0.22)退出的元素:浮层关闭
ease-out-quadcubic-bezier(0.25, 0.46, 0.45, 0.94)可用,目前未使用
ease-out-quartcubic-bezier(0.165, 0.84, 0.44, 1)可用,目前未使用
ease-in-quadcubic-bezier(0.55, 0.085, 0.68, 0.53)可用,目前未使用
ease-in-out-quadcubic-bezier(0.455, 0.03, 0.515, 0.955)可用,目前未使用
决策树
Is the element entering or exiting?
├── Entering the screen → ease-out-fluid
├── Exiting the screen → ease-in-quart
└── No (staying on screen)
    ├── Moving or morphing? → ease-smooth
    ├── Color/background change? → ease-smooth
    ├── Constant motion (spinner, progress)? → linear
    └── Default → ease-smooth
在 Tailwind 中
  • 状态过渡:ease-smooth(直接使用工具类)
  • 浮层进入:data-open:[animation-timing-function:var(--ease-out-fluid)] 或 ease-out-fluid
  • 浮层退出:data-closed:[animation-timing-function:var(--ease-in-quart)]
  • 居中 Dialog:ease-out-fluid(进入和退出均使用)
  • Navigation menu:ease-[cubic-bezier(0.22,1,0.36,1)](导航专用,不是 Token)
为什么使用 ease-out

UI 默认使用 ease-out,因为它开始时速度快、结束时减速,给人以立即响应的感觉。UI 动画绝不使用 ease-in——它会在结束时加速,让人感觉迟缓、反应迟钝。300ms ease-out 给人的感觉明显比 300ms ease-in 更快。内置 CSS 缓动曲线无法满足精致 UI 的需求,因此始终使用项目自定义的 Token。

3. 时长等级

等级时长用途
Instant100msAlert Dialog、Context Menu、HoverCard、Menubar、Popover/Tooltip 退出动画
Fast150ms状态转换(所有交互元素)、覆盖层进入动画
Standard200msDialog(居中)、sheet 退出动画、checkbox 控件/SVG、accordion、button 伪元素
Medium250msSwitch 轨道、slider 滑块
Smooth300msSheet 进入动画、switch 滑块、meter/progress 条
Long350msNavigation menu 内容(大型滑动面板)
规则
  • UI 动画时长应低于 300ms(大型面板的导航菜单例外,为 350ms)
  • 退出动画应比进入动画快 20-33%(150→100、300→200)
  • 较大的元素动画较慢(dialog 200ms > tooltip 150ms > alert 100ms)
  • 时长应与移动距离相匹配——移动越远,时长越长
  • 按住确认操作使用约 2s 的线性动画;松开后的动画应较快(约 200ms ease-out)
  • 频繁出现的动画会令人厌烦——应彻底移除,而不是缩短时长

4. 缩放值

场景值类
覆盖层进入/退出0.95zoom-in-95 / zoom-out-95
Dialog 进入1.02(过冲)zoom-in-[1.02]
Dialog 退出0.98zoom-out-[0.98]
Nav menu 弹出层0.90scale-90(起始/结束样式)
Button/toggle 按压0.97scale-[0.97]
Menu item 按压0.98scale-[0.98]
小型控件按压0.95scale-[0.95]
Slider 滑块拖动0.85[&::after]:scale-[0.85]
禁用按压的覆盖值1.0scale-100

切勿从 scale(0) 开始动画——这会让人感觉突兀、不自然。最小起始缩放值为 0.85(slider 拖动)或 0.90(nav menu)。进入/退出动画始终应将缩放与透明度结合使用。


5. 与原点相关的覆盖层

所有定位的覆盖层都必须使用:

origin-(--transform-origin)

这会映射到 transform-origin: var(--transform-origin),Base UI 会根据弹出层相对于触发器的位置自动设置该值。若不使用它,覆盖层会从中心缩放,而不是从触发器缩放——这会让人感觉脱节。

适用对象: Popover、Menu、Tooltip、Select、Combobox、Context Menu、HoverCard/PreviewCard、Navigation Menu 弹出层。


6. 性能限制

仅对以下属性使用动画
  • transform(缩放、平移、旋转)——仅合成
  • opacity——仅合成
  • clip-path——硬件加速,不会造成布局偏移
切勿这样做
  • 使用 transition-all——它会为布局属性添加动画,导致卡顿
  • 直接对 width、height、padding、margin 添加动画(例外:accordion 使用关键帧高度动画,这是可接受的)
  • 在运行时通过 CSS 自定义变量添加动画——设置 CSS 变量会触发样式重新计算,并波及所有子元素。应直接应用变换。
  • 永久使用 will-change——仅在动画期间应用:data-entering:will-change-[opacity,transform] data-exiting:will-change-[opacity,transform]
明确列出过渡属性

切勿单独使用 transition。始终准确列出属性:

transition-[color,background-color,border-color,box-shadow,opacity]   → state transitions
transition-[color,background-color,border-color,box-shadow,transform] → checkbox (includes scale)
transition-[background-color,transform]                                → slider thumb
transition-[margin-inline-start,background-color]                      → switch thumb
transition-[width,background-color]                                    → meter/progress indicator
可中断性

对于交互式动画,CSS transitions 优于关键帧,因为 transitions 允许在序列中途重新设定目标——动画仍在运行时,你可以更改结束位置,动画会平滑地转向新目标。关键帧会锁定到固定终点,无法干净地中断。

Prototyper UI 中的所有状态转换都使用 CSS transition-*(完全可中断)。覆盖层的进入/退出使用关键帧(animate-in/animate-out)——这是可以接受的,因为它们持续时间很短(100-200ms),且由打开/关闭状态触发,而非由持续的用户交互触发。


7. 决策门

添加任何动画前,请回答三个问题:

  1. 它有明确的用途吗? 必须用于说明状态变化、提供响应反馈、构建空间逻辑,或带来愉悦感。
  2. 用户会多频繁地看到它? 不常见 = 合适的候选项。频繁出现或由键盘触发 = 完全跳过动画。
  3. 它会提升感知性能吗? 如果会造成延迟或阻塞交互,请重新考虑。

硬性规则:

  • 绝不为键盘触发的操作添加动画——这会让人觉得缓慢、迟滞且脱节
  • 绝不为用户在每个会话中使用数百次的操作添加动画(例如列表项悬停)
  • Ghost 按钮和链接按钮使用 motion-safe:active:scale-100(明确不使用按压缩放)
  • 标签页内容面板不添加进入/退出动画(即时切换)

8. 无障碍检查清单

每个动画元素都必须包含所有适用项:

  • motion-reduce:transition-none — 用于任何带有 transition-* 类的元素
  • motion-reduce:animate-none — 用于任何带有 animate-in/animate-out/animate-* 类的元素
  • motion-safe: 前缀 — 用于所有 active:scale-* 按压反馈(绝不裸用)
  • hover-only:hover: — 用于不应在触屏设备上触发的悬停效果(防止悬停状态粘滞)
  • motion-reduce:[&::after]:scale-100 — 用于伪元素缩放效果(Slider)
  • motion-reduce:after:transition-none — 用于伪元素过渡效果(Tabs)

自定义变体(定义于 prototyper-tokens.css):

@custom-variant motion-reduce (@media (prefers-reduced-motion: reduce));
@custom-variant motion-safe (@media (prefers-reduced-motion: no-preference));
@custom-variant hover-only (@media (hover: hover));

动画可能引发恶心感或分散注意力——务必提供减少动态效果的替代方案。优先使用淡入淡出而非弹跳,使用即时切换而非滑动。


9. 各组件覆盖层类

每种覆盖层组件的确切类字符串。大多数遵循模式 A,但时长/缓动各有不同。

Popover(标准配置——进入时长 150ms,ease-out-fluid;退出时长 100ms,ease-in-quart)
data-open:animate-in data-open:duration-150 data-open:[animation-timing-function:var(--ease-out-fluid)]
data-open:fade-in-0 data-open:zoom-in-95
data-closed:animate-out data-closed:duration-100 data-closed:[animation-timing-function:var(--ease-in-quart)]
data-closed:fade-out-0 data-closed:zoom-out-95
data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2
data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2
origin-(--transform-origin)
data-entering:will-change-[opacity,transform] data-exiting:will-change-[opacity,transform]
motion-reduce:animate-none motion-reduce:transition-none
Tooltip(时长与 Popover 相同)
data-open:animate-in data-open:duration-150 data-open:[animation-timing-function:var(--ease-out-fluid)]
data-open:fade-in-0 data-open:zoom-in-95
data-closed:animate-out data-closed:duration-100 data-closed:[animation-timing-function:var(--ease-in-quart)]
data-closed:fade-out-0 data-closed:zoom-out-95
data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2
data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2
origin-(--transform-origin)
data-entering:will-change-[opacity,transform] data-exiting:will-change-[opacity,transform]
motion-reduce:animate-none motion-reduce:transition-none
Menu/DropdownMenu(进入时长 150ms,退出时长 150ms——两个方向的时长相同)
data-open:animate-in data-open:duration-150 data-open:[animation-timing-function:var(--ease-out-fluid)]
data-closed:animate-out data-closed:duration-150 data-closed:[animation-timing-function:var(--ease-in-quart)]
data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95
data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2
data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2
origin-(--transform-origin)
data-entering:will-change-[opacity,transform] data-exiting:will-change-[opacity,transform]
motion-reduce:animate-none
Select(duration-150 ease-smooth,不拆分设置时长)
data-open:animate-in data-closed:animate-out
data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95
data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2
data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2
duration-150 ease-smooth origin-(--transform-origin)
data-entering:will-change-[opacity,transform] data-exiting:will-change-[opacity,transform]
motion-reduce:animate-none
Combobox(进入时长 duration-150,不显式设置退出时长)
data-open:animate-in data-open:duration-150 data-closed:animate-out
data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95
data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2
data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2
origin-(--transform-origin)
data-entering:will-change-[opacity,transform] data-exiting:will-change-[opacity,transform]
motion-reduce:animate-none
Context Menu、HoverCard/PreviewCard(duration-100 + 内联方向)
data-open:animate-in data-closed:animate-out
data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95
duration-100
data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2
data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2
data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2
origin-(--transform-origin) motion-reduce:animate-none
Alert Dialog(duration-100,不滑动、不设置变换原点)
Overlay: data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 duration-100
Content: data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 duration-100
Menubar(duration-100,继承 DropdownMenu)
data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 duration-100
+ directional slides + data-[side=inline-start/end] slides
Drawer(仅覆盖层——内容由 vaul 处理)
data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0

10. 特殊组件模式

Checkbox SVG 绘制动画
<path
  strokeDasharray={22}
  style={{
    strokeDashoffset: "var(--check-offset, 66)",
    transition: "stroke-dashoffset 200ms linear 50ms",
  }}
/>
// Toggle: group-data-checked:[--check-offset:44]
// Unchecked: offset 66 (stroke hidden). Checked: offset 44 (stroke draws on).
// 50ms delay lets background fill animation begin first. Linear for consistent draw speed.
Slider 拖动
cursor-grab data-dragging:cursor-grabbing data-disabled:cursor-default
data-dragging:[&::after]:scale-[0.85]  motion-reduce:[&::after]:scale-100
transition-[background-color,transform] duration-250 ease-smooth
[&::after]:transition-transform [&::after]:duration-250 [&::after]:ease-out-fluid
motion-reduce:transition-none [&::after]:motion-reduce:transition-none
NumberField 拖动区域
cursor-ew-resize touch-none
Accordion
data-open:animate-accordion-down data-closed:animate-accordion-up
h-(--accordion-panel-height) data-ending-style:h-0 data-starting-style:h-0

关键帧:0.2s ease-out(在 tokens 中定义为 --animate-accordion-down/up)。

导航菜单(独特的复杂模式)
  • 内容:duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)]
  • 较大的滑动距离:slide-in-from-right-52、slide-in-from-left-52
  • 激活方向样式:data-starting-style:data-activation-direction=left:translate-x-[-50%]、data-starting-style:data-activation-direction=right:translate-x-[50%]
  • 内容不透明度:data-ending-style:opacity-0 data-starting-style:opacity-0
  • 定位器:transition-[top,left,right,bottom] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)]
  • 定位器即时模式:data-instant:transition-none(用于连续悬停)
  • 弹出框:origin-(--transform-origin)、data-starting-style:scale-90 data-starting-style:opacity-0
  • 弹出框过渡:transition-[opacity,transform,width,height,scale,translate] duration-[0.35s]
  • 指示器:data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in
  • 切换图标:transition duration-300 group-data-open/navigation-menu-trigger:rotate-180
  • 触发器状态:transition-[color,background-color,border-color,box-shadow,opacity] duration-150 ease-smooth
开关
  • 轨道:transition-[color,background-color,box-shadow,opacity] duration-250 ease-smooth + motion-safe:active:scale-[0.97] + motion-reduce:transition-none
  • 滑块:transition-[margin-inline-start,background-color] duration-300 ease-out-fluid + motion-reduce:transition-none
进度条 / 计量器
  • 指示器:transition-[width,background-color] duration-300 ease-smooth + motion-reduce:transition-none
  • 不确定状态:data-[indeterminate]:animate-pulse + motion-reduce:animate-none
较长的状态过渡(与模式 E 的差异)
组件属性时长缓动
复选框控件color,background-color,border-color,box-shadow,transform200msease-smooth
组合框项 / 芯片移除colors200msease-smooth
开关轨道color,background-color,box-shadow,opacity250msease-smooth
开关滑块margin-inline-start,background-color300msease-out-fluid
滑块控件background-color,transform250msease-smooth
滑块控件 ::aftertransform250msease-out-fluid
计量器/进度条指示器width,background-color300msease-smooth
伪元素过渡
组件元素类
按钮(默认)::afterafter:transition-opacity after:duration-200
按钮(轮廓)::afterafter:transition-colors after:duration-200 after:ease-out
标签页(触发器)::afterafter:transition-opacity + motion-reduce:after:transition-none
NavigationMenu切换图标transition duration-300 group-data-open/navigation-menu-trigger:rotate-180
加载 / 不确定状态
  • 进度条:data-[indeterminate]:animate-pulse + motion-reduce:animate-none
  • Toast:在 Loader2Icon 上使用 animate-spin(sonner 负责处理 Toast 的所有其他动画)
抽屉 / Toast

动画分别由 vaul 和 sonner 库处理。不要添加自定义动画类,而应配置库的属性。

无动画组件

Avatar、Collapsible、Label、Separator——按设计不包含任何动画、过渡或运动类。

ScrollArea

极简:ScrollBar 上的 transition-colors(使用浏览器默认时长)。


11. 高级技巧

Clip-path

由硬件加速,不会导致布局偏移。clip-path: inset(top right bottom left):

值效果
inset(0)完全可见
inset(100%)完全隐藏
inset(0 0 100% 0)从底部隐藏
inset(0 100% 0 0)从右侧隐藏
inset(0 0 50% 0)隐藏下半部分
inset(50% 0 0 0)隐藏上半部分

添加圆角:clip-path: inset(0 75% 0 0 round 17px)。

适用场景:图像揭示、对比滑块、文本遮罩效果、标签页过渡、滚动触发的揭示效果、按住删除时的填充效果、主题切换。建议的缓动函数:cubic-bezier(0.77, 0, 0.175, 1)。

模糊衔接

当调整时长和缓动函数后动画仍然不够自然时,可在过渡期间添加 filter: blur(2px)。这能衔接不同状态间的视觉差异,让视觉效果看起来更加平滑。与缩小效果搭配效果很好。目前 Prototyper 的 UI 组件中尚未使用此技巧——可将其作为变形动画的最后手段。

可中断设计

用户应能在动画进行过程中平滑地更改动画状态。若动画在完成前锁定用户操作,会让人觉得响应迟缓。CSS 过渡天然支持中断——可以在首次过渡完成前重新指定结束值,动画会平滑地改变方向。

手势与拖动处理
  • 动量速度: 计算方式为 Math.abs(swipeAmount) / timeTaken,阈值设为 0.11,即使未达到距离阈值,也可通过快速轻扫关闭
  • 阻尼/阻力: 朝受限方向拖动时(例如抽屉位于顶部时向上拖动),逐渐增加阻力——距离越大,移动幅度越小。与 iOS 原生行为一致。
  • 滚动与拖动冲突: 仅当内容滚动到顶部时才允许拖动手势。到达顶部后添加 100ms 的超时,以防滚动惯性导致意外关闭。
  • 多点触控: 初始触点之后的所有触点都应忽略,直到初始触点释放。这样可防止用户切换手指时元素位置跳动。
  • 指针捕获: 滑动或拖动时使用 element.setPointerCapture(event.pointerId),这样指针移出元素边界后仍不会丢失手势。
  • 摩擦: 朝“错误”方向拖动时实现阻力,而不是突然停止——这样感觉更自然。
Toast 动画(sonner)

所有 Toast 动画均由 sonner 库处理:

  • 进入:translateY(100%) → translateY(0),400ms 缓动
  • 堆叠:绝对定位,使用 translateY(calc(var(--lift-amount) * var(--toasts-before))),缩放系数为 0.05 * index
  • 滑动关闭:动量阈值 0.11、指针捕获、向上拖动时施加摩擦
  • 悬停间隙::after 伪元素填充堆叠 Toast 之间的间隙
  • 标签页可见性:通过 visibilitychange,在 document.hidden 时暂停计时器

不要实现自定义 Toast 动画——请配置 sonner 属性。

Drawer 动画(vaul)

所有 Drawer 动画均由 vaul 库处理:

  • 标志性曲线:cubic-bezier(0.32, 0.72, 0, 1) = 我们的 --ease-out-fluid(500ms,与 iOS Sheet 一致)
  • 背景缩放:根据拖动进度,在页面容器上应用 transform 和 border-radius
  • 吸附点:释放时吸附到最近的吸附点,支持视口比例和固定像素值,并基于动量跳过吸附点
  • 键盘:Visual Viewport API(visualViewport resize 事件)会调整抽屉高度

不要添加自定义动画类——请配置 vaul 属性。

工具提示与顺序交互

Base UI 的 TooltipProvider 原生处理工具提示分组:

  • 第一个工具提示:加入延迟,防止意外触发
  • 同一组中的后续工具提示:完全跳过延迟和动画
  • 实现方式:使用 data-instant 属性,并设置 transition-duration: 0ms

评论

支持 Markdown