shadcn-svelte 组件组合规范实战指南:Composition 规则全解析
【免费下载链接】shadcn-svelteshadcn/ui, but for Svelte. ✨项目地址: https://gitcode.com/GitHub_Trending/sh/shadcn-svelte
本文是 shadcn-svelte 组件库的组合规范(Component Composition)深度指南。它总结了在使用 shadcn-svelte 构建 Svelte 5 应用时必须遵循的组件结构与组装约定——从Select.Item必须放入Select.Group、overlay 组件如何按场景选型,到 Dialog 必须带 Title、按钮加载态如何用Spinner组合等。读完本文,你将掌握一套可复现、可审查的组件写法,避免常见的组合错误,并理解每条规则背后的无障碍与语义化动机。
为什么需要组合规范
shadcn-svelte 采用"复制到你的项目里再修改"的分发模型(registry),组件以源码形式存在于你的项目中,例如docs/src/lib/registry/ui/目录下的select/、dialog/、card/等子目录。每个组件目录都由若干部件(part)组成,并通过index.ts以命名空间方式统一导出。以 select/index.ts 为例:
import Group from "./select-group.svelte"; import Item from "./select-item.svelte"; // ... export { Root, Group, Label, Item, /* ... */ };因此你在业务代码中通常是import * as Select from "$lib/components/ui/select",再以Select.Content、Select.Group、Select.Item的形式组合使用。这种"原子部件 + 显式组合"的设计,要求开发者遵守一套清晰的组装纪律——这正是本文档规则的价值所在。以下逐条展开。
Items 必须放在对应的 Group 组件内
规则:永远不要把条目(Item)直接渲染在内容容器(Content)里,必须先包一层 Group。
错误写法:
<script lang="ts"> import * as Select from "$lib/components/ui/select"; </script> <Select.Content> <Select.Item value="apple">Apple</Select.Item> <Select.Item value="banana">Banana</Select.Item> </Select.Content>正确写法:
<script lang="ts"> import * as Select from "$lib/components/ui/select"; </script> <Select.Content> <Select.Group> <Select.Item value="apple">Apple</Select.Item> <Select.Item value="banana">Banana</Select.Item> </Select.Group> </Select.Content>这一规则适用于所有基于 Group 的组件,对应关系如下:
| Item(条目) | Group(分组) |
|---|---|
Select.Item、Select.Label | Select.Group |
DropdownMenu.Item、DropdownMenu.Label、DropdownMenu.Sub | DropdownMenu.Group |
Menubar.Item | Menubar.Group |
ContextMenu.Item | ContextMenu.Group |
Command.Item | Command.Group |
从源码看,这些 Group 部件都是对 bits-ui 原始组件的薄封装。例如 select-group.svelte 只是接收SelectPrimitive.GroupProps、合并 class 并透传剩余 props,并未额外添加逻辑。这意味着分组语义(如键盘导航的分组边界、屏幕阅读器对选项组的分组朗读)直接继承自底层库,若省略 Group 直接渲染 Item,会丢失分组语义与正确的无障碍结构。同理可对照dropdown-menu、menubar、context-menu、command目录下的对应部件源码。
提示类信息使用 Alert
需要向用户展示告警、警告或需要注意的信息时,使用Alert组合,而不是自己拼<div class="...">:
<script lang="ts"> import * as Alert from "$lib/components/ui/alert"; </script> <Alert.Root> <Alert.Title>Warning</Alert.Title> <Alert.Description>Something needs attention.</Alert.Description> </Alert.Root>从 alert/index.ts 可以看到,Alert 由Root、Title、Description、Action四部分组成(并导出alertVariants供变体扩展),它们共同保证了标题-描述结构的语义完整。在 shadcn-svelte 文档站中,代码示例旁的安全提醒、迁移说明等 callout 正是以这种组合方式实现的。
空状态使用 Empty 组件
列表无数据、搜索无结果等"空状态"场景,统一使用Empty组件家族,形成一致的视觉与结构:
<script lang="ts"> import * as Empty from "$lib/components/ui/empty"; import { Button } from "$lib/components/ui/button"; import FolderIcon from "@lucide/svelte/icons/folder"; </script> <Empty.Root> <Empty.Header> <Empty.Media variant="icon"><FolderIcon /></Empty.Media> <Empty.Title>No projects yet</Empty.Title> <Empty.Description >Get started by creating a new project.</Empty.Description > </Empty.Header> <Empty.Content> <Button>Create Project</Button> </Empty.Content> </Empty.Root>从 empty/index.ts 可见,Empty 家族包含Root、Header、Media、Title、Description、Content六个部件。Empty.Media支持图标与图片两种variant,用于承载引导性视觉;Empty.Content用于放置操作按钮等行动号召。@lucide/svelte是当前项目推荐的图标源(图标源码统一维护在docs/src/lib/registry/icons/,对应文档见 icons.md)。
Toast 通知统一使用 svelte-sonner
所有轻量级 Toast 通知,统一走svelte-sonner的toastAPI,而不是自建提示组件:
<script lang="ts"> import { toast } from "svelte-sonner"; </script>toast.success("Changes saved."); toast.error("Something went wrong."); toast("File deleted.", { action: { label: "Undo", onClick: () => undoDelete() }, });在应用布局(layout)中只需挂载一次Toaster(从你的 UI 目录导入,即docs/src/lib/registry/ui/sonner/下的Toaster),后续所有页面即可共享通知通道。svelte-sonner的完整用法与属性说明见 sonner 组件文档。注意action回调中应调用你自己作用域内的处理函数(如示例中的undoDelete),不要引用未定义的全局变量。
overlay 组件如何选型
shadcn-svelte 提供了多种覆盖层(overlay)组件,选型依据是交互任务的形态:
| 使用场景 | 组件 |
|---|---|
| 需要输入、聚焦单一任务 | Dialog |
| 破坏性操作确认 | AlertDialog |
| 侧边面板,承载详情或筛选 | Sheet |
| 移动端优先的底部面板 | Drawer |
| 悬停时展示快速信息 | HoverCard |
| 点击后展示小型上下文内容 | Popover |
这组选择的本质区别在于交互语义:Dialog是模态聚焦;AlertDialog强调"危险动作的二次确认";Sheet是内容型抽屉(通常伴随筛选表单);Drawer面向触屏手势;HoverCard不抢占点击焦点;Popover是轻量浮层。它们各自拥有独立的部件目录(docs/src/lib/registry/ui/下的dialog/、alert-dialog/、sheet/、drawer/、hover-card/、popover/),均可按命名空间方式组合。
Dialog、Sheet、Drawer 必须提供 Title
规则:Dialog.Title、Sheet.Title、Drawer.Title是必选项——这些组件底层基于 ARIA dialog 角色,缺失标题会破坏屏幕阅读器的可访问性。如果视觉上不需要显示标题,用class="sr-only"隐藏而不是删掉。
<script lang="ts"> import * as Dialog from "$lib/components/ui/dialog"; </script> <Dialog.Content> <Dialog.Header> <Dialog.Title>Edit Profile</Dialog.Title> <Dialog.Description>Update your profile.</Dialog.Description> </Dialog.Header> ... </Dialog.Content>从 dialog-title.svelte 的源码可以看出,Dialog.Title是 bits-uiDialogPrimitive.Title的封装——底层组件本身就被设计为承载aria-labelledby语义的角色,省略它意味着弹窗失去可访问名称。Sheet、Drawer的实现与之同构,规范同样适用。
Card 的标准结构:使用完整组合
不要把内容全部塞进Card.Content。应使用完整的 Card 组合:
<script lang="ts"> import * as Card from "$lib/components/ui/card"; import { Button } from "$lib/components/ui/button"; </script> <Card.Root> <Card.Header> <Card.Title>Team Members</Card.Title> <Card.Description>Manage your team.</Card.Description> </Card.Header> <Card.Content>...</Card.Content> <Card.Footer> <Button>Invite</Button> </Card.Footer> </Card.Root>这样划分的好处:Header统一承载标题与描述,保持间距一致;Content只负责主体内容;Footer固定放操作按钮区(对齐到卡片底部)。Card家族部件同样维护在docs/src/lib/registry/ui/card/下,逐个部件均有独立源码文件,便于按需裁剪。
Button 没有 isPending / isLoading prop:用 Spinner 组合
规则:shadcn-svelte 的Button不提供isPending或isLoading这类加载态 prop。加载状态应当通过组合实现——在Button内部放入Spinner并配合disabled:
<script lang="ts"> import { Button } from "$lib/components/ui/button"; import { Spinner } from "$lib/components/ui/spinner"; </script> <Button disabled> <Spinner><script lang="ts"> import * as Tabs from "$lib/components/ui/tabs"; let tab = $state("account"); </script> <Tabs.Root bind:value={tab}> <Tabs.List> <Tabs.Trigger value="account">Account</Tabs.Trigger> <Tabs.Trigger value="password">Password</Tabs.Trigger> </Tabs.List> <Tabs.Content value="account">...</Tabs.Content> </Tabs.Root>注意示例使用了 Svelte 5 的$state声明响应式状态,并通过bind:value={tab}让选中值与外部状态双向同步。Tabs.List之下也可以自由组合Tabs.Trigger与Tabs.Indicator等部件(见docs/src/lib/registry/ui/tabs/),但Trigger永远不能脱离List存在。
Avatar 必须包含 Avatar.Fallback
规则:Avatar组合中必须包含Avatar.Fallback,用于图片加载失败或未提供图片时的降级展示:
<script lang="ts"> import * as Avatar from "$lib/components/ui/avatar"; </script> <Avatar.Root> <Avatar.Image src="/avatar.png" alt="User" /> <Avatar.Fallback>JD</Avatar.Fallback> </Avatar.Root>从 avatar-fallback.svelte 的源码可见,Avatar.Fallback封装自 bits-ui 的AvatarPrimitive.Fallback,其核心行为是:仅当图片尚未加载完成或加载失败时渲染(通常是姓名缩写或默认图标),成功加载后自动隐藏。不写 Fallback 意味着头像加载失败时会出现空缺口。多个头像组合、尺寸变体等用法可参考 avatar 文档。
优先使用现成组件替代自定义标记
遇到以下"手写样式"冲动时,用库内组件替换:
| 不要这么写 | 应该用 |
|---|---|
<hr>或<div class="border-t"> | <Separator />(import { Separator } from "$lib/components/ui/separator") |
<div class="animate-pulse">加手写占位 div | <Skeleton class="h-4 w-3/4" />(import { Skeleton } from "$lib/components/ui/skeleton") |
<span class="rounded-full bg-green-100 ..."> | <Badge variant="secondary">(import { Badge } from "$lib/components/ui/badge") |
替换动机有三点:
- 语义与无障碍:
Separator使用role="separator"语义(docs/src/lib/registry/ui/separator/),比裸hr更明确;Skeleton自带aria-hidden与脉动动画,不会干扰读屏。 - 主题一致性:
Badge的variant体系(default/secondary/outline 等)与全局主题色绑定,手写bg-green-100这类硬编码颜色会脱离主题定制(主题定义见 theming 文档 与docs/src/lib/registry/themes.ts)。 - 可维护性:使用库组件后,改主题或改间距只需动一处,无需全局搜索手写 class。
汇总:组合规范的核心理念
总结本规范,shadcn-svelte 的组合哲学可以提炼为三条:
- 先查库,再手写:
Separator、Skeleton、Badge、Alert、Empty等语义化部件已经覆盖了绝大多数"散件"需求,优先组合而非造轮子; - 尊重底层语义:Group、Title、Fallback、List 这些"必须"项,其约束来自 bits-ui / ARIA 的语义要求,省略会导致功能或无障碍缺陷,而非单纯的风格偏好;
- 状态外置为组合:
Button的加载态、Avatar的降级态等,都通过部件组合表达,保持组件 API 精简统一。
按此规范编写的组件代码,既能在npm run dev(SvelteKit 开发服务器)下获得稳定的交互行为,也便于后续按需修改。想要快速应用到自己的项目,可先阅读 CLI 使用说明 与 安装指南 将所需组件安装到项目中,再对照本文规则进行组装。
【免费下载链接】shadcn-svelteshadcn/ui, but for Svelte. ✨项目地址: https://gitcode.com/GitHub_Trending/sh/shadcn-svelte
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考