shadcn/ui 注册表里躺着 411 个条目,真正的组件只有 54 个
- shadcn/ui 的官方注册表 apps/v4/registry.json 共 411 个条目,其中真正意义上的 UI 组件只有 54 个,其余是 238 个示例、97 个 block、5 套主题和 2 套 style
- 一条 npx add 命令背后是完整的注册表分发管线:zod 校验条目、递归解析依赖树、ts-morph AST 改写源码、按 CSS 变量/内联颜色两路分发后写入项目
- 2026 年三底座架构重塑(radix/base/aria)把价值从无头库剥离,全部押在分发管线和设计系统上
- AI 成为一等公民:MCP 命令让 AI 能自装组件,@shadcn/react 提供聊天滚动和问卷原语,@shadcn/helpers 提供链式 chat 构造器
- 三年半未关闭的
2023 年 1 月 4 日,shadcn 在 GitHub 建了个仓库,就叫 ui。那年前端圈对「组件库」的标准答案还是 npm install 加 import,一条龙走完。他偏不。npx shadcn-ui@latest add button,命令跑完,Button 组件的源码直接躺进你的 components 目录,没有 node_modules 里的黑盒,你拿到手的就是一份能随便改的 tsx 文件。
当时不少人觉得这是行为艺术,组件哪有这么发的。
三年半过去,这个仓库攒下 122,766 颗星、10,067 次 fork,MIT 协议,TypeScript 写的,主分支今天还在动(最后推送时间 2026-09-02)。但真正让我下决心拆它的不是星数,是仓库 description 的最后五个词,a code distribution platform。
代码分发平台。
一个靠组件库起家的项目,把自己重新定义成了平台。这也不是文案口误,我把它官方注册表 apps/v4/registry.json 拉下来数了一遍,411 个条目里,真正意义上的 UI 组件(registry:ui)只有 54 个,剩下的是 238 个 example、97 个 block、5 套主题、2 套 style,外加 lib 和 hook 各 1 个。
组件库的皮,平台的骨头。
一条 npx 命令背后,是一条完整的分发管线
先说清楚它到底在分发什么。
npm 模式分发的最小单元是包,你消费编译产物和类型声明。shadcn 分发的最小单元是文件,注册表里每个条目其实是一段 JSON,声明这个组件叫什么、依赖哪些 npm 包、携带哪些文件、要注入哪些 CSS 变量。
这条管线的入口在 packages/shadcn/src/registry/constants.ts,第一行就有意思。
export const REGISTRY_URL =
process.env.REGISTRY_URL ?? "https://ui.shadcn.com/r"
默认从官方拉取,环境变量可以整体换掉。企业内网想镜像一份官方注册表,改一个变量就行,不用 fork 仓库。紧挨着的 BUILTIN_REGISTRIES 定义了唯一内置命名空间 @shadcn,注释写得直白,built-in registries are always available and cannot be overridden。
schema 是这套协议的合同。packages/shadcn/src/registry/schema.ts 用 zod 定义了 14 种条目类型,从 registry:ui、registry:block 到 registry:font,其中 2 种是内部专用。几个细节值得停下来看。registry:file 和 registry:page 必须带 target 字段,用 z.discriminatedUnion 强制校验,因为这两类条目的文件要写进你项目的指定路径,没有 target 就不知道往哪放。第三方注册表的命名空间必须以 @ 开头,schema 里的 refine 规则连报错文案都替你想好了,Registry names must start with @ (e.g., @v0, @acme)。注册表配置还支持 headers 字段,私有注册表可以带鉴权头,这是一条明着留给企业的路。
依赖解析在 resolver.ts。组件之间用 registryDependencies 声明依赖,比如日历依赖按钮,resolveRegistryTree 先拉索引,resolveDependenciesRecursively 再递归展开整棵依赖树,直到把要拷的文件凑齐。你在终端敲一次 add,背后是索引、条目、依赖树三层解析。
然后是整条管线里最硬核的一步,源码改写。
组件源码默认用语义化颜色 token,bg-background、text-foreground 这类。如果你在 init 时选了不用 CSS 变量,transform-css-vars.ts 就得把每个组件源码里的语义类名,替换成具体的浅色加深色类名对。它用的不是正则,是 ts-morph 的 AST。sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral) 遍历文件里所有字符串字面量,splitClassName 把类名拆成 variant、值、透明度修饰三段(sm:group-data-[size=default]/alert-dialog-content:text-left 这种嵌套写法都处理了),再交给 applyColorMapping 按 bg-、text-、border-、ring-offset-、ring- 五个前缀查表替换,最后产出 bg-white dark:bg-slate-950 这样的成对类名。
这个文件里还躺着两大段注释掉的 jscodeshift 旧实现。你能看到作者从 jscodeshift 迁到 ts-morph 的完整痕迹,旧方案只扫 JSXAttribute 里的 className,新方案扫全部字符串。工具换了两代,需求没变过,改写必须发生在语法树层面,正则扛不住这种复杂度。
文件写盘只是终点站的一半。依赖安装、CSS 变量注入(cssVars 字段分 light 和 dark 两套)、字体处理(transform-font.ts),各自是一个独立 transformer,在 utils/transformers/index.ts 里串成流水线。整条链路走完,你项目里多出来的不是一条依赖,是一份按你的口味定制过的源码。
配一张图,把这条管线整个走一遍。
一句话总结这张图,注册表负责声明,解析器负责算账,transformer 负责改写,你的项目负责持有。
组件只是外壳,底座才是产品
2026 年的 shadcn 有个很大的结构变化,style 这个概念被 base 替代了。
老的 default、new-york 两套 style 退役,现在 apps/v4/registry/bases.ts 定义了三个底座,radix(依赖 radix-ui)、base(依赖 @base-ui/react)、aria(依赖 react-aria-components),每个都是一个 registry:style 条目。底座决定无头组件层用哪家,主题决定长什么样。__components__ 目录下我数出 24 个组合文件,aria-luma、radix-nova 这种命名,3 个底座乘 8 套主题(luma、lyra、maia、mira、nova、rhea、sera、vega)。
说真的,这步棋比看起来激进。Radix、Base UI、React Aria 是三个定位几乎相同的无头组件库,shadcn 把它们抽象成可替换的底座,等于把自己的价值从「用了哪家无头库」里剥离出去,全部押在分发管线和设计系统上。无头层打架,它三家全押。
底座之间还有细微分叉,藏在 constants.ts 的弃用清单里。DEPRECATED_COMPONENTS 声明 toast 已被 sonner 取代,但补了一句,toast 只对 Base UI 项目可用。对应的 COMPONENTS_HIDDEN_FROM_SELECTION 里,sonner 在 base 底座下被隐藏。翻译过来,同一个通知需求,radix 和 aria 项目里用 sonner,base 项目里反而用回官方 toast。你在 A 底座建立的心智,到 B 底座要反着来,这是多底座架构的成本第一次露头。
色板也换了血。BASE_COLORS 如今是 neutral、zinc、stone、mauve、olive、mist、taupe 七个,老玩家熟悉的 slate 和 gray 已经不在列表里。FALLBACK_STYLE 还叫 new-york-v4,算是对旧时代的一点留念。
模板一侧,src/templates/ 下有 next、vite、laravel、astro、react-router、monorepo、start 七份模板。一个 React 组件体系给 Laravel 留了一等公民座位,这个信号本身就很说明问题,它想当的是不分框架的公共层。
AI 成了第一等公民
如果说底座是结构变化,AI 相关的投入就是态度变化。
CLI 里有一条 mcp 命令。npx shadcn mcp 直接起一个 stdio 的 MCP server,npx shadcn mcp init --client claude 则把配置写进 Claude Code 的 .mcp.json。commands/mcp.ts 里的 CLIENTS 数组支持五家,claude、cursor、vscode、codex、opencode,每家的配置文件格式都预置好了。版本那句 SHADCN_MCP_VERSION = "latest" 有点粗暴,永远拉最新版,锁版本这件事在这里不存在。
更有说服力的是仓库自己。根目录下有 .claude/、.cursor/rules/,还有 .cursor-plugin/plugin.json,一个 Cursor 插件的骨架。CI 里有 monitor-registries.yml 和 validate-registries.yml,监控和校验注册表进了持续集成。这个项目在自己的开发流程里就把 AI 工具当成默认同事。
然后是两个新包。
@shadcn/react 今年 8 月 31 日发了 0.3.1,目前两个导出,MessageScroller 和 Questionnaire。前者是给聊天记录用的无头滚动容器。别小看「聊天窗口往下滚」这件事,流式输出的滚动是个深坑,内容在视口内增长、用户往上翻、新消息锚点定位,三个需求互相打架。它的解法包括给消息列表默认 role="log" 加 aria-relevant="additions",用 data-pending-scroll 把服务端渲染的首帧藏到滚动位置应用完为止(防止刷新时闪一下顶部,0.3.1 刚修的),以及把 ResizeObserver 回调合并进 requestAnimationFrame,修掉流式增长时的 ResizeObserver loop 报错。这些细节在它的 PERFORMANCE.md 和 changelog 里都有据可查。Questionnaire 是 0.3.0 加的多步问卷原语,专门服务 AI 应用里「结构化问用户几个问题」的场景。
@shadcn/helpers 则提供 createChat,一个链式的聊天脚本构造器,writer.reasoning() 写推理段,writer.tool() 发工具调用。最有意思的设计是 human-in-the-loop,README 原文写着,A tool call left unresolved pauses the turn。一个没被解决的工具调用会挂起整个回合,客户端拿到控制权,用户补上 addToolOutput 之后,下一个回调回合作为延续接着跑。
你想想看这几个动作放在一起意味着什么。MCP 让 AI 能自己装组件,chat 原语让 AI 应用的界面有现成积木,注册表让这一切可被机器解析。Rauch 那句判断,shadcn 才是人们真正想要的 React,上下文窗口正在成为新的分发层,在这个仓库里已经长出了实体。连 add.ts 里都有个 /chat/b/ 的正则,专门识别聊天区块的安装。
三年半没关上的那个 issue
批判的部分,从一条最老的 issue 说起。
#66,标题就一个问句,Multi select ?,2023 年 2 月 8 日开的,108 条评论,今天还开着。三年半,一个多选组件,官方一个字没回,评论区俨然成了许愿池,隔几个月就有人来问一次。坦白讲,多选不是边缘需求,社区里用 cmdk 和 popover 拼出多选的方案一抓一把,官方就是不出,你也没地方说理去,毕竟源码在你手里,「想要自己加」就是这套架构的标准答案。
维护的集中度也值得看一眼。contributors 榜单上,shadcn 本人 1,333 次提交,第二名是 github-actions 机器人(115 次),第三名 dependabot(35 次),排在最前的人类外部贡献者 kapishdima,21 次。这依然是那个一个人的项目,加上 Vercel 的基建和一小圈帮手。bus factor 低到这个程度,12 万颗星的生态坐在单点之上。
再往前翻,Tailwind v4 迁移期留下的坑到现在还开着。#6446 说 CLI 无法校验 Tailwind CSS 安装(58 条评论),#6843 是 Tailwind v4 下按钮 hover 不出 pointer(52 条),#5552 是 Next.js 15 里 Theme Provider 的 hydration 报错(38 条)。单个看都不致命,合起来的图景是,每当上游来一次破坏性更新,拷进你项目的源码就要跟着过一遍阵痛,而 CLI 的迁移工具(migrations/ 目录下 migrate-radix、migrate-icons、migrate-base-color、migrate-rtl 四个迁移器)永远慢半拍。
这其实是源码分发模式的宿命。npm 包把升级责任留在维护方,你升个版本号就行。shadcn 把所有权交给你,升级的责任也一起交了。diff 命令的存在本身就是承认,官方改了组件,你本地那份不会自己变。所有权是个礼物,也是个负担。
还有个小坑我顺手验了一下,registryItemFontSchema 里 provider 字段是 z.literal("google"),字体分发只认 Google Fonts 一家,自托管字体这条路目前是封死的。
最后一个观察有点微妙。README 里加粗写着 Use this to build your own component library,仓库 description 却说自己是 a code distribution platform。两个口号打架,前者还是 2023 年那个拷贝组件的叙事,后者已经是平台叙事。定位在漂移,文档没跟上。
所有权分发,一个正在被复制的模式
拆完这个仓库,我提炼一个可带走的东西,就叫它所有权分发(ownership-first distribution)。
它有三层。第一层是协议,一份机器可读的 manifest(shadcn 用的是 registry JSON schema)描述每个分发单元的依赖、文件、配置副作用。第二层是交付,把内容直接写进用户的项目而不是装进依赖目录,交付的终点是用户的文件系统。第三层是所有权,用户拿到源码,改不改、升不升级、什么时候升,全是用户自己的事。
npm 模式是租,所有权分发是买。租的好处是省心,坏处是你的天花板就是包作者的天花板。买的好处是天花板消失,坏处是维护责任平移。shadcn 用 12 万颗星验证了一件事,对认真做产品的团队,买这个选项的吸引力大得多。
这个模式早就开始扩散了。我之前拆过的 thesvg 用同样的方式发图标,cult-ui、tweakcn 这些项目直接建在它的注册表协议上,Rauch 说上下文窗口是新分发层,说的也是同一件事,AI 消费代码的最佳格式不是包,是带声明的可读源码。
选型结论给三句。你在做正经产品、有能力消化源码升级,shadcn 是目前 React 生态最稳的默认答案,三底座选一个(新项目我倾向 radix,生态最厚)就开工。你只想快速拼原型或内部工具,不在乎样式同质化,它反而是最快的那条路。你的团队对升级可控性有硬要求(金融、医疗这类要审 diff 的场景),拷贝分发对你反而是合规优势,每一行进你仓库的代码都有案可查。
至于那个 multi-select,我再等等看。

评论互动