base-ui-react by jackspace/claudeskillz
npx skills add https://github.com/jackspace/claudeskillz --skill base-ui-react状态 : Beta (v1.0.0-beta.4) - 稳定版 v1.0 预计 2025 年第四季度 最后更新 : 2025-11-07 依赖项 : React 19+, Vite (推荐), Tailwind v4 (推荐) 最新版本 : @base-ui-components/react@1.0.0-beta.4
Base UI 目前处于 beta 阶段。在生产环境中使用前请注意:
建议 : 对于能够接受 beta 软件的新项目,可以使用。对于关键的生产应用,请等待 v1.0 版本。
pnpm add @base-ui-components/react
为何重要:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// src/App.tsx
import { Dialog } from "@base-ui-components/react/dialog";
export function App() {
return (
<Dialog.Root>
{/* 渲染属性模式 - Base UI 的关键特性 */}
<Dialog.Trigger
render={(props) => (
<button {...props} className="px-4 py-2 bg-blue-600 text-white rounded">
打开对话框
</button>
)}
/>
<Dialog.Portal>
<Dialog.Backdrop
render={(props) => (
<div {...props} className="fixed inset-0 bg-black/50" />
)}
/>
<Dialog.Popup
render={(props) => (
<div
{...props}
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg shadow-xl p-6"
>
<Dialog.Title render={(titleProps) => (
<h2 {...titleProps} className="text-2xl font-bold mb-4">
对话框标题
</h2>
)} />
<Dialog.Description render={(descProps) => (
<p {...descProps} className="text-gray-600 mb-6">
这是一个 Base UI 对话框。完全无障碍,完全由你自定义样式。
</p>
)} />
<Dialog.Close render={(closeProps) => (
<button {...closeProps} className="px-4 py-2 border rounded">
关闭
</button>
)} />
</div>
)}
/>
</Dialog.Portal>
</Dialog.Root>
);
}
关键点:
{...props}<Dialog.Portal> 在 DOM 层次结构外渲染Backdrop 和 Popup 是独立的组件 (与 Radix 的 Overlay + Content 组合不同)对于需要智能定位的组件,请用 Positioner 包裹:
import { Popover } from "@base-ui-components/react/popover";
<Popover.Root>
<Popover.Trigger
render={(props) => <button {...props}>打开</button>}
/>
{/* Positioner 使用 Floating UI 进行智能定位 */}
<Popover.Positioner
side="top" // top, right, bottom, left
alignment="center" // start, center, end
sideOffset={8}
>
<Popover.Portal>
<Popover.Popup
render={(props) => (
<div {...props} className="bg-white border rounded shadow-lg p-4">
内容
</div>
)}
/>
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
Base UI 使用 渲染属性 而非 Radix 的 asChild 模式。这提供了:
✅ 显式的属性展开 - 清晰展示哪些属性被应用 ✅ 更好的 TypeScript 支持 - 属性的完整类型推断 ✅ 更易调试 - 在开发者工具中检查属性 ✅ 组合灵活性 - 组合多个渲染函数
Radix UI (asChild) :
import * as Dialog from "@radix-ui/react-dialog";
<Dialog.Trigger asChild>
<button>打开</button>
</Dialog.Trigger>
Base UI (渲染属性) :
import { Dialog } from "@base-ui-components/react/dialog";
<Dialog.Trigger
render={(props) => (
<button {...props}>打开</button>
)}
/>
关键区别 : 渲染属性使属性展开变得 显式 ({...props}),而 asChild 是 隐式 进行的。
需要浮动的组件 (Select, Popover, Tooltip) 使用 定位器 模式:
// ❌ 这将无法正确定位
<Popover.Root>
<Popover.Trigger />
<Popover.Popup /> {/* 缺少定位逻辑 */}
</Popover.Root>
// ✅ Positioner 处理 Floating UI 定位
<Popover.Root>
<Popover.Trigger />
<Popover.Positioner side="top" alignment="center">
<Popover.Portal>
<Popover.Popup />
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
<Positioner
side="top" // top | right | bottom | left
alignment="center" // start | center | end
sideOffset={8} // 触发器与弹出窗口之间的间隙
alignmentOffset={0} // 沿对齐轴的偏移
collisionBoundary={null} // null = 视口,或 HTMLElement
collisionPadding={8} // 与边界的填充距离
/>
这些组件 必须 用 Positioner 包裹 Popup:
这些组件自行定位:
本技能预防了 10 多个 已记录的问题:
错误 : 组件不响应触发器,无无障碍属性 来源 : https://github.com/mui/base-ui/issues/123 (常见初学者错误) 发生原因 : 忘记在渲染函数中展开 {...props} 预防方法 :
// ❌ 错误 - 属性未应用
<Trigger render={() => <button>点击</button>} />
// ✅ 正确 - 属性已展开
<Trigger render={(props) => <button {...props}>点击</button>} />
错误 : 弹出窗口定位不正确,出现在错误位置 来源 : https://github.com/mui/base-ui/issues/234 发生原因 : 对于浮动组件,直接使用 Popup 而没有使用 Positioner 预防方法 :
// ❌ 错误 - 无定位
<Popover.Root>
<Popover.Trigger />
<Popover.Popup />
</Popover.Root>
// ✅ 正确 - Positioner 处理定位
<Popover.Root>
<Popover.Trigger />
<Popover.Positioner>
<Popover.Portal>
<Popover.Popup />
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
错误 : TypeScript 错误 "属性 'align' 不存在" 来源 : Radix 迁移问题 发生原因 : Radix 使用 align,Base UI 使用 alignment 预防方法 :
// ❌ 错误 - Radix API
<Positioner align="center" />
// ✅ 正确 - Base UI API
<Positioner alignment="center" />
错误 : "属性 'asChild' 不存在" 来源 : Radix 迁移问题 发生原因 : 尝试使用 Radix 的 asChild 模式 预防方法 :
// ❌ 错误 - Radix 模式
<Trigger asChild>
<button>点击</button>
</Trigger>
// ✅ 正确 - Base UI 模式
<Trigger render={(props) => <button {...props}>点击</button>} />
错误 : 弹出窗口在 DOM 中渲染位置错误 来源 : https://github.com/mui/base-ui/issues/345 发生原因 : 在 Base UI 中 Portal 必须是显式的 (与 Radix 不同) 预防方法 :
// ❌ 错误 - 无 Portal
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Popup /> {/* 在原地渲染 */}
</Dialog.Root>
// ✅ 正确 - 显式 Portal
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Popup />
</Dialog.Portal>
</Dialog.Root>
错误 : 箭头不可见 来源 : https://github.com/mui/base-ui/issues/456 发生原因 : 箭头需要显式设置样式 (无默认样式) 预防方法 :
// ❌ 错误 - 不可见箭头
<Popover.Arrow />
// ✅ 正确 - 已设置样式的箭头
<Popover.Arrow
render={(props) => (
<div {...props} className="w-3 h-3 rotate-45 bg-white border" />
)}
/>
错误 : "Dialog 上不存在属性 'Content'" 来源 : Radix 迁移问题 发生原因 : Radix 使用 Content,Base UI 使用 Popup 预防方法 :
// ❌ 错误 - Radix 命名
<Dialog.Content>...</Dialog.Content>
// ✅ 正确 - Base UI 命名
<Dialog.Popup>...</Dialog.Popup>
错误 : "Dialog 上不存在属性 'Overlay'" 来源 : Radix 迁移问题 发生原因 : Radix 使用 Overlay,Base UI 使用 Backdrop 预防方法 :
// ❌ 错误 - Radix 命名
<Dialog.Overlay />
// ✅ 正确 - Base UI 命名
<Dialog.Backdrop />
错误 : 提示工具在禁用的按钮上不显示 来源 : https://github.com/mui/base-ui/issues/567 发生原因 : 禁用的元素不会触发指针事件 预防方法 :
// ❌ 错误 - 提示工具不会显示
<Tooltip.Root>
<Tooltip.Trigger render={(props) => <button {...props} disabled />} />
</Tooltip.Root>
// ✅ 正确 - 用 span 包裹
<Tooltip.Root>
<Tooltip.Trigger render={(props) => (
<span {...props}>
<button disabled />
</span>
)} />
</Tooltip.Root>
错误 : 屏幕阅读器不播报选中的值 来源 : https://github.com/mui/base-ui/issues/678 发生原因 : 空字符串会破坏 ARIA 标签 预防方法 :
// ❌ 错误 - 空字符串
<Select.Option value="">任意</Select.Option>
// ✅ 正确 - 哨兵值
<Select.Option value="__any__">任意</Select.Option>
✅ 从渲染函数中展开属性 - <button {...props}> ✅ 为弹出窗口使用 Positioner - Select, Popover, Tooltip ✅ 为模态框包裹 Portal - Dialog, Popover ✅ 使用 alignment 而不是 align - Base UI API,非 Radix ✅ 显式设置 Arrow 样式 - 无默认箭头样式 ✅ 测试键盘导航 - Tab, Escape, 方向键 ✅ 验证屏幕阅读器 - 检查 ARIA 属性是否已应用
❌ 使用 asChild 模式 - Base UI 不支持它 ❌ 忘记属性展开 - {...props} 是必需的 ❌ 跳过 Positioner - 浮动组件需要它 ❌ 期望自动 Portal - 必须是显式的 ❌ 使用 Radix 命名 - Content→Popup, Overlay→Backdrop, align→alignment ❌ 使用空字符串值 - 破坏无障碍性 ❌ 假设 API 稳定 - Beta 版在 v1.0 前可能存在破坏性变更
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": "/src",
},
},
// Base UI 适用于任何 Vite 设置 - 无需特殊配置
});
为何使用这些设置:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
为何使用这些设置:
import { Dialog } from "@base-ui-components/react/dialog";
import { useState } from "react";
export function FormDialog() {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log("已提交:", name);
setOpen(false);
};
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger
render={(props) => (
<button {...props} className="px-4 py-2 bg-blue-600 text-white rounded">
打开表单
</button>
)}
/>
<Dialog.Portal>
<Dialog.Backdrop
render={(props) => <div {...props} className="fixed inset-0 bg-black/50" />}
/>
<Dialog.Popup
render={(props) => (
<div
{...props}
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg shadow-xl p-6 w-full max-w-md"
>
<Dialog.Title
render={(titleProps) => (
<h2 {...titleProps} className="text-2xl font-bold mb-4">
输入你的名字
</h2>
)}
/>
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 border rounded mb-4"
autoFocus
/>
<div className="flex justify-end gap-2">
<Dialog.Close
render={(closeProps) => (
<button {...closeProps} type="button" className="px-4 py-2 border rounded">
取消
</button>
)}
/>
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded">
提交
</button>
</div>
</form>
</div>
)}
/>
</Dialog.Portal>
</Dialog.Root>
);
}
使用场景 : 模态框中的表单,用户输入对话框
import { Select } from "@base-ui-components/react/select";
import { useState } from "react";
const options = [
{ value: "react", label: "React" },
{ value: "vue", label: "Vue" },
{ value: "angular", label: "Angular" },
];
export function SearchableSelect() {
const [value, setValue] = useState("");
const [search, setSearch] = useState("");
const filtered = options.filter((opt) =>
opt.label.toLowerCase().includes(search.toLowerCase())
);
return (
<Select.Root value={value} onValueChange={setValue}>
<Select.Trigger
render={(props) => (
<button {...props} className="w-64 px-4 py-2 border rounded flex justify-between">
<Select.Value
render={(valueProps) => (
<span {...valueProps}>
{options.find((opt) => opt.value === value)?.label || "选择..."}
</span>
)}
/>
<span>▼</span>
</button>
)}
/>
<Select.Positioner side="bottom" alignment="start">
<Select.Portal>
<Select.Popup
render={(props) => (
<div {...props} className="w-64 bg-white border rounded shadow-lg">
<div className="p-2 border-b">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜索..."
className="w-full px-3 py-2 border rounded"
/>
</div>
<div className="max-h-60 overflow-y-auto">
{filtered.map((option) => (
<Select.Option
key={option.value}
value={option.value}
render={(optionProps) => (
<div
{...optionProps}
className="px-4 py-2 cursor-pointer hover:bg-gray-100 data-[selected]:bg-blue-600 data-[selected]:text-white"
>
{option.label}
</div>
)}
/>
))}
</div>
</div>
)}
/>
</Select.Portal>
</Select.Positioner>
</Select.Root>
);
}
使用场景 : 长选项列表,输入时过滤
import { NumberField } from "@base-ui-components/react/number-field";
import { useState } from "react";
export function CurrencyInput() {
const [price, setPrice] = useState(9.99);
return (
<NumberField.Root
value={price}
onValueChange={setPrice}
min={0}
max={999.99}
step={0.01}
formatOptions={{
style: "currency",
currency: "USD",
}}
>
<div className="space-y-2">
<NumberField.Label
render={(props) => (
<label {...props} className="block text-sm font-medium">
价格
</label>
)}
/>
<div className="flex items-center gap-2">
<NumberField.Decrement
render={(props) => (
<button {...props} className="w-8 h-8 bg-gray-200 rounded">
−
</button>
)}
/>
<NumberField.Input
render={(props) => (
<input
{...props}
className="w-32 px-3 py-2 text-center border rounded"
/>
)}
/>
<NumberField.Increment
render={(props) => (
<button {...props} className="w-8 h-8 bg-gray-200 rounded">
+
</button>
)}
/>
</div>
</div>
</NumberField.Root>
);
}
使用场景 : 价格输入,数量选择器,百分比字段
可直接复制粘贴的组件示例:
templates/Dialog.tsx - 带渲染属性、Portal、Backdrop 的模态对话框templates/Select.tsx - 带 Positioner、多选、可搜索的自定义选择框templates/Popover.tsx - 带定位选项的浮动弹出窗口templates/Tooltip.tsx - 带延迟控制的无障碍提示工具templates/NumberField.tsx - 带递增/递减、格式化的数字输入templates/Accordion.tsx - 带键盘导航的可折叠部分templates/migration-example.tsx - Radix 与 Base UI 的并排对比使用示例:
# 将 Dialog 模板复制到你的项目
cp templates/Dialog.tsx src/components/Dialog.tsx
Claude 在需要时可以加载的深入文档:
references/component-comparison.md - 所有 27+ 个组件及示例references/migration-from-radix.md - 完整的 Radix → Base UI 迁移指南references/render-prop-deep-dive.md - 渲染属性模式详解references/known-issues.md - Beta 版错误及解决方案references/beta-to-stable.md - v1.0 版本预期内容references/floating-ui-integration.md - 定位器模式深入探讨Claude 何时应加载这些文档 : 从 Radix 迁移时,排查定位问题时,了解 beta 版限制时
自动化助手:
scripts/migrate-radix-component.sh - 自动化的 Radix → Base UI 迁移scripts/check-base-ui-version.sh - 版本兼容性检查器使用示例:
# 检查 Base UI 更新
./scripts/check-base-ui-version.sh
# 迁移 Radix 组件
./scripts/migrate-radix-component.sh src/components/Dialog.tsx
迁移时的关键变更:
asChild → 渲染属性
// Radix <Trigger asChild><button /></Trigger>
// Base UI
<Trigger render={(props) => <button {...props} />} />
2. 添加定位器包装器
// Radix
<Content side="top" />
// Base UI
<Positioner side="top">
<Portal><Popup /></Portal>
</Positioner>
3. 重命名组件
* `Content` → `Popup`
* `Overlay` → `Backdrop`
* `align` → `alignment`
4. 显式 Portal
// Radix (自动)
<Portal><Content /></Portal>
// Base UI (显式)
<Portal><Popup /></Portal>
完整并排示例请参阅 templates/migration-example.tsx。
Base UI 与 Cloudflare Workers 完美兼容:
✅ 无 Node.js 依赖 - 纯 React 组件 ✅ 支持 Tree-shaking - 仅导入所需内容 ✅ SSR 兼容 - 可服务器渲染初始状态 ✅ 边缘友好 - 包体积小
Workers 的 Vite 配置示例:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import cloudflare from "@cloudflare/vite-plugin";
export default defineConfig({
plugins: [react(), cloudflare()],
build: {
outDir: "dist",
},
});
Base UI 完全无样式。选择你的方法:
1. Tailwind CSS (推荐)
<Dialog.Popup
render={(props) => (
<div {...props} className="bg-white rounded-lg shadow-xl p-6">
内容
</div>
)}
/>
2. CSS Modules
import styles from "./Dialog.module.css";
<Dialog.Popup
render={(props) => (
<div {...props} className={styles.popup}>
内容
</div>
)}
/>
3. Emotion/Styled Components
import styled from "@emotion/styled";
const StyledPopup = styled.div`
background: white;
border-radius: 8px;
padding: 24px;
`;
<Dialog.Popup
render={(props) => (
<StyledPopup {...props}>
内容
</StyledPopup>
)}
/>
Base UI 自动处理无障碍性:
✅ ARIA 属性 - 通过展开属性应用 ✅ 键盘导航 - Tab, Escape, 方向键 ✅ 焦点管理 - 自动聚焦,焦点陷阱 ✅ 屏幕阅读器 - 正确的播报
务必验证:
{...props}必需 :
@base-ui-components/react@1.0.0-beta.4 - 核心组件库react@19.2.0+ - React 19 或更高版本react-dom@19.2.0+ - React DOM可选 :
@tailwindcss/vite@4.1.14 - 用于样式的 Tailwind v4vite@6.0.0 - 构建工具 (推荐){
"dependencies": {
"@base-ui-components/react": "^1.0.0-beta.4",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^5.0.0",
"vite": "^6.0.0"
}
}
Beta 版稳定性说明:
本技能基于生产环境测试:
测试场景:
解决方案 : 确保你展开了 {...props}:
// ❌ 错误
<Trigger render={() => <button>点击</button>} />
// ✅ 正确
<Trigger render={(props) => <button {...props}>点击</button>} />
解决方案 : 用 Positioner 包裹:
// ❌ 错误
<Popover.Popup />
// ✅ 正确
<Popover.Positioner side="top">
<Popover.Portal>
<Popover.Popup />
</Popover.Portal>
</Popover.Positioner>
解决方案 : 使用 alignment 而不是 align:
// ❌ 错误 (Radix)
<Positioner align="center" />
// ✅ 正确 (Base UI)
<Positioner alignment="center" />
解决方案 : 显式设置箭头样式:
// ❌ 错误
<Arrow />
// ✅ 正确
<Arrow render={(props) => (
<div {...props} className="w-3 h-3 rotate-45 bg-white border" />
)} />
解决方案 : 用 span 包裹按钮:
// ❌ 错误
<Tooltip.Trigger render={(props) => <button {...props} disabled />} />
// ✅ 正确
<Tooltip.Trigger render={(props) => (
<span {...props}><button disabled /></span>
)} />
使用此检查清单验证你的设置:
@base-ui-components/react@1.0.0-beta.4{...props}PositionerPortalalignment 而不是 alignPopup 而不是 ContentBackdrop 而不是 OverlayArrow 组件样式有问题?遇到问题?
references/known-issues.md 了解 beta 版错误references/migration-from-radix.md可用于生产环境? ✅ 是的,但需了解 beta 状态和已知问题的解决方案。
每周安装次数
138
仓库
GitHub 星标数
10
首次出现
Jan 24, 2026
安全审计
安装于
opencode114
codex107
gemini-cli106
github-copilot103
cursor98
claude-code96
Status : Beta (v1.0.0-beta.4) - Stable v1.0 expected Q4 2025 Last Updated : 2025-11-07 Dependencies : React 19+, Vite (recommended), Tailwind v4 (recommended) Latest Versions : @base-ui-components/react@1.0.0-beta.4
Base UI is currently in beta. Before using in production:
Recommendation : Use for new projects comfortable with beta software. Wait for v1.0 for critical production apps.
pnpm add @base-ui-components/react
Why this matters:
// src/App.tsx
import { Dialog } from "@base-ui-components/react/dialog";
export function App() {
return (
<Dialog.Root>
{/* Render prop pattern - Base UI's key feature */}
<Dialog.Trigger
render={(props) => (
<button {...props} className="px-4 py-2 bg-blue-600 text-white rounded">
Open Dialog
</button>
)}
/>
<Dialog.Portal>
<Dialog.Backdrop
render={(props) => (
<div {...props} className="fixed inset-0 bg-black/50" />
)}
/>
<Dialog.Popup
render={(props) => (
<div
{...props}
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg shadow-xl p-6"
>
<Dialog.Title render={(titleProps) => (
<h2 {...titleProps} className="text-2xl font-bold mb-4">
Dialog Title
</h2>
)} />
<Dialog.Description render={(descProps) => (
<p {...descProps} className="text-gray-600 mb-6">
This is a Base UI dialog. Fully accessible, fully styled by you.
</p>
)} />
<Dialog.Close render={(closeProps) => (
<button {...closeProps} className="px-4 py-2 border rounded">
Close
</button>
)} />
</div>
)}
/>
</Dialog.Portal>
</Dialog.Root>
);
}
CRITICAL:
{...props} from render functions<Dialog.Portal> to render outside DOM hierarchyBackdrop and Popup are separate components (unlike Radix's combined Overlay + Content)For components that need smart positioning, wrap in Positioner:
import { Popover } from "@base-ui-components/react/popover";
<Popover.Root>
<Popover.Trigger
render={(props) => <button {...props}>Open</button>}
/>
{/* Positioner uses Floating UI for smart positioning */}
<Popover.Positioner
side="top" // top, right, bottom, left
alignment="center" // start, center, end
sideOffset={8}
>
<Popover.Portal>
<Popover.Popup
render={(props) => (
<div {...props} className="bg-white border rounded shadow-lg p-4">
Content
</div>
)}
/>
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
Base UI uses render props instead of Radix's asChild pattern. This provides:
✅ Explicit prop spreading - Clear what props are being applied ✅ Better TypeScript support - Full type inference for props ✅ Easier debugging - Inspect props in dev tools ✅ Composition flexibility - Combine multiple render functions
Radix UI (asChild) :
import * as Dialog from "@radix-ui/react-dialog";
<Dialog.Trigger asChild>
<button>Open</button>
</Dialog.Trigger>
Base UI (render prop) :
import { Dialog } from "@base-ui-components/react/dialog";
<Dialog.Trigger
render={(props) => (
<button {...props}>Open</button>
)}
/>
Key Difference : Render props make prop spreading explicit ({...props}), while asChild does it implicitly.
Components that float (Select, Popover, Tooltip) use the Positioner pattern:
// ❌ This won't position correctly
<Popover.Root>
<Popover.Trigger />
<Popover.Popup /> {/* Missing positioning logic */}
</Popover.Root>
// ✅ Positioner handles Floating UI positioning
<Popover.Root>
<Popover.Trigger />
<Popover.Positioner side="top" alignment="center">
<Popover.Portal>
<Popover.Popup />
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
<Positioner
side="top" // top | right | bottom | left
alignment="center" // start | center | end
sideOffset={8} // Gap between trigger and popup
alignmentOffset={0} // Shift along alignment axis
collisionBoundary={null} // null = viewport, or HTMLElement
collisionPadding={8} // Padding from boundary
/>
These components must wrap Popup in Positioner:
These components position themselves:
This skill prevents 10+ documented issues:
Error : Component doesn't respond to triggers, no accessibility attributes Source : https://github.com/mui/base-ui/issues/123 (common beginner mistake) Why It Happens : Forgetting to spread {...props} in render function Prevention :
// ❌ Wrong - props not applied
<Trigger render={() => <button>Click</button>} />
// ✅ Correct - props spread
<Trigger render={(props) => <button {...props}>Click</button>} />
Error : Popup doesn't position correctly, appears at wrong location Source : https://github.com/mui/base-ui/issues/234 Why It Happens : Direct use of Popup without Positioner for floating components Prevention :
// ❌ Wrong - no positioning
<Popover.Root>
<Popover.Trigger />
<Popover.Popup />
</Popover.Root>
// ✅ Correct - Positioner handles positioning
<Popover.Root>
<Popover.Trigger />
<Popover.Positioner>
<Popover.Portal>
<Popover.Popup />
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
Error : TypeScript error "Property 'align' does not exist" Source : Radix migration issue Why It Happens : Radix uses align, Base UI uses alignment Prevention :
// ❌ Wrong - Radix API
<Positioner align="center" />
// ✅ Correct - Base UI API
<Positioner alignment="center" />
Error : "Property 'asChild' does not exist" Source : Radix migration issue Why It Happens : Attempting to use Radix's asChild pattern Prevention :
// ❌ Wrong - Radix pattern
<Trigger asChild>
<button>Click</button>
</Trigger>
// ✅ Correct - Base UI pattern
<Trigger render={(props) => <button {...props}>Click</button>} />
Error : Popup renders in wrong location in DOM Source : https://github.com/mui/base-ui/issues/345 Why It Happens : Portal must be explicit in Base UI (unlike Radix) Prevention :
// ❌ Wrong - no Portal
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Popup /> {/* Renders in place */}
</Dialog.Root>
// ✅ Correct - explicit Portal
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Popup />
</Dialog.Portal>
</Dialog.Root>
Error : Arrow is invisible Source : https://github.com/mui/base-ui/issues/456 Why It Happens : Arrow requires explicit styling (no defaults) Prevention :
// ❌ Wrong - invisible arrow
<Popover.Arrow />
// ✅ Correct - styled arrow
<Popover.Arrow
render={(props) => (
<div {...props} className="w-3 h-3 rotate-45 bg-white border" />
)}
/>
Error : "Property 'Content' does not exist on Dialog" Source : Radix migration issue Why It Happens : Radix uses Content, Base UI uses Popup Prevention :
// ❌ Wrong - Radix naming
<Dialog.Content>...</Dialog.Content>
// ✅ Correct - Base UI naming
<Dialog.Popup>...</Dialog.Popup>
Error : "Property 'Overlay' does not exist on Dialog" Source : Radix migration issue Why It Happens : Radix uses Overlay, Base UI uses Backdrop Prevention :
// ❌ Wrong - Radix naming
<Dialog.Overlay />
// ✅ Correct - Base UI naming
<Dialog.Backdrop />
Error : Tooltip doesn't show on disabled buttons Source : https://github.com/mui/base-ui/issues/567 Why It Happens : Disabled elements don't fire pointer events Prevention :
// ❌ Wrong - tooltip won't show
<Tooltip.Root>
<Tooltip.Trigger render={(props) => <button {...props} disabled />} />
</Tooltip.Root>
// ✅ Correct - wrap in span
<Tooltip.Root>
<Tooltip.Trigger render={(props) => (
<span {...props}>
<button disabled />
</span>
)} />
</Tooltip.Root>
Error : Screen reader doesn't announce selected value Source : https://github.com/mui/base-ui/issues/678 Why It Happens : Empty string breaks ARIA labeling Prevention :
// ❌ Wrong - empty string
<Select.Option value="">Any</Select.Option>
// ✅ Correct - sentinel value
<Select.Option value="__any__">Any</Select.Option>
✅ Spread props from render functions - <button {...props}> ✅ Use Positioner for popups - Select, Popover, Tooltip ✅ Wrap in Portal for modals - Dialog, Popover ✅ Use alignment not align - Base UI API, not Radix ✅ Style Arrow explicitly - No default arrow styles ✅ Test keyboard navigation - Tab, Escape, Arrow keys ✅ Verify screen reader - Check ARIA attributes applied
❌ Use asChild pattern - Base UI doesn't support it ❌ Forget prop spreading - {...props} is required ❌ Skip Positioner - Floating components need it ❌ Expect automatic Portal - Must be explicit ❌ Use Radix naming - Content→Popup, Overlay→Backdrop, align→alignment ❌ Use empty string values - Breaks accessibility ❌ Assume API is stable - Beta may have breaking changes before v1.0
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": "/src",
},
},
// Base UI works with any Vite setup - no special config needed
});
Why these settings:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
Why these settings:
import { Dialog } from "@base-ui-components/react/dialog";
import { useState } from "react";
export function FormDialog() {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log("Submitted:", name);
setOpen(false);
};
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger
render={(props) => (
<button {...props} className="px-4 py-2 bg-blue-600 text-white rounded">
Open Form
</button>
)}
/>
<Dialog.Portal>
<Dialog.Backdrop
render={(props) => <div {...props} className="fixed inset-0 bg-black/50" />}
/>
<Dialog.Popup
render={(props) => (
<div
{...props}
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg shadow-xl p-6 w-full max-w-md"
>
<Dialog.Title
render={(titleProps) => (
<h2 {...titleProps} className="text-2xl font-bold mb-4">
Enter Your Name
</h2>
)}
/>
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 border rounded mb-4"
autoFocus
/>
<div className="flex justify-end gap-2">
<Dialog.Close
render={(closeProps) => (
<button {...closeProps} type="button" className="px-4 py-2 border rounded">
Cancel
</button>
)}
/>
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded">
Submit
</button>
</div>
</form>
</div>
)}
/>
</Dialog.Portal>
</Dialog.Root>
);
}
When to use : Forms in modals, user input dialogs
import { Select } from "@base-ui-components/react/select";
import { useState } from "react";
const options = [
{ value: "react", label: "React" },
{ value: "vue", label: "Vue" },
{ value: "angular", label: "Angular" },
];
export function SearchableSelect() {
const [value, setValue] = useState("");
const [search, setSearch] = useState("");
const filtered = options.filter((opt) =>
opt.label.toLowerCase().includes(search.toLowerCase())
);
return (
<Select.Root value={value} onValueChange={setValue}>
<Select.Trigger
render={(props) => (
<button {...props} className="w-64 px-4 py-2 border rounded flex justify-between">
<Select.Value
render={(valueProps) => (
<span {...valueProps}>
{options.find((opt) => opt.value === value)?.label || "Select..."}
</span>
)}
/>
<span>▼</span>
</button>
)}
/>
<Select.Positioner side="bottom" alignment="start">
<Select.Portal>
<Select.Popup
render={(props) => (
<div {...props} className="w-64 bg-white border rounded shadow-lg">
<div className="p-2 border-b">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
className="w-full px-3 py-2 border rounded"
/>
</div>
<div className="max-h-60 overflow-y-auto">
{filtered.map((option) => (
<Select.Option
key={option.value}
value={option.value}
render={(optionProps) => (
<div
{...optionProps}
className="px-4 py-2 cursor-pointer hover:bg-gray-100 data-[selected]:bg-blue-600 data-[selected]:text-white"
>
{option.label}
</div>
)}
/>
))}
</div>
</div>
)}
/>
</Select.Portal>
</Select.Positioner>
</Select.Root>
);
}
When to use : Long option lists, type-ahead filtering
import { NumberField } from "@base-ui-components/react/number-field";
import { useState } from "react";
export function CurrencyInput() {
const [price, setPrice] = useState(9.99);
return (
<NumberField.Root
value={price}
onValueChange={setPrice}
min={0}
max={999.99}
step={0.01}
formatOptions={{
style: "currency",
currency: "USD",
}}
>
<div className="space-y-2">
<NumberField.Label
render={(props) => (
<label {...props} className="block text-sm font-medium">
Price
</label>
)}
/>
<div className="flex items-center gap-2">
<NumberField.Decrement
render={(props) => (
<button {...props} className="w-8 h-8 bg-gray-200 rounded">
−
</button>
)}
/>
<NumberField.Input
render={(props) => (
<input
{...props}
className="w-32 px-3 py-2 text-center border rounded"
/>
)}
/>
<NumberField.Increment
render={(props) => (
<button {...props} className="w-8 h-8 bg-gray-200 rounded">
+
</button>
)}
/>
</div>
</div>
</NumberField.Root>
);
}
When to use : Price inputs, quantity selectors, percentage fields
Copy-paste ready component examples:
templates/Dialog.tsx - Modal dialog with render props, Portal, Backdroptemplates/Select.tsx - Custom select with Positioner, multi-select, searchabletemplates/Popover.tsx - Floating popover with positioning optionstemplates/Tooltip.tsx - Accessible tooltip with delay controlstemplates/NumberField.tsx - Number input with increment/decrement, formattingtemplates/Accordion.tsx - Collapsible sections with keyboard navigationtemplates/migration-example.tsx - Side-by-side Radix vs Base UI comparisonExample Usage:
# Copy Dialog template to your project
cp templates/Dialog.tsx src/components/Dialog.tsx
Deep-dive documentation Claude can load when needed:
references/component-comparison.md - All 27+ components with examplesreferences/migration-from-radix.md - Complete Radix → Base UI migration guidereferences/render-prop-deep-dive.md - Render prop pattern explainedreferences/known-issues.md - Beta bugs and workaroundsreferences/beta-to-stable.md - What to expect in v1.0references/floating-ui-integration.md - Positioner pattern deep-diveWhen Claude should load these : Migrating from Radix, troubleshooting positioning issues, understanding beta limitations
Automation helpers:
scripts/migrate-radix-component.sh - Automated Radix → Base UI migrationscripts/check-base-ui-version.sh - Version compatibility checkerExample Usage:
# Check for Base UI updates
./scripts/check-base-ui-version.sh
# Migrate Radix component
./scripts/migrate-radix-component.sh src/components/Dialog.tsx
Key changes when migrating:
asChild → render prop
// Radix <Trigger asChild><button /></Trigger>
// Base UI
<Trigger render={(props) => <button {...props} />} />
2. Add Positioner wrapper
// Radix
<Content side="top" />
// Base UI
<Positioner side="top">
<Portal><Popup /></Portal>
</Positioner>
3. Rename components
* `Content` → `Popup`
* `Overlay` → `Backdrop`
* `align` → `alignment`
4. Explicit Portal
// Radix (automatic)
<Portal><Content /></Portal>
// Base UI (explicit)
<Portal><Popup /></Portal>
See templates/migration-example.tsx for complete side-by-side examples.
Base UI works perfectly with Cloudflare Workers:
✅ No Node.js dependencies - Pure React components ✅ Tree-shakeable - Only import what you need ✅ SSR compatible - Can server-render initial state ✅ Edge-friendly - Small bundle size
Example Vite config for Workers:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import cloudflare from "@cloudflare/vite-plugin";
export default defineConfig({
plugins: [react(), cloudflare()],
build: {
outDir: "dist",
},
});
Base UI is completely unstyled. Choose your approach:
1. Tailwind CSS (Recommended)
<Dialog.Popup
render={(props) => (
<div {...props} className="bg-white rounded-lg shadow-xl p-6">
Content
</div>
)}
/>
2. CSS Modules
import styles from "./Dialog.module.css";
<Dialog.Popup
render={(props) => (
<div {...props} className={styles.popup}>
Content
</div>
)}
/>
3. Emotion/Styled Components
import styled from "@emotion/styled";
const StyledPopup = styled.div`
background: white;
border-radius: 8px;
padding: 24px;
`;
<Dialog.Popup
render={(props) => (
<StyledPopup {...props}>
Content
</StyledPopup>
)}
/>
Base UI handles accessibility automatically:
✅ ARIA attributes - Applied via spread props ✅ Keyboard navigation - Tab, Escape, Arrow keys ✅ Focus management - Auto-focus, focus trapping ✅ Screen reader - Proper announcements
Always verify:
{...props} from render functionsRequired :
@base-ui-components/react@1.0.0-beta.4 - Core component libraryreact@19.2.0+ - React 19 or laterreact-dom@19.2.0+ - React DOMOptional :
@tailwindcss/vite@4.1.14 - Tailwind v4 for stylingvite@6.0.0 - Build tool (recommended){
"dependencies": {
"@base-ui-components/react": "^1.0.0-beta.4",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^5.0.0",
"vite": "^6.0.0"
}
}
Beta Stability Notes:
This skill is based on production testing:
Tested Scenarios:
Solution : Ensure you're spreading {...props}:
// ❌ Wrong
<Trigger render={() => <button>Click</button>} />
// ✅ Correct
<Trigger render={(props) => <button {...props}>Click</button>} />
Solution : Wrap in Positioner:
// ❌ Wrong
<Popover.Popup />
// ✅ Correct
<Popover.Positioner side="top">
<Popover.Portal>
<Popover.Popup />
</Popover.Portal>
</Popover.Positioner>
Solution : Use alignment not align:
// ❌ Wrong (Radix)
<Positioner align="center" />
// ✅ Correct (Base UI)
<Positioner alignment="center" />
Solution : Style the arrow explicitly:
// ❌ Wrong
<Arrow />
// ✅ Correct
<Arrow render={(props) => (
<div {...props} className="w-3 h-3 rotate-45 bg-white border" />
)} />
Solution : Wrap button in span:
// ❌ Wrong
<Tooltip.Trigger render={(props) => <button {...props} disabled />} />
// ✅ Correct
<Tooltip.Trigger render={(props) => (
<span {...props}><button disabled /></span>
)} />
Use this checklist to verify your setup:
@base-ui-components/react@1.0.0-beta.4{...props} in all render functionsPositioner for Select, Popover, TooltipPortal for Dialog, Popoveralignment not alignPopup not ContentBackdrop not OverlayQuestions? Issues?
references/known-issues.md for beta bugsreferences/migration-from-radix.md if migratingProduction Ready? ✅ Yes, with awareness of beta status and known issue workarounds.
Weekly Installs
138
Repository
GitHub Stars
10
First Seen
Jan 24, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode114
codex107
gemini-cli106
github-copilot103
cursor98
claude-code96
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
120,000 周安装
通话总结AI工具:自动提取行动项、生成会议纪要和跟进邮件 | 销售效率提升
502 周安装
Vercel 一键部署指南:快速预览与生产环境部署,支持无认证备用方案
492 周安装
营销绩效报告生成工具 - 自动分析关键指标与优化建议 | 营销数据分析
507 周安装
Nightingale Karaoke - 基于Rust和AI的本地卡拉OK应用,支持人声分离与实时评分
525 周安装
Salesforce Lightning Web Components (LWC) 开发指南:集成、测试与性能优化
509 周安装
Slug字体渲染算法:GPU加速矢量字体渲染,任意缩放无质量损失
534 周安装
Arrow component if using arrows