首页
/ shadcn/ui 组件组合规则详解:从 Group 嵌套、浮层选型到禁用自定义标记的完整实践

shadcn/ui 组件组合规则详解:从 Group 嵌套、浮层选型到禁用自定义标记的完整实践

2026-09-04 14:04:25作者:裘旻烁

本篇技术指南围绕 shadcn/ui 仓库中 composition.md 这份组件组合(Composition)规则文档展开,系统讲解 13 条“始终强制执行”的组合约定:Item 必须嵌套在 Group 中、浮层组件如何选型、Toast 如何跟随项目 base、Button 加载态为何不用 isPending 等。读完后你将掌握在 shadcn/ui 项目中写出结构正确、可访问、可复用代码的组合范式,并能直接对照仓库中的注册表源码验证每条规则的实现依据。

规则文档在项目中的位置

composition.md 是 shadcn Agent 技能(SKILL.md)“Critical Rules”(关键规则)体系的组成部分。在 SKILL.md 中,这份文档被两次引用:

  • Component Structure(组件结构):覆盖 Group 嵌套、Dialog/Sheet/Drawer 的 Title、Card 完整组合、Button 加载态、TabsTrigger 嵌套、AvatarFallback;
  • Use Components, Not Custom Markup(用组件而非自定义标记):覆盖 Alert、Empty、Toast、Separator、Skeleton、Badge。

