shadcn-svelte 组件组合规范实战指南:Composition 规则全解析
2026/9/16 11:37:48 网站建设 项目流程

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.ContentSelect.GroupSelect.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.ItemSelect.LabelSelect.Group
DropdownMenu.ItemDropdownMenu.LabelDropdownMenu.SubDropdownMenu.Group
Menubar.ItemMenubar.Group
ContextMenu.ItemContextMenu.Group
Command.ItemCommand.Group

从源码看,这些 Group 部件都是对 bits-ui 原始组件的薄封装。例如 select-group.svelte 只是接收SelectPrimitive.GroupProps、合并 class 并透传剩余 props,并未额外添加逻辑。这意味着分组语义(如键盘导航的分组边界、屏幕阅读器对选项组的分组朗读)直接继承自底层库,若省略 Group 直接渲染 Item,会丢失分组语义与正确的无障碍结构。同理可对照dropdown-menumenubarcontext-menucommand目录下的对应部件源码。


提示类信息使用 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 由RootTitleDescriptionAction四部分组成(并导出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 家族包含RootHeaderMediaTitleDescriptionContent六个部件。Empty.Media支持图标与图片两种variant,用于承载引导性视觉;Empty.Content用于放置操作按钮等行动号召。@lucide/svelte是当前项目推荐的图标源(图标源码统一维护在docs/src/lib/registry/icons/,对应文档见 icons.md)。


Toast 通知统一使用 svelte-sonner

所有轻量级 Toast 通知,统一走svelte-sonnertoastAPI,而不是自建提示组件:

<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.TitleSheet.TitleDrawer.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语义的角色,省略它意味着弹窗失去可访问名称。SheetDrawer的实现与之同构,规范同样适用。


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不提供isPendingisLoading这类加载态 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.TriggerTabs.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"

替换动机有三点:

  1. 语义与无障碍Separator使用role="separator"语义(docs/src/lib/registry/ui/separator/),比裸hr更明确;Skeleton自带aria-hidden与脉动动画,不会干扰读屏。
  2. 主题一致性Badgevariant体系(default/secondary/outline 等)与全局主题色绑定,手写bg-green-100这类硬编码颜色会脱离主题定制(主题定义见 theming 文档 与docs/src/lib/registry/themes.ts)。
  3. 可维护性:使用库组件后,改主题或改间距只需动一处,无需全局搜索手写 class。

汇总:组合规范的核心理念

总结本规范,shadcn-svelte 的组合哲学可以提炼为三条:

  • 先查库,再手写SeparatorSkeletonBadgeAlertEmpty等语义化部件已经覆盖了绝大多数"散件"需求,优先组合而非造轮子;
  • 尊重底层语义: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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询