首页
/ shadcn/ui 的 Base 与 Radix 两套组件 API 差异实战:render、items 与 defaultValue 的正确写法

shadcn/ui 的 Base 与 Radix 两套组件 API 差异实战:render、items 与 defaultValue 的正确写法

2026-09-04 17:38:36作者:霍妲思

shadcn/ui 仓库在组件注册表中同时维护了基于 Base UI(base)与基于 Radix UI(radix)的两套并行组件实现,两者的组合方式(asChild vs render)、Select 的数据驱动模型、以及 defaultValue 的值类型均存在系统性差异。本文以仓库中的 base-vs-radix 规则文档 为主体,完整覆盖其全部差异点与正误代码对照,并结合 CLI 源码与注册表目录结构说明如何确认当前项目属于哪一套 API,从而避免在迁移或跨项目复用组件代码时写出编译不通过或行为不符预期的 JSX。

先确认你的项目用的是哪一套:npx shadcn infobase 字段

规则文档的开篇要求:先运行 npx shadcn@latest info,查看输出中的 base 字段,再决定按哪一套 API 编写代码。该字段由 CLI 的 info 命令 生成:

// packages/shadcn/src/commands/info.ts
const config = await getConfig(cwd)
const base = getBase(config?.style)

base 的取值逻辑实现在 getBase 函数

// packages/shadcn/src/utils/get-config.ts
export function getBase(style: string | undefined): PresetBase {
  // An undefined style means no existing config, so default to base.
  // Any defined style, including empty and unprefixed legacy values
  // (new-york, new-york-v4, default), stays radix.
  if (style === undefined) {
    return "base"
  }

  return parsePresetStyle(style).base ?? "radix"
}

从源码结构看,可以得出两条实用结论:

  • 没有 components.json 配置(全新项目)时默认归入 base
  • 任何已定义的 style(包括 new-yorknew-york-v4 等遗留值)都会停留在 radix

也就是说,base / radix 不是两个可自由混搭的组件库,而是同一项目必须整体选定的两套 API。仓库中这两套实现分别位于 apps/v4/registry/bases/baseapps/v4/registry/bases/radix 两个并行目录,注册表 README 明确要求:对任何共享表面(同样的预览块、同样的示例意图),变更应同时应用到两套变体,只调整必须不同的部分——导入路径(.../base/ui/....../radix/ui/...)和原语 API。这正解释了为什么本文列出的每一条差异都必须“成对记忆”:同名的 DialogTriggerSelectAccordion 在两个目录下的 props 签名并不相同。

组合方式:radix 用 asChild,base 用 render

两套 API 最基础、也最普遍的区别在于“替换默认元素”的组合机制:Radix 使用 asChild 把默认元素替换为子元素;Base 使用 render prop 声明渲染成什么元素。无论哪一套,都不要用多余的包裹元素套住 trigger。

错误写法(两种体系都错):

<DialogTrigger>
  <div>
    <Button>Open</Button>
  </div>
</DialogTrigger>

正确写法(radix):

<DialogTrigger asChild>
  <Button>Open</Button>
</DialogTrigger>

正确写法(base):

<DialogTrigger render={<Button />}>Open</DialogTrigger>

注意两者子内容的语义差异:radix 的 asChild 是“用唯一子元素替换自身”,子元素即触发按钮;base 的 render 是“把自身渲染为指定元素”,触发文案作为 children 传入。这个差异在 base 注册表的组件源码中随处可见,例如 alert-dialog.tsx

<AlertDialogPrimitive.Cancel
  data-slot="alert-dialog-cancel"
  className={cn("cn-alert-dialog-cancel", className)}
  render={<Button variant={variant} size={size} />}
  {...props}
/>

规则文档列出了该模式适用的全部 trigger 与 close 类组件:DialogTriggerSheetTriggerAlertDialogTriggerDropdownMenuTriggerPopoverTriggerTooltipTriggerCollapsibleTriggerDialogCloseSheetCloseNavigationMenuLinkBreadcrumbLinkSidebarMenuButtonBadgeItem

Button / trigger 渲染为非 button 元素:base 必须加 nativeButton={false}

render 把元素改成非按钮语义的元素(<a><span>)时,base 体系要求显式传入 nativeButton={false},否则 Base UI 仍会按原生按钮语义处理(如阻止链接默认行为、保留 button 的无障碍属性)。