规则文档自身以“Incorrect / Correct”代码对的格式给出反例与正例,属于可被 Agent 与人类开发者共同遵循的强制约定,而非建议性风格。仓库中的注册表组件源码(registry/bases/*registry/new-york-v4/*)则提供了每条规则落地的实现证据。

Items 必须嵌套在 Group 组件内

第一条规则是:永远不要将 item 直接渲染在内容容器内,必须包一层对应的 Group。

错误写法:

<SelectContent>
  <SelectItem value="apple">Apple</SelectItem>
  <SelectItem value="banana">Banana</SelectItem>
</SelectContent>

正确写法:

<SelectContent>
  <SelectGroup>
    <SelectItem value="apple">Apple</SelectItem>
    <SelectItem value="banana">Banana</SelectItem>
  </SelectGroup>
</SelectContent>

规则文档给出了完整的 Item → Group 映射表,适用于所有基于分组的组件:

Item Group
SelectItemSelectLabel SelectGroup
DropdownMenuItemDropdownMenuLabelDropdownMenuSub DropdownMenuGroup
MenubarItem MenubarGroup
ContextMenuItem ContextMenuGroup
CommandItem CommandGroup
MessageScrollerItem MessageScrollerContent
Message(连续、同一发送者) MessageGroup
Bubble(堆叠) BubbleGroup
Attachment(同一行) AttachmentGroup

对于聊天组件,嵌套顺序是固定的:MessageScrollerProviderMessageScrollerMessageScrollerViewportMessageScrollerContentMessageScrollerItem,更细的规则见 chat.md

仓库中的实现佐证了这一结构:聊天滚动组件的完整源码位于 message-scroller.tsx,其中 MessageScrollerItemMessageScrollerContent 作为独立导出的组合子部件存在,说明“Item 必须在 Content/Group 内”不是文档约定,而是组件本身的结构设计。

提示框(Callout)统一使用 Alert

页面中需要引起用户注意的提示信息,一律使用 Alert,不要手写带边框的 div

<Alert>
  <AlertTitle>Warning</AlertTitle>
  <AlertDescription>Something needs attention.</AlertDescription>
</Alert>

从注册表源码可以确认其组合结构与能力。以 alert.tsx 为例:

  • Alert 根元素渲染为 div,携带 role="alert"data-slot="alert",并支持 defaultdestructive 两个 variant,通过 CVA(class-variance-authority)管理样式;
  • AlertTitleAlertDescription 通过 col-start-2 等 CSS Grid 布局类实现标题/描述与图标的两列排布(根类中的 grid-cols-[0_1fr]has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] 表明传入子级 SVG 时自动出现图标列);
  • destructive 变体会将 [&>svg] 与描述文本一并染为 destructive 色。

这意味着使用 Alert 时,语义角色、变体切换、图标排布全部由组件承担,无需任何额外标记。

空状态(Empty state)使用 Empty 组件

空列表、无数据等场景不要拼凑自定义标记,统一使用 Empty 组合结构:

<Empty>
  <EmptyHeader>
    <EmptyMedia variant="icon"><FolderIcon /></EmptyMedia>
    <EmptyTitle>No projects yet</EmptyTitle>
    <EmptyDescription>Get started by creating a new project.</EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button>Create Project</Button>
  </EmptyContent>
</Empty>

结构上分为两部分:EmptyHeader 承载媒体(EmptyMedia,示例中使用 variant="icon")、标题与描述;EmptyContent 承载行动按钮等后续操作。该组件在注册表中以 empty.tsx 提供(Base UI 版),在 new-york-v4/ui/empty.tsx 中亦有对应实现,说明它是跨 base 的标准组件。

Toast 通知跟随项目的 base 选择

Toast 的实现取决于项目的 base 字段(可通过 npx shadcn@latest info 查看):

Base UI 项目使用 toast 组件,以命令式 API 添加通知:

import { toast } from "@/components/ui/toast"

toast.add({
  title: "Changes saved.",
})

Radix 与 React Aria 项目则使用 Sonner:

import { toast } from "sonner"

toast.success("Changes saved.")
toast.error("Something went wrong.")
toast("File deleted.", {
  action: { label: "Undo", onClick: () => undoDelete() },
})

仓库源码印证了两条路线的共存:

  • Base UI 路线的完整实现见 toast.tsx:文件顶部通过 ToastPrimitive.createToastManager() 创建 toast 单例,并导出 toastuseToastManagercreateToastManager,这正是文档中 toast.add(...) 命令式调用的来源;Toaster 组件内部组合了 ToastProviderToastPortalToastViewportToastList,其中 ToastListuseToastManager() 读取 toasts 并逐条渲染 Toast + ToastContent + ToastIcon/ToastTitle/ToastDescription/ToastAction/ToastCloseToastIcon 还支持 successinfowarningerrorloading 五种类型的图标(toast.tsx#L142-L224),示例可参考 toast-example.tsx
  • Radix/Aria 路线见 sonner.tsx,用法示例见 sonner-example.tsx

因此,在写 Toast 前先确认项目 base,混用(如在 Base UI 项目里引入 Sonner)会破坏与主题变量的联动。

浮层组件选型:Dialog、Sheet、Drawer 与轻信息浮层

规则文档给出了六类浮层场景的选型表:

使用场景 组件
需要输入的重点任务 Dialog
破坏性操作的确认 AlertDialog
带详情或筛选器的侧边面板 Sheet
移动优先的底部面板 Drawer
悬停时的快速信息 HoverCard
点击后的小块上下文内容 Popover

这些组件在注册表中均有对应实现,例如 dialog.tsxsheet.tsxdrawer.tsxalert-dialog.tsxhover-card.tsx,选型时按“交互语义”而不是“视觉相似度”判断。

Dialog、Sheet、Drawer 必须带 Title

DialogTitleSheetTitleDrawerTitle可访问性(accessibility)必需项,必须始终存在。如果需要视觉上隐藏标题,使用 className="sr-only",而不是直接省略:

<DialogContent>
  <DialogHeader>
    <DialogTitle>Edit Profile</DialogTitle>
    <DialogDescription>Update your profile.</DialogDescription>
  </DialogHeader>
  ...
</DialogContent>

这条规则的原因在于浮层组件的无障碍实现依赖标题节点作为 aria-labelledby 的指向目标;省略标题会导致屏幕阅读器无法获知浮层用途。sr-only 方案在保留 DOM 结构的同时实现了视觉隐藏,两者兼得。

Card 使用完整组合,不要把内容全塞进 CardContent

规则要求使用完整的 Card 组合结构,而不是把所有东西堆进 CardContent

<Card>
  <CardHeader>
    <CardTitle>Team Members</CardTitle>
    <CardDescription>Manage your team.</CardDescription>
  </CardHeader>
  <CardContent>...</CardContent>
  <CardFooter>
    <Button>Invite</Button>
  </CardFooter>
</Card>

五个子部件各司其职:CardHeader 放标题与描述、CardContent 放主体、CardFooter 放尾部操作区。card.tsx 的注册表实现中每个子部件都带有独立的 data-slot(如 data-slot="card"),这也意味着下游可以通过 slot 选择器精细定制各区域样式——拆得越完整,可定制面越大。

Button 没有 isPending / isLoading 属性

shadcn/ui 的 Button 不提供 isPendingisLoading 这类加载态属性。正确做法是用 Spinner + data-icon + disabled 组合表达:

<Button disabled>
  <Spinner data-icon="inline-start" />
  Saving...
</Button>

这里 data-icon="inline-start" 是仓库图标规则的一部分(见 icons.md),由组件 CSS 负责图标的定位与尺寸,调用方不需要写 size-4mr-2 之类的工具类。Spinner 本身的实现见 spinner.tsx

TabsTrigger 必须放在 TabsList 内

永远不要把 TabsTrigger 直接渲染在 Tabs 里,必须包在 TabsList 中:

<Tabs defaultValue="account">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
  </TabsList>
  <TabsContent value="account">...</TabsContent>
</Tabs>

TabsList 承担触发器组的样式容器职责(背景、圆角、聚焦环),tabs.tsx 中对 TabsListTabsTrigger 的样式分工印证了这一点:跳过 TabsList 会让触发器失去组级样式并破坏键盘导航的视觉反馈。

Avatar 必须始终搭配 AvatarFallback

头像图片加载失败时需要兜底,因此 AvatarFallback 是必需的:

<Avatar>
  <AvatarImage src="/avatar.png" alt="User" />
  <AvatarFallback>JD</AvatarFallback>
</Avatar>

规则的理由很直接:没有 AvatarFallback 时,图片 404 或网络异常会导致头像位置出现空白,而 AvatarFallback(通常放姓名首字母)保证了视觉与语义的双重兜底。

用现成组件替代自定义标记

规则的最后一条是“先查组件库,再写自定义标记”的三组替代映射:

不要用 改用
<hr><div className="border-t"> <Separator />
带样式的 <div className="animate-pulse"> <Skeleton className="h-4 w-3/4" />
<span className="rounded-full bg-green-100 ..."> <Badge variant="secondary">

三者均在注册表中有对应实现:separator.tsxskeleton.tsxbadge.tsx。使用现成组件的收益在于:它们内置了主题变量(如 text-muted-foregroundbg-primary)、data-slot 钩子与变体系统,且会随注册表更新而演进;手写的 <div className="border-t"> 则既无法跟随主题,也容易被后续维护者误当成一次性标记而改坏。

小结:组合规则的核查清单

composition.md 的 13 条规则收敛为一份可逐项自查的清单:

  1. SelectItem/DropdownMenuItem/CommandItem 等是否都包在对应 Group 内(对照上文映射表);
  2. 提示条是否用了 Alertdefault/destructive 变体)而非自定义 div;
  3. 空状态是否用了 Empty + EmptyHeader/EmptyContent
  4. Toast 是否匹配项目 base(Base UI → toast 组件,Radix/Aria → Sonner);
  5. 浮层选型是否按场景对应 Dialog/AlertDialog/Sheet/Drawer/HoverCard/Popover;
  6. Dialog/Sheet/Drawer 是否都有 *Title(视觉隐藏时用 sr-only);
  7. Card 是否使用了 Header/Title/Description/Content/Footer 完整结构;
  8. 按钮加载态是否用 Spinner + data-icon + disabled 组合,而非不存在的 isPending
  9. TabsTrigger 是否包在 TabsList 内;
  10. Avatar 是否带了 AvatarFallback
  11. 分隔线/骨架屏/徽章是否分别用了 Separator/Skeleton/Badge

这 13 条规则的共同点是:把结构与语义的职责交给组件的官方组合方式,而不是用通用 HTML 标记“模拟”一个组件的外观。SKILL.mdcomposition.mdstyling.mdforms.mdchat.md 等并列为“始终强制”的 Critical Rules,配合 base-vs-radix.mdasChildrender 的差异),构成了 shadcn/ui 项目内组件代码的正确性基线。

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

项目优选

收起
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