🧠 Headless / Unstyled 组件

一句话定性

Headless UI 是整部组件库史里最关键的一次范式转变:它把一个组件拆成两半——“行为 + 状态 + 无障碍(a11y)“(那个极难、极该外包的内核)和 “样式”(那个高度个性化、该归你的部分)——然后只交付前者,样式完全留白。它精准地回答了全包型组件库的两难:你终于可以只买最难的部分,而把品牌和设计的控制权 100% 拿回来。


一、它是什么 & 出现的时代

约 2019 年起,2018-2023 工程化时代的中后段,一批 Headless(无头)/ Unstyled(无样式) 组件库登场:

项目出品方特点
Radix UIWorkOSReact 无样式原语(primitives),a11y 标杆,后成为 shadcn 的底座
Headless UITailwind Labs(Tailwind CSS 团队)为配合 Tailwind 而生的无样式组件
React Aria / React StatelyAdobe把行为(Aria)与状态(Stately)再拆开,框架无关的 a11y 内核
TanStack(Table/Virtual/Query 等)Tanner Linsley”Headless” 思路推广到表格、虚拟列表、数据层——只给逻辑,不给 UI

它们的共同信条:组件库不该替你决定长什么样

// Radix: 组件只提供"行为/状态/无障碍"结构, 没有任何视觉样式
// 样式完全由你用 Tailwind / CSS-in-JS 注入
import * as Dialog from '@radix-ui/react-dialog';
 
<Dialog.Root>
  <Dialog.Trigger className="px-4 py-2 rounded bg-black text-white">打开</Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Overlay className="fixed inset-0 bg-black/50" />
    <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6">
      {/* 焦点陷阱、Esc 关闭、aria-* 朗读、点击外部关闭, Radix 全包了; 样式全是你的 */}
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

二、为什么会出现(解决上一代什么痛点)

全包型组件库把两个本应分开的诉求焊死了

Element 给你完整组件,但带来一道无法拆开的焊缝:

  • 你想要它极难自己写的行为和 a11y → 必须连它的整套内置样式一起吃下;
  • 你想要自己的设计 / Design System → 又要和它的内置样式做无尽的覆盖斗争(!important:deep()、追内部 className、改 Less 变量……),脆弱且升级即崩

“我只想要逻辑,不想要你的皮”——这个朴素诉求,在全包模型下无法满足。

而焊缝的两半,难度天差地别。这要从一个被长期低估的事实说起:

真正难的,从来不是样式,而是 a11y 和行为

自己写一个生产级的下拉/弹窗/组合框,要正确处理:

  • 焦点管理:focus trap、关闭后焦点归位、Tab 顺序;
  • 键盘交互:↑↓ 导航、Enter 选择、Esc 关闭、Home/End、type-ahead 首字母跳转;
  • WAI-ARIA:正确的 rolearia-expandedaria-activedescendantaria-controls,让屏幕阅读器朗读正确;
  • 边界:点击外部关闭、屏幕边缘翻转定位、滚动锁定、SSR 兼容……

这些几乎没人能凭一己之力写对,且踩坑无数。它才是组件库真正的价值内核——也正是最该被专业项目托管、最该外包的部分。

Headless 的洞察(一次漂亮的第一性原理):把这个最难的内核单独抽出来交付,样式这种高度个性化、人人想自己掌控的部分,本就不该由组件库决定。 于是——解耦

它的出现还踩中了一个关键时代红利:Tailwind CSS 的崛起。当样式可以用 className 原子类极其顺手地就地拼出来,“组件不带样式”从”麻烦”变成了”自由”——你不再需要为每个组件写 CSS 文件,直接在 Headless 组件的 className 上拼 Tailwind 类即可。Headless + Tailwind 是天作之合,这也解释了为什么 Headless UI 干脆就是 Tailwind 团队出品。


三、核心机制 & 为什么流行

  • 行为与样式解耦(核心):组件只暴露结构、状态和无障碍语义(Dialog.TriggerSelect.ItemisOpenaria-*),零视觉样式。样式由你通过 className / style / CSS-in-JS 注入。
  • a11y 默认正确:键盘、焦点、ARIA 这些”做对极难”的事,开箱即对。这是 Headless 最硬的卖点。
  • 可组合的原语(primitives):Radix 提供的是细粒度可组合的零件(Root/Trigger/Content/Portal…),而非铁板一块的成品组件——你按需拼装。
  • 逻辑与渲染分离的更激进形态:React Aria 把”行为(Aria hooks)“与”状态(Stately)“进一步拆开,做成框架无关的逻辑层;TanStack 用 headless hooks 交付表格/虚拟滚动/数据请求的纯逻辑,UI 完全交给你——“Headless”从组件扩展成一种通用设计哲学