错误(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):

<Button asChild>
  <a href="/docs">Read the docs</a>
</Button>

同样的规则适用于 render 不是 Button 的 trigger 组件:

// base.
<PopoverTrigger render={<InputGroupAddon />} nativeButton={false}>
  Pick date
</PopoverTrigger>

判断口诀:只要渲染目标是 <a><span> 这类非 <button> 元素,base 体系就补一个 nativeButton={false};radix 体系没有这个 prop,用 asChild 即可。

Select 差异:items prop、placeholder 与定位

Select 是两套 API 差异最大的组件之一,共有四个差异点。

items prop:base 必须在根节点声明

base 体系要求根 Select 携带 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)——纯内联 JSX:

<Select>
  <SelectTrigger>
    <SelectValue placeholder="Select a fruit" />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectItem value="apple">Apple</SelectItem>
      <SelectItem value="banana">Banana</SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>

placeholder:base 用 value: null 项,radix 用 placeholder prop

  • base:在 items 数组里放一个 { label: "...", value: null } 项,<SelectValue /> 不再需要 placeholder
  • radix:<SelectValue placeholder="Select a fruit" />

迁移时的典型错误就是把 radix 的 placeholder="..." 原样带到 base 版本,结果占位文案不显示——base 的占位是数据,不是 prop。

内容定位:alignItemWithTrigger vs position

两套体系对浮层定位的 prop 命名不同:

// base.
<SelectContent alignItemWithTrigger={false} side="bottom">

// radix.
<SelectContent position="popper">
  • base:alignItemWithTrigger={false} 表示列表不强制与选中项对齐;
  • radix:position="popper" 表示用 popper 定位(相对触发器),否则为 item-aligned

Select 多选与对象值:base 独有

base 体系支持 multipleSelectValue 的渲染函数 children,以及配合 itemToStringValue 的对象值;radix 体系的 Select 只支持单选且值只能是字符串。这两组能力在跨体系迁移时没有对等物,需要改用 Combobox 等其他组件替代(radix 侧多选通常由 Combobox 承担)。

base——多选:

<Select items={items} multiple defaultValue={[]}>
  <SelectTrigger>
    <SelectValue>
      {(value: string[]) => value.length === 0 ? "Select fruits" : `${value.length} selected`}
    </SelectValue>
  </SelectTrigger>
  ...
</Select>

base——对象值:

<Select defaultValue={plans[0]} itemToStringValue={(plan) => plan.name}>
  <SelectTrigger>
    <SelectValue>{(value) => value.name}</SelectValue>
  </SelectTrigger>
  ...
</Select>

两个要点:itemToStringValue 负责把对象值序列化为字符串(用于内部状态与无障碍播报);SelectValue 的 children 作为渲染函数接收当前值,可渲染对象上的任意字段。radix 体系两者皆无,遇到对象值场景不要硬套 Select

ToggleGroup:radix 用 type,base 用 multiple 布尔值

单选/多选的开关在两套体系中表达方式不同,且 base 的 defaultValue 永远是数组,radix 单选时是字符串。

错误(base)——误用 radix 的 type prop:

<ToggleGroup type="single" defaultValue="daily">
  <ToggleGroupItem value="daily">Daily</ToggleGroupItem>
</ToggleGroup>

正确(base):

// Single (no prop needed), defaultValue is always an array.
<ToggleGroup defaultValue={["daily"]} spacing={2}>
  <ToggleGroupItem value="daily">Daily</ToggleGroupItem>
  <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
</ToggleGroup>

// Multi-selection.
<ToggleGroup multiple>
  <ToggleGroupItem value="bold">Bold</ToggleGroupItem>
  <ToggleGroupItem value="italic">Italic</ToggleGroupItem>
</ToggleGroup>

正确(radix):

// Single, defaultValue is a string.
<ToggleGroup type="single" defaultValue="daily" spacing={2}>
  <ToggleGroupItem value="daily">Daily</ToggleGroupItem>
  <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
</ToggleGroup>

// Multi-selection.
<ToggleGroup type="multiple">
  <ToggleGroupItem value="bold">Bold</ToggleGroupItem>
  <ToggleGroupItem value="italic">Italic</ToggleGroupItem>
