Coolify 前端组件指南:shadcn/ui 中 base 与 radix 两套原语库的 API 差异详解
【免费下载链接】coolifyAn open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers.项目地址: https://gitcode.com/GitHub_Trending/co/coolify
Coolify 仓库在.agents/skills/shadcn/rules/base-vs-radix.md中维护了一份针对 AI/开发协作的规则文档,专门记录 shadcn/ui 项目使用base原语库(Base UI)与radix原语库(Radix UI)时,组件 API 的系统性差异。本文完整继承并展开该文档的全部规则:从组合模式(asChildvsrender)、nativeButton修正,到 Select、ToggleGroup、Slider、Accordion 四大组件的逐项 API 对照,帮助你在新建或维护 shadcn/ui 项目时,先确认项目所属原语库,再写出符合该库语义的正确组件代码。
1. 先判断项目属于哪套原语库:base字段
两套 API 的差异是"同组件、不同签名"级别的问题,同一份代码直接照搬另一套库会编译报错或行为异常,因此第一步永远是确认当前项目的原语库。
规则文档的第一句话即给出了方法:检查npx shadcn@latest info输出中的base字段。配套的 CLI 参考文档(cli.md)对info命令做了更完整的说明:
info命令读取项目根目录的components.json,输出项目信息与配置字段;components.json字段表中的base字段被明确定义为:原语库(radix或base),决定组件 API 与可用 props;- 其他相关字段还包括
style(视觉风格)、iconLibrary(图标库)、tailwindVersion(v3/v4)、resolvedPaths(各别名的绝对文件系统路径)等。
npx shadcn@latest info技能主文件 SKILL.md 在"Component Structure"规则中直接引用了本文档作为强制规则:
使用
asChild(radix)或render(base)做自定义 trigger;通过npx shadcn@latest info检查base字段。
此外,docs命令的输出也会以base radix的列头展示各组件在不同原语库下的文档与示例位置,这从侧面印证了 shadcn/ui 官方对两套库分别维护文档的机制。
2. 组合模式:asChild(radix)vsrender(base)
这是两套库最基础、出现频率最高的差异:Radix 用asChild属性来"替换"默认渲染的元素;Base 用render属性传入元素。通用原则是:不要把 trigger 再包一层多余的元素。
错误写法(两套库均适用)——trigger 内嵌套了多余div:
<DialogTrigger> <div> <Button>Open</Button> </div> </DialogTrigger>正确写法(radix):
<DialogTrigger asChild> <Button>Open</Button> </DialogTrigger>正确写法(base):
<DialogTrigger render={<Button />}>Open</DialogTrigger>注意两者的语义差别:radix 的asChild让 trigger 把属性"合并"进唯一子元素;base 的render则是直接接收一个 React 元素作为渲染目标,文本作为 children 传入。这一条规则适用于文档列出的全部 trigger/close 类组件:
DialogTrigger、SheetTrigger、AlertDialogTrigger、DropdownMenuTrigger、PopoverTrigger、TooltipTrigger、CollapsibleTrigger、DialogClose、SheetClose、NavigationMenuLink、BreadcrumbLink、SidebarMenuButton、Badge、Item。
3. 把 Button / trigger 渲染成非按钮元素(base 专属陷阱)
当render把一个元素改成非按钮元素(如<a>、<span>)时,base 库必须额外加上nativeButton={false},否则底层仍按原生<button>处理,导致出现<button>包裹<a>这类无效嵌套。
错误写法(base)——缺少nativeButton={false}:
<Button render={<a href="/docs" />}>Read the docs</Button>正确写法(base):
<Button render={<a href="/docs" />} nativeButton={false}> Read the docs </Button>正确写法(radix)——等价场景用asChild,无此属性:
<Button asChild> <a href="/docs">Read the docs</a> </Button>同样的规则适用于render目标不是Button的 trigger,例如把 Popover 的 trigger 渲染成输入组的附加元素:
// base. <PopoverTrigger render={<InputGroupAddon />} nativeButton={false}> Pick date </PopoverTrigger>4. Select:itemsprop、占位符与内容定位
4.1itemsprop(base 专属)
Base 要求根组件传入items数组作为数据源;Radix 只使用内联 JSX,没有这个概念。
错误写法(base)——漏传items:
<Select> <SelectTrigger><SelectValue placeholder="Select a fruit" /></SelectTrigger> </Select>正确写法(base):
const items = [ { label: "Select a fruit", value: null }, { label: "Apple", value: "apple" }, { label: "Banana", value: "banana" }, ] <Select items={items}> <SelectTrigger> <SelectValue /> </SelectTrigger> <SelectContent> <SelectGroup> {items.map((item) => ( <SelectItem key={item.value} value={item.value}>{item.label}</SelectItem> ))} </SelectGroup> </SelectContent> </Select>正确写法(radix)——内联声明选项:
<Select> <SelectTrigger> <SelectValue placeholder="Select a fruit" /> </SelectTrigger> <SelectContent> <SelectGroup> <SelectItem value="apple">Apple</SelectItem> <SelectItem value="banana">Banana</SelectItem> </SelectGroup> </SelectContent> </Select>注意两个写法的共同点:SelectItem必须位于SelectGroup之内。这是仓库中另一条强制规则——"Items 必须放在各自的 Group 内"(见 composition.md,SelectItem→SelectGroup是其列举的第一行),与本文档互相印证。
4.2 占位符(Placeholder)的实现方式不同
- base:占位符是
items数组中value: null的那一项(如上面的{ label: "Select a fruit", value: null }); - radix:使用
<SelectValue placeholder="...">属性表达。
4.3 下拉内容的定位属性
- base用
alignItemWithTrigger; - radix用
position。
// base. <SelectContent alignItemWithTrigger={false} side="bottom"> // radix. <SelectContent position="popper">5. Select:多选与对象值(base 专属能力)
文档明确指出:Base 支持multiple、SelectValue的 render-function children、以及通过itemToStringValue处理对象值;而Radix 的 Select 是单选、且仅支持字符串值。也就是说,多选与对象值场景在 base 库是"一等公民",在 radix 库需要换组件或自行封装。
base 多选示例:
<Select items={items} multiple defaultValue={[]}> <SelectTrigger> <SelectValue> {(value: string[]) => value.length === 0 ? "Select fruits" : `${value.length} selected`} </SelectValue> </SelectTrigger> ... </Select>这里SelectValue的 children 是一个函数,参数为当前选中的值数组,可在其中渲染任意摘要文本(如"已选 N 项")。
base 对象值示例(选中项是对象时用itemToStringValue提取字符串身份,用 render function 展示展示字段):
<Select defaultValue={plans[0]} itemToStringValue={(plan) => plan.name}> <SelectTrigger> <SelectValue>{(value) => value.name}</SelectValue> </SelectTrigger> ... </Select>6. ToggleGroup:typevsmultiple
Base 使用multiple布尔属性表达多选;Radix 使用type="single"或type="multiple"枚举属性。更隐蔽的差异在于defaultValue的类型:base 的defaultValue始终是数组,radix 的单选defaultValue是字符串。
错误写法(base)——误用了 radix 的type="single"与字符串默认值:
<ToggleGroup type="single" defaultValue="daily"> <ToggleGroupItem value="daily">Daily</ToggleGroupItem> </ToggleGroup>正确写法(base):
// 单选(不需要任何 prop),defaultValue 始终是数组。 <ToggleGroup defaultValue={["daily"]} spacing={2}> <ToggleGroupItem value="daily">Daily</ToggleGroupItem> <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem> </ToggleGroup> // 多选。 <ToggleGroup multiple> <ToggleGroupItem value="bold">Bold</ToggleGroupItem> <ToggleGroupItem value="italic">Italic</ToggleGroupItem> </ToggleGroup>正确写法(radix):
// 单选,defaultValue 是字符串。 <ToggleGroup type="single" defaultValue="daily" spacing={2}> <ToggleGroupItem value="daily">Daily</ToggleGroupItem> <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem> </ToggleGroup> // 多选。 <ToggleGroup type="multiple"> <ToggleGroupItem value="bold">Bold</ToggleGroupItem> <ToggleGroupItem value="italic">Italic</ToggleGroupItem> </ToggleGroup>受控单选值的差异——base 需要在状态与回调之间手动"包裹/解包"数组:
// base —— 数组的 wrap/unwrap。 const [value, setValue] = React.useState("normal") <ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}> // radix —— 直接是普通字符串。 const [value, setValue] = React.useState("normal") <ToggleGroup type="single" value={value} onValueChange={setValue}>这一条是迁移代码时最容易遗漏的坑:状态值在 base 中是string[],在 radix 中是string,回调签名随之不同。
7. Slider:标量 vs 数组
Base 的单滑块接受普通数字;Radix 的defaultValue一律是数组。
错误写法(base)——把 radix 的数组习惯带过来:
<Slider defaultValue={[50]} max={100} step={1} />正确写法(base):
<Slider defaultValue={50} max={100} step={1} />正确写法(radix):
<Slider defaultValue={[50]} max={100} step={1} />两套库在 range(双滑块)场景都使用数组。但 base 的受控onValueChange回调可能需要一次类型断言:
// base. const [value, setValue] = React.useState([0.3, 0.7]) <Slider value={value} onValueChange={(v) => setValue(v as number[])} /> // radix. const [value, setValue] = React.useState([0.3, 0.7]) <Slider value={value} onValueChange={setValue} />8. Accordion:type/collapsiblevsmultiple
Radix 要求type="single"或type="multiple",并支持collapsible;defaultValue是字符串。Base 没有typeprop,用multiple布尔值表达多选,且defaultValue始终是数组。
错误写法(base)——照搬 radix 的type="single" collapsible与字符串默认值:
<Accordion type="single" collapsible defaultValue="item-1"> <AccordionItem value="item-1">...</AccordionItem> </Accordion>正确写法(base):
<Accordion defaultValue={["item-1"]}> <AccordionItem value="item-1">...</AccordionItem> </Accordion> // 多选。 <Accordion multiple defaultValue={["item-1", "item-2"]}> <AccordionItem value="item-1">...</AccordionItem> <AccordionItem value="item-2">...</AccordionItem> </Accordion>正确写法(radix):
<Accordion type="single" collapsible defaultValue="item-1"> <AccordionItem value="item-1">...</AccordionItem> </Accordion>9. 差异速查表
| 主题 | base(Base UI) | radix(Radix UI) |
|---|---|---|
| 组合/自定义元素 | render={<Button />} | asChild+ 子元素 |
非按钮渲染(<a>/<span>) | 需加nativeButton={false} | 无此概念,直接asChild |
| Select 数据源 | 根组件必须传itemsprop | 内联 JSX 选项 |
| Select 占位符 | items中value: null项 | <SelectValue placeholder="..." /> |
| Select 定位 | alignItemWithTrigger | position="popper" |
| Select 多选/对象值 | 支持multiple、render-functionSelectValue、itemToStringValue | 单选、仅字符串值 |
| ToggleGroup 模式 | 无 prop = 单选;multiple布尔 | type="single"/type="multiple" |
| ToggleGroup 默认值 | 始终是数组 | 单选为字符串 |
| Slider 单滑块 | defaultValue={50}标量 | defaultValue={[50]}数组 |
| Slider 受控回调 | 可能需要as number[]断言 | 直接可用 |
| Accordion 模式 | 无type,multiple布尔 + 数组默认值 | type="single"/"multiple"+collapsible+ 字符串默认值 |
10. 与仓库内其他规则文档的关系
本文档并非孤立存在,它是 shadcn 技能包中一组强制规则的成员,全部规则文件位于.agents/skills/shadcn/rules/目录:
- base-vs-radix.md(本文主题)——
asChildvsrender、Select、ToggleGroup、Slider、Accordion 的 API 差异; - composition.md——Group/Item 结构、overlay 组件选择、Card/Tabs/Avatar 等组合规则;
- forms.md——FieldGroup/Field、InputGroup、ToggleGroup 在表单中的用法;
- styling.md——语义色、
gap-*、size-*、cn()等样式规则; - icons.md——
data-icon与图标尺寸规则。
技能主文件 SKILL.md 把上述文件列为"always enforced"的 Critical Rules,其中与本文直接挂钩的强制条目是:"UseasChild(radix) orrender(base) for custom triggers. Checkbasefield fromnpx shadcn@latest info"。此外 SKILL.md 还提到:预设(preset)代码并不编码 base 信息,CLI 会自动从components.json保留当前项目的 base;若必须在临时目录做--dry-run对比,需显式传--base <current-base>。从这套文档的组织方式可以推断:base字段是整个 shadcn/ui 工程约定中决定"组件签名"的开关,本文覆盖的所有差异都应以它为前提先做判定,再进入具体组件的 API 选择。
【免费下载链接】coolifyAn open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers.项目地址: https://gitcode.com/GitHub_Trending/co/coolify
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考