为什么这是"关键范式转变"

在此之前,“组件库”默认意味着”它决定长什么样”。Headless 把这个二十年的隐含假设掀翻:组件库的职责应当止于”行为/状态/a11y”,样式的主权属于产品。 这一刀切下去,直接重塑了责任边界——

  • 该外包的(a11y/行为内核)→ 交给 Radix/React Aria 这类专业项目;
  • 不该外包的(样式/品牌)→ 收回到产品自己手里(配 Tailwind)。

它为 shadcn-ui 的诞生铺好了全部地基:shadcn 正是 Radix(行为/a11y)+ Tailwind(样式) 的封装与再交付。


四、带来的新问题 / 副作用

自由的代价

  1. 样式要从零写:Headless 不给样式,意味着每个组件的视觉都得你自己出。对”只想快速搭个后台”的场景,这是负生产力——你又退回了 Bootstrap/AntD 想解决的”做页面要自己写样式”的起点(只不过这次你保留了 a11y)。
  2. 强依赖 Tailwind / 设计能力:Headless 的体验高度绑定 Tailwind 与一定的设计素养;团队没有设计资源时,产出可能还不如直接用 AntD 整齐。
  3. 拼装心智成本:Radix 的可组合原语很灵活,但要理解 Root/Trigger/Portal/Content 的组合方式,比 <Modal open /> 一行多了学习曲线。
  4. “还差最后一公里”:你想要的其实是”带了合理默认样式、但能随便改”的组件——纯 Headless 太裸,全包组件库太死。这个中间地带的空缺,正是 shadcn 要填的。

五、为什么会衰落 / 现状

Headless 没有衰落,正当红,且已成为现代组件生态的底层基础设施:

  • Radix 成为事实标准的无样式原语层,shadcn-ui、众多设计系统都建立其上;
  • React Aria 是 a11y 领域的标杆,被越来越多严肃产品采用;
  • TanStack 系列把 headless 哲学带到表格/虚拟列表/数据层,广受欢迎。

它真正的”现状”,是被 shadcn-ui 推上了大众舞台——shadcn 用 “Radix(行为)+ Tailwind(样式)+ 复制源码进项目” 的组合,把 Headless 那”还差的最后一公里”补齐:既有合理的默认样式可直接用,又因为源码归你而可以随便改。 可以说,Headless 是引擎,shadcn 是把引擎装进了人人会开的车。


六、对后续技术的影响(因果链)

全包型组件库 (AntD/MUI/Element): 样式覆盖难 + "想要行为却被迫连样式一起吃" (焊缝)
        │   + 关键事实: 真正难的是 a11y/行为, 不是样式
        │   + 时代红利: [[原子化CSS|Tailwind]] 崛起 → "组件不带样式"从麻烦变自由
        ↓
Headless / Unstyled (2019+): 解耦! 只交付"行为+状态+a11y", 样式 100% 留白
   Radix(WorkOS) / Headless UI(Tailwind Labs) / React Aria(Adobe) / TanStack
        │
        ├─► 重塑责任边界: 该外包的(a11y内核)外包, 该收回的(样式/品牌)收回
        │
        ├─► 副作用: 样式要从零写, 强依赖 Tailwind + 设计能力 ("最后一公里"空缺)
        │       │
        │       ↓ 谁来补"既有默认样式又能随便改"的中间地带?
        │   [[shadcn-ui]] (2023) = Radix(行为) + Tailwind(样式) + 源码复制进项目
        │
        └─► "Headless"升华为通用哲学: 逻辑/渲染分离 ──► TanStack 推广到数据/表格层
                                                    ──► React Aria 框架无关的 a11y 内核

历史地位

Headless 是”控制权钟摆”回摆途中最关键的一击:它没有简单地”把控制权全还给你”(那等于退回手写时代),而是做了一次精准的责任再切分——把人类几乎写不对的 a11y/行为内核留给专业项目托管,把样式主权交还给产品。这个”只外包最难的、收回该自己定的”的智慧,既是对 全包型组件库那道焊缝的根治,也为 shadcn-ui 把控制权推到极致(连源码都归你)铺好了全部地基。


🔗 本组:UI组件库演进史 | Bootstrap | 组件库时代-AntD-MUI-Element | shadcn-ui 🔗 时代:2018-2023 工程化时代 | 2023-未来 AI时代 🔗 框架:React | Vue 🔗 样式:原子化CSS | CSS-in-JS | CSS演进史