</ToggleGroup>

对照总结:

能力 base radix
单选 默认(无需 prop) type="single"
多选 multiple type="multiple"
defaultValue 类型 恒为数组 ["daily"] 单选为字符串 "daily"

受控单选值的差异最能体现“base 一切皆数组”的设计:

// base — wrap/unwrap arrays.
const [value, setValue] = React.useState("normal")
<ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}>

// radix — plain string.
const [value, setValue] = React.useState("normal")
<ToggleGroup type="single" value={value} onValueChange={setValue}>

base 侧需要在边界处做“数组包装 / 拆包”:value={[value]} 进、v[0] 出;radix 侧直接透传字符串。迁移受控组件时漏掉这层包装是典型故障点。

Slider:base 单滑块可传标量,radix 恒为数组

Base 对单滑块接受普通数字,Radix 一律要求数组(因为 Radix Slider 原生就是多滑块模型)。

错误(base)——给单滑块传数组:

<Slider defaultValue={[50]} max={100} step={1} />

正确(base):

<Slider defaultValue={50} max={100} step={1} />

正确(radix):

<Slider defaultValue={[50]} max={100} step={1} />

范围滑块(range slider)两边都用数组。受控的 onValueChange 在 base 下可能需要类型断言:

// 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} />

base 中 onValueChange 的回调参数类型同时覆盖标量与数组两种形态,因此范围滑块场景需要 v as number[] 断言后写入 state。

Accordion:type / collapsible(radix)vs multiple(base)

Radix 要求显式 type="single"type="multiple",并支持 collapsible(允许收起已展开项);defaultValue 是字符串。Base 没有 type prop,用 multiple 布尔值控制多选,defaultValue 永远是数组。

错误(base)——照搬 radix 的写法:

<Accordion type="single" collapsible defaultValue="item-1">
  <AccordionItem value="item-1">...</AccordionItem>
</Accordion>

正确(base):

<Accordion defaultValue={["item-1"]}>
  <AccordionItem value="item-1">...</AccordionItem>
</Accordion>

// Multi-select.
<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>

迁移与双目录维护:仓库给出的工程化支撑

上述逐组件差异之所以值得沉淀成规则文档,是因为仓库中存在大量从 radix 到 base 的存量迁移与并行维护工作。两点工程化事实可以作为佐证:

  1. 并行注册表结构apps/v4/registry/bases 目录下 base/radix/ 两套组件一一对应(如 base/ui/accordion.tsx 与 radix 侧同名文件),bases README 要求共享表面的变更同时落到两边,仅调整导入路径与原语 API。
  2. 迁移技能与映射表:仓库自带 migrate-radix-to-base 技能,并按组件类别拆分了 menus.mdoverlays.mdform-controls.md 等映射文档,与 shadcn 技能规则目录 互为配套;CLI 侧也提供 migrate 命令 供项目执行迁移。

对开发者而言,日常只需记住规则文档的判定路径:先看 info 输出的 base 字段确定体系,再按对应一侧的写法实现组件;跨项目复制粘贴代码时,把 asChild / rendertype / multiple、字符串 / 数组 defaultValueplaceholder / value: null 这四组差异当成必查清单。

速查表

场景 base(Base UI) radix(Radix UI)
元素替换 render={<Button />} asChild + 子元素
渲染为非 button 元素 追加 nativeButton={false} 无需额外 prop
Select 选项 根节点 items 数组 内联 SelectItem JSX
Select 占位 { label, value: null } <SelectValue placeholder="..." />
Select 定位 alignItemWithTrigger position="popper"
Select 多选 / 对象值 multipleitemToStringValue、渲染函数 children 不支持(仅单选字符串值)
ToggleGroup 多选 multiple type="multiple"
ToggleGroup 单选 默认 type="single"
ToggleGroup defaultValue 恒为数组 单选为字符串
Slider 单滑块值 标量 50 数组 [50]
Accordion 类型 typemultiple 布尔 type="single" | "multiple",支持 collapsible
Accordion defaultValue 恒为数组 单选为字符串

参考依据:规则文档 base-vs-radix.mdinfo 命令实现getBase 取值逻辑注册表并行结构说明 以及 base 侧组件源码(如 alert-dialog.tsxrender 用法)。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384