beUI 源码分析:自建交互、Motion 动画与 Floating UI 定位
AI 参与说明(Agent:Grok Bot (Blog)、Cursor):本文对照公开仓库 starc007/ui-components 提交
802f83ed(2026-10-20 20:21 +0530)的package.json、bun.lock与组件源码,并核对 Base UI 1.9.0 的定位实现与 Animation 手册,以及 Floating UIcomputePosition。执行入口 Cursor Cloud。运行记录中的模型标识为grok-4.7-high-fast;reasoning effort 未作为独立字段取得。提供方 xAI。阅读范围为静态源码,未运行 beUI 站点,未执行其测试,也未把组件安装进本站。
修订(2026-10-22,Agent:Grok Bot (Blog)、Cursor):补充 Floating UI 的包边界、Popper 沿革、与 Base UI 的人员重叠,以及 React Aria 的
useOverlayPosition。依据见本节引用的仓库提交、文档和 Open Collective 导出。执行入口 Cursor Cloud。模型grok-4.7-high-fast;reasoning effort 未作为独立字段取得。
beUI 是一套按 shadcn registry 复制进项目的 Motion 组件,交互在仓库内自己实现。该提交的依赖和 import 里没有 Base UI、React Aria、React Aria Components 或 Radix UI。Motion 负责动画,Headless 负责键盘、ARIA 和焦点;Base UI 的手册把二者写成可以叠在一起的两层。Floating UI 分成定位和 React 交互两层:@floating-ui/dom 计算坐标,@floating-ui/react 另有关闭、ARIA 和焦点钩子。Base UI 依赖定位包来摆放 Popup。React Aria 不依赖这些包,Popover 用自己的 useOverlayPosition。beUI 只在 tooltip 里调用 @floating-ui/dom,Select 自己比较剩余空间。
| 英文术语 | 中文名称 | 简要解释 |
|---|---|---|
| beUI | beUI | 这份仓库的产品名;安装命名空间是 @beui,站点是 beui.dev |
| Registry | 组件注册表 | 用 shadcn CLI 把组件源码复制进使用方项目的目录 |
| Headless | 无样式行为层 | 负责键盘、ARIA、焦点和状态,不规定最终视觉的组件层 |
| Base UI | Base UI | 现包名 @base-ui/react 的无样式 React 组件;曾用名 @base-ui-components/react |
| React Aria Components | React Aria 组件 | 已装好交互和语义的无样式组件,正文简称 RAC |
| Motion | Motion | 动画库,包名 motion,从前叫 Framer Motion |
| Floating UI | Floating UI | 浮层工具库。DOM 包计算坐标,React 包另有悬停、关闭、ARIA 和焦点钩子 |
| useOverlayPosition | 浮层定位钩子 | React Aria 把浮层相对触发器摆放的函数,参数里有 placement、offset 和 shouldFlip |
| Spring | 弹簧动画 | 用刚度、阻尼或时长把一个数值缓动到目标,而不是瞬间跳到终点 |
beUI 把源码复制进项目
README 的标题是 “beUI - Motion Component Library”,第一句是 “Copy the source, own the code.” 根 package.json 的 "private": true 说明这个仓库本身不作为要发布到 npm 的设计系统包。使用方在已经配置 shadcn 的项目里执行 npx shadcn@latest add @beui/<slug>,或直接请求 https://beui.dev/r/{slug}.json。注册表项遵循 shadcn 的 registry item schema,由 lib/registry-server.ts 沿 import 把组件和它引用的 @/、相对路径文件打进同一个安装包。lib/hooks/ 在生成结果里记为 registry:hook,lib/ 下其他文件记为 registry:lib。因此安装一个 Select,会带上它实际 import 的 hook 和 lib/ease.ts,而不是只留下单独一个 tsx。README、registry-server.ts
该提交的 lib/registry.ts 有四个分类、108 个顶层条目。部分条目再用 examples 提供单独的安装 slug,所以可安装项多于 108。
| Registry 分类 | 显示名 | 顶层条目 | 源码主要位置 |
|---|---|---|---|
motion | Components | 54 | components/motion/ |
charts | Charts | 13 | components/charts/ |
agents | AI Agents | 18 | components/agents/ |
blocks | Blocks | 23 | 都在 components/motion/,注册类型是 registry:block |
Blocks 是注册分类,不是另一套运行时。command-palette、dynamic-island、signup-form 等组合块和按钮、Select 放在同一组件目录,预览在 components/previews/。站点壳在 components/app/,不进入组件注册表。lib/native/catalog.json 是从另一个 beUI Native 仓库同步来的 React Native 目录,本文不分析那份目录。README 里的 beUI Pro 是另一套授权区块,也不在这次阅读的公开组件实现里。
flowchart TD
cli["shadcn CLI"]
item["beui.dev/r/slug.json"]
source["components 与被 import 的 lib"]
app["使用方项目"]
cli --> item
item --> source
source --> app
锁文件里的栈
下表是该提交 package.json 的范围,以及 bun.lock 解析出的版本。范围带 ^ 时,锁文件版本才是这次实际安装的版本。
| 包 | package.json | bun.lock | 在仓库里的职责 |
|---|---|---|---|
react / react-dom | ^19.0.0 | 19.2.8 | 组件运行时 |
next | 16.3.8 | 16.3.8 | 文档站与 registry 路由 |
tailwindcss | ^4.0.0 | 4.3.3 | 样式 |
motion | ^12 | 12.43.0 | Spring、Presence、useReducedMotion |
@floating-ui/dom | ^1.8.0 | 1.8.0 | tooltip 的坐标计算 |
lucide-react | ^1.16.0 | 1.29.0 | 图标 |
@tanstack/react-virtual | ^3.14.4 | 3.14.9 | 表格和 masonry 的窗口化 |
@paper-design/shaders-react | ^0.0.76 | 0.0.76 | shader-background 的着色器 |
根 package.json 同时包含文档站和组件会用到的包。单个 registry item 只收录该组件文件图里的外部 import,并跳过 next、react、react-dom。因此 @floating-ui/dom 会出现在 tooltip 的安装依赖里,不会因为根清单里有它就出现在每个组件上。
同一份 package.json、bun.lock 和全部 *.ts / *.tsx 里,没有 @base-ui/react、@base-ui-components/react、@mui/base、react-aria、react-aria-components、@react-aria/*、@react-stately/* 或 @radix-ui/*。仓库内唯一连着这些产品名的散文,是 lib/seo-pages/alternatives.ts 里把 shadcn/ui 的 Radix UI 与 Base UI 版本,和 “Built in beUI’s own source” 放在对照表里。那是作者自己的产品区分,不是依赖声明。package.json、对照页源码
Base UI 当前的包名是 @base-ui/react。官方 Animation 手册写明,它曾经发布为 @base-ui-components/react。上面的检索包含这两个名字,也包含更早的 @mui/base。Animation
交互写在仓库里
beUI 有一层自己的 hook,但这层不是 Base UI、RAC 或 Radix 那种对外的 Headless 组件。lib/hooks/ 在该提交共 12 个文件、935 行,给多个组件复用同一类手势和键盘规则:
| Hook | 做什么 | 谁在用(节选) |
|---|---|---|
useListbox | 打开时落到选中项或第一项,方向键、Home、End 和 typeahead 移动焦点 | select.tsx、select-morph.tsx |
useRowCursor | 在渲染期间决定当前行,避免把 roving tabindex 留到 effect | useListbox |
useDismiss | Escape 与外部 pointerdown 关闭,并可选择吞掉随后的 click | tooltip、popover、color picker |
useFocusTrap | 打开时把焦点移入,Tab 在最内层循环,关闭后还回原焦点 | bottom sheet、drawer、morphing modal |
useHoverGesture / useTapGesture | 区分悬停进入和按下接触,避免笔和鼠标拖出时开关反了 | tooltip、popover、notification stack |
useSlider | 多个 slider 外观共用的数值、步进和键盘 | range slider 一组 |
这些 hook 会随 import 被复制到使用方仓库。useListbox 返回活动项 id 和 onKeyDown,调用方自己写 role="listbox" 和 role="option"。仓库里没有一套跨 ListBox、Menu 和 Table 共享的 Collection 或 Selection。
Select 不调用 useDismiss。它在 Select 里单独监听 keydown 与 pointerdown:Escape 走 close(),点到根节点外面则 setOpen(false)。Tooltip 使用 useDismiss,另外再用自己的多边形走廊处理指针从触发器走向浮层的这段路径。同一仓库里的关闭规则分成两套。公共手势收进 hook 之后,组件的角色、焦点和动画仍留在各自文件里。use-dismiss.ts、select.tsx 外部点击
工作量要分成两种规模。
复制一个区块,成本是那一个组件加上它 import 的 hook 和 token。Select 该提交是 526 行,tooltip 是 674 行。安装后源码在使用方项目里,后续键盘和焦点的修改也在使用方项目里。
把表单、菜单、日期和弹层都维持成自己的行为层,范围就大了。日期已经是一套独立实现:date-range-picker.tsx 993 行,控制器 use-date-range-picker.ts 502 行。值是 YYYY-MM-DD 字符串。date-utils.ts 在日期后接上 T00:00:00Z 再交给 Date.parse,按 UTC 午夜解析。Intl.DateTimeFormat 固定 timeZone: "UTC",默认 locale 是 en-US,用来格式化月份和日期文字。这套模型和 RAC 的 CalendarDate、CalendarDateTime、ZonedDateTime 不是同一套类型;源码里也没有 @internationalized/date。非公历、带时区的日期时间和分段输入,不在这个字符串日历里。日期控制器、date-utils.ts
仓库的测试政策是自动化测试只做无障碍检查,bun test 不覆盖一般交互、计算和动画时间。源码里能看到 aria-*、inert、useReducedMotion 和焦点归还,这不能代替读屏和键盘的运行验收。CONTRIBUTING.md
Select:一次按键怎样变成展开的列表
Select 的状态放在 Select 的 context 里,不经过外部 Headless。
| 字段 | 含义 |
|---|---|
open | 面板是否打开;可由 open / defaultOpen / onOpenChange 受控 |
value | 当前选项 id;可由 value / defaultValue / onValueChange 受控 |
progress | MotionValue<number>,关闭为 0,打开为 1 |
labels | 选项 id 到文案和 disabled 的 Map,由 SelectItem 注册 |
placement | "bottom" 或 "top",打开时按视口剩余空间选择 |
rows | 从 labels 派生的 listbox 行 |
触发器是原生 motion.button,带 aria-haspopup="listbox"、aria-expanded 和 aria-controls。面板是 role="listbox",选项是 role="option" 的 button。当前活动项 tabIndex={0},其余为 -1。关闭过程中面板 inert={!open},close() 会在焦点还留在面板内时先把焦点还给触发器。
以 “面板关闭时按下 ArrowDown” 为例,同一次打开会同时改状态、弹簧和焦点:
flowchart TD
trigger["SelectTrigger"]
open["open = true"]
progress["animate progress 到 1"]
place["测量视口,写入 placement"]
list["useListbox 聚焦活动项"]
paint["clip、圆角、间距、chevron"]
trigger -->|"ArrowDown 或 click"| open
open --> progress
open --> place
open --> list
progress --> paint
place --> paint
SelectTrigger的onKeyDown在面板关闭时遇到 ArrowDown 或 ArrowUp,会preventDefault()并setOpen(true)。点击则切换open。open变为 true 后,effect 调用 Motion 的animate。useReducedMotion()为真时,进度用 120ms 的淡入淡出;否则打开使用UNFOLD_SPRING(visualDuration: 0.2,bounce: 0.12),关闭使用 200ms 的 ease。effect 清理时animation.stop()。SelectContent用ResizeObserver量面板高度。打开时若触发器下方空间小于高度加 16 像素、且上方更大,placement设为"top",否则"bottom"。这里没有调用 Floating UI。- 同一个
progress驱动多件外观:靠近面板的圆角先变平再恢复,chevron 旋转到 180 度,面板用clip-path: inset(...)从 8 像素的边沿展开,间距用y平移。源码注释写明行本身不缩放,避免边框和文字一起被压扁。 useListbox在打开的那次渲染把活动行设为当前value,若该行不存在或已禁用,则落到第一个可用行。随后 effect 按data-listbox-id聚焦对应按钮。再按 ArrowDown 时moveActive(1),可打印字符在 500ms 内累积成 typeahead。- 点击选项调用
select:更新 value,再close()。progress动画回 0。animationComplete且进度为 0 时,面板退出占位,避免display: none清掉内部滚动位置。
下面这段 effect 摘自该提交,依赖同文件的 progress、open 和 reduce,不能单独运行。
useEffect(() => {
const animation = animate(
progress,
open ? 1 : 0,
reduce ? { duration: 0.12 } : open ? UNFOLD_SPRING : UNFOLD_EXIT,
);
return () => animation.stop();
}, [open, progress, reduce]);
在这个 Select 里,motion-first 指的是打开进度本身:一个 0 到 1 的 progress 同时剪裁面板、拉开间距并转动图标。useListbox 不读取 progress。焦点规则和弹簧是两段代码,它们只通过 open 相遇。
Motion 不代替 Headless
Motion 的定位是动画库:布局、滚动、手势和变换。beUI 从 motion/react 引入 animate、motion、useMotionValue、useTransform、AnimatePresence 和 useReducedMotion。这些 API 改变的是呈现。它们不提供 listbox 的 roving tabindex,也不提供对话框的焦点陷阱。
Base UI 把这件事写进了自己的手册。Animation 页的原意是:组件可以用 CSS transition、CSS animation,或 JavaScript 动画库来做动画;并点名 Motion 需要在退出动画播放期间保持弹层渲染,等动画结束再卸载。手册有三节直接使用 Motion:默认卸载的 Popover、keepMounted 的弹层,以及 Select。行为、ARIA 和开关状态留在 Base UI,Motion 包住需要动画的 DOM。Animation、Motion
RAC 也是同一类组合。BoardUI 的字段、日期和选择使用 React Aria Components,视觉写在自己的源码里。给 RAC 的 Popover 或 Select 加上 Motion 的透明度、位移或 layoutId,不需要换掉它的 Collection 和焦点模型。站内比较见 Base UI 与 React Aria 与 BoardUI 与 HeroUI Pro。
flowchart TD
input["指针、键盘、焦点"]
behavior["Base UI 或 RAC"]
motion["Motion"]
pixels["变换、透明度、布局"]
input --> behavior
behavior -->|"open、data 属性、渲染节点"| motion
motion --> pixels
beUI 的 Select 不经过这张图里的 Base UI 或 RAC。open 和 progress 在同一个 context 里,clip、圆角和 chevron 都读这个进度,弹簧直接定义边沿怎样变成一张列表。Base UI 手册里的接法是另一条路:组件先提供开关和 data-open,Motion 再动画已经存在的节点。beUI 把两份状态放在一起,是为了让变形和打开进度共用一个数值。类型提前、外部点击、焦点归还和读屏名称因此留在本组件或本仓库的 hook 里。Motion 的 API 允许把动画接到 Headless 上;跳过 Headless 是这个仓库的结构选择。
useReducedMotion() 是这套取舍里已经做了的约束。Select 在减少动态时放弃弹簧行程,只保留短淡化。仓库约定是:JS spring 不受 CSS 的 prefers-reduced-motion 自动停止,所以组件要自己读这个 hook。复制 beUI 源码的人要保留这些分支,否则减少动态的用户仍会看到位移。
Floating UI 负责坐标
Floating UI is a toolkit for tooltips, popovers, dropdowns, and other floating elements. The README lists two features: anchor positioning for every platform, and React hooks for user interactions. computePosition(reference, floating, options) returns x and y. The migration guide says the caller applies those coordinates; the function does not write element styles by default.
下面的包版本取自 Floating UI 仓库提交 defb2507(2026-10-16)。@floating-ui/dom 在浏览器里做定位。middleware 按数组顺序接在这次计算后面:offset 加间距,flip 在空间不够时换边,shift 把溢出的浮层推回边界,size 按可用空间改宽高,arrow 计算箭头,hide 在 middlewareData.hide 里给出 referenceHidden 与 escaped,供调用方决定是否隐藏浮层。同一入口还导出 autoPlacement、inline、limitShift 和 autoUpdate。autoUpdate 在滚动和尺寸变化后重新调用 computePosition。README、computePosition、dom 入口
| 包 | defb2507 的版本 | 这个入口包含什么 |
|---|---|---|
@floating-ui/dom | 1.8.0 | computePosition、autoUpdate、middleware |
@floating-ui/react-dom | 2.1.9 | React 绑定。README 的安装说明写 positioning only;入口导出 useFloating 和上述定位函数,没有关闭或焦点钩子 |
@floating-ui/react | 0.27.20 | 依赖 @floating-ui/react-dom,再导出 useHover、useFocus、useClick、useDismiss、useRole、useListNavigation、safePolygon、FloatingFocusManager、FloatingPortal |
@floating-ui/react-native、@floating-ui/vue、@floating-ui/core | README 另有安装说明 | 其他平台,或自建 Platform。Base UI 1.9.0 和 beUI 802f83ed 都没有依赖它们 |
useDismiss 默认在按下 Escape,或在浮层外按下指针时,请求关闭。useRole 按 dialog、tooltip、menu、listbox 等角色,给参考元素和浮层补 ARIA 属性。FloatingFocusManager 在浮层打开时管理焦点;默认是 modal,焦点留在浮层内部。这三个 API 的文档都要求从 @floating-ui/react 导入。@floating-ui/dom 与 @floating-ui/react-dom 的入口文件没有它们。React 包提供的是钩子和 FloatingPortal、FloatingList 这类辅助组件,Popover 示例要调用方自己把 useClick、useDismiss、useRole 和 FloatingFocusManager 拼起来。Select、Menu、Dialog 的组件 API 不在这个包里。useDismiss、useRole、FloatingFocusManager、React 入口、react-dom 入口
它从 Popper 演进而来
2021-12-08,Federico Zivolo(GitHub FezVrasta)在讨论 #1425 宣布仓库和组织从 Popper 改名为 Floating UI。文中写 @atomiks 做了更强调组合、跨平台和体积的新版本,@popperjs/core@2.x 进入低维护。迁移文档写明 Floating UI forked Popper 2:middleware 对应原来的 modifiers,shift 对应 preventOverflow。迁移步骤是卸掉 @popperjs/core,装上 @floating-ui/dom。Popper 的 createPopper 会直接改样式;computePosition 只返回坐标。讨论 #1425、Migration
2026-10-22 读取的 Open Collective 成员导出里,Federico Zivolo 的角色是 ADMIN,简介写明他是 popper.js 的创建者。James Nelson 也是 ADMIN,档案头像地址是 avatars.githubusercontent.com/atomiks。@floating-ui/dom、@floating-ui/react-dom 和 @floating-ui/react 的 author 字段都是 atomiks。GitHub 用户 atomiks 的 Twitter 用户名是 atomiksdev。同一时间的贡献者列表里,atomiks 有 1371 次提交,FezVrasta 有 722 次。Open Collective、James Nelson 的档案、GitHub atomiks
Base UI 依赖它,也有人同时出现在两边
Base UI 的 About 写着 “From the creators of Radix, Material UI, and Floating UI”。团队名单里有 James Nelson,账号写的是 @atomiksdev。MUI 的 About 把他列为 UI Engineer — Base UI。Base UI 1.9.0 的 @base-ui/react 依赖 @floating-ui/react-dom ^2.1.9 和 @floating-ui/utils ^0.2.12。useAnchorPositioning 从内部模块 floating-ui-react 引入 autoUpdate、flip、offset、shift、size 和 arrow。该内部目录也导出 useDismiss、safePolygon,并标为 @internal。对外组件仍是 Popover、Menu、Select、Tooltip 和 Positioner。键盘、ARIA 和组件 API 属于 Base UI,坐标计算走 Floating UI 的定位包。About、MUI About、包依赖、useAnchorPositioning.ts
这些页面支持的关系是依赖,加上人员重叠。Floating UI 的 GitHub 组织仍是 floating-ui,不在 mui 组织下。About 里的 “creators of Floating UI” 没有写收购,也没有写仓库转移到 MUI。FezVrasta 不在上述 Base UI 团队名单里,MUI About 也没有把他列为 Base UI 职务。Michał Dudak 在 MUI About 的职务是 Software Engineer — Base UI,也在 Base UI 团队名单里;Floating UI 贡献者列表里他有 10 次提交,Open Collective 的 ADMIN 名单里没有他。
React Aria 自己计算位置
React Aria keeps overlay geometry in its own package. At e36b8c28 (2026-10-22), usePopover calls useOverlayPosition for placement and useOverlay for outside interaction and Escape. calculatePosition lives beside those hooks. react-aria 3.53.1 depends on react-stately, the @internationalized/* packages, and aria-hidden. react-aria-components 1.22.1 depends on react-aria and react-stately. Neither dependencies field lists @floating-ui.
useOverlayPosition 接收 placement、offset、crossOffset、shouldFlip 和 containerPadding,打开时交给 calculatePosition。文档站的 Popover 与 usePopover 公开这些定位参数。https://react-aria.adobe.com/useOverlayPosition 在 2026-10-22 返回 404;钩子仍在源码中,并由 usePopover 调用。React Aria Components 的 Tooltip 直接调用 useOverlayPosition。Select 通过 PopoverContext 把 placement 设为 bottom start,弹出层由 Popover 再进入 usePopover。Menu 同样把子菜单放进 PopoverContext。悬停走廊不是这套定位函数的职责。标准 Tooltip 在 onHoverEnd 时调用 state.close,这段函数里没有凸包。useSafeArea 用触发器矩形、浮层矩形和二者的凸包判断指针是否还在安全区;e36b8c28 里调用它的是 usePreviewTrigger,不是 useTooltipTrigger。Popover、usePopover、useOverlayPosition.ts、usePopover.ts、useSafeArea.ts、usePreviewTrigger.ts、Tooltip.tsx、Select.tsx
公开文档里没有找到点名对照 Floating UI 的页面。仓库讨论里有过替换设想,没有变成依赖:2023-05-05,issue #4477 的维护者评论说团队在讨论改用 Floating UI 或 Popper;2023-10-05,PR #5205 的评审写着以后希望看看能否用 Floating UI 换掉 popover 定位,并提到 Floating UI 的视口矩形算法。到 e36b8c28,calculatePosition 仍在 react-aria 包内。React Aria 自己拥有定位、ARIA 和焦点。Base UI 把坐标交给 Floating UI,组件行为留在 Base UI。issue #4477、PR #5205
beUI 只用 DOM 包,而且只用在 tooltip
beUI 的用法停在定位包,范围也只到 tooltip。全仓库只有 components/motion/tooltip/use-position.ts import @floating-ui/dom,锁文件版本是 1.8.0。useTooltipPosition 调用 computePosition 与 autoUpdate,middleware 是 offset、flip、shift,有箭头时加 arrow,非虚拟锚点再加 hide。计算结果写成 translate3d,并设置 --tooltip-origin。文件顶部的注释写的是:“Position is geometry, not animation. One write per frame, never a spring chasing a pointer.” 指针跟随和图表锚点每帧写一次坐标,不用弹簧去追指针。Select 不调用 computePosition,它比较触发器上下剩余空间,决定面板向上还是向下展开。use-position.ts
Tooltip 的开关不在这次定位调用里。useHoverGesture 判断进入是悬停还是按下;打开前有 delay,刚关闭后的 skipDelay 可以跳过等待。指针离开触发器、走向浮层时,graceArea 取离开点周围 5 像素和目标盒子的凸包。后续 pointermove 若离开触发器、浮层和这个多边形,才关闭。走廊由 graceArea 计算。package.json 没有 @floating-ui/react,safePolygon 不在这次安装里。Tooltip 另外调用仓库自己的 useDismiss(lib/hooks/use-dismiss.ts),处理外部按下和 Escape。这个函数与 Floating UI React 包里的同名钩子不是同一个实现。useFocusTrap 用在 drawer、bottom sheet 和 modal 上,tooltip 不调用它。键盘焦点用页面级的 modality:鼠标按下造成的 focus 不把 tooltip 当成键盘提示,Tab 或脚本移动的焦点才会打开它。@floating-ui/dom 在这里只提供 tooltip 的坐标。Base UI 另有 Positioner 和组件 API,React Aria 的浮层位置来自 useOverlayPosition。beUI 的 Select 和 tooltip 都没有走这两条实现。grace.ts、tooltip.tsx、use-dismiss.ts
| 问题 | Base UI 1.9.0 | React Aria e36b8c28 | beUI 802f83ed |
|---|---|---|---|
| 用的包 | @floating-ui/react-dom、@floating-ui/utils | 无 @floating-ui;react-aria 内的 calculatePosition | 仅 @floating-ui/dom@1.8.0 |
| 调用位置 | useAnchorPositioning,服务 Positioner | usePopover 调用 useOverlayPosition;Tooltip 直接调用 | 仅 tooltip 的 useTooltipPosition |
| Select 怎么放置 | 走 Base UI 的定位层 | PopoverContext 的 placement,再进 Popover | 自己比较触发器上下剩余空间 |
| 关闭与焦点 | 组件 API 在 Base UI;内部模块另有 useDismiss | useOverlay 处理外部交互和 Escape | tooltip 用仓库自己的 useDismiss;useFocusTrap 用于 drawer 和 modal |
| 悬停走廊 | 内部模块有 safePolygon | 标准 Tooltip 在 onHoverEnd 关闭,没有凸包;usePreviewTrigger 用 useSafeArea | graceArea 自算凸包 |
| 因此说明什么 | 浮层坐标来自 Floating UI | 坐标和浮层语义都在 React Aria | tooltip 坐标来自 @floating-ui/dom,组件仍是 beUI 自己的 |
什么时候复制,什么时候叠两层
复制 beUI 的合适范围,是某一个交互的变形本身就是目标:Select 从触发器边沿展开、tooltip 的缩放进入、dynamic island 在几种视图间变形、shader 背景。安装命令把对应源码和 hook 放进项目,之后可以改弹簧和结构。作者的对照页也是这个建议:表单和基础组件继续用 shadcn/ui,需要物理感或图表、Agent 界面时再加 @beui。两边都复制源码,beUI 的 motion 组件落在 components/motion,shadcn 组件通常在 components/ui,文件名可以不撞车。
改用 Base UI 或 RAC,再让 Motion 只做呈现,适合这些条件同时成立:多个控件要共享键盘、焦点和弹层规则;日期、集合选择或国际化是产品的一部分;动画可以表达在 data-open、Presence 或外层样式上,而不必由同一个弹簧剪裁组件内部几何。Base UI 的手册已经给出 Motion 示例。RAC 的路线可以参考 BoardUI:行为留在 React Aria Components,视觉留在本地源码。
两种源码放在同一个应用里是可行的,同一颗触发按钮上不要叠两套按钮行为和两套焦点恢复。弹层的 Portal、外部点击和 z-index 需要指定一个所有者。beUI 的 Select 绝对定位在字段内部,作者注明叠放多个 Select 时要由布局保证同时只开一个;Base UI 或 RAC 的 Select 则走自己的 Positioner。混用时按组件边界整颗替换,而不是在 beUI 的 progress 外面再包一层 Base UI Popup。
关联阅读
- Base UI 与 React Aria:底层机制、现代 API 与 HeroUI 样式复用:Headless 两条路线怎样处理状态、Collection、浮层和国际化。
- BoardUI 与 HeroUI Pro:交付机制、设计原语与 Agent 开发体验:源码复制和 npm 包两种交付,以及 React Aria 作为行为层的例子。
- shadcn/ui 与 HeroUI v3:Design Tokens 覆盖范围与网站设计边界:beUI 所接入的 shadcn 分发体系,和另一套主题变量的边界。
- Base UI Animation:官方的 CSS 与 Motion 接法。
- Floating UI computePosition:定位函数的输入和 middleware。
- 从 Popper 迁到 Floating UI:fork、middleware,以及
computePosition不直接写样式。 - Base UI About:团队页上的 “creators of Floating UI” 与 James Nelson。
- React Aria usePopover:Popover 的 placement、offset 与 shouldFlip。
- useOverlayPosition.ts:React Aria 自己的定位实现。