Chakra UI 我前后用了快三年,从 1.x 一路追到现在的 2.x,期间也踩过不少坑。坦白说,刚接触时我觉得它就是个“样式组件库”,无非是把 Tailwind 那套搬到 React 上,但真正深入用下去才发现,它的设计哲学、主题机制和开发体验,和其他组件库完全不是一个路子。这篇东西我不打算写成一本文档翻译,而是把我实际使用中最有价值的部分拆开揉碎,讲讲它的设计思路、核心机制、定制技巧,以及那些官方文档里不会告诉你的细节。
先说结论:Chakra UI 适合谁?适合 React 技术栈、想快速搭出可访问性良好且视觉统一的产品、又不想被组件库的默认风格绑死的团队。它和 Ant Design 这类“重组件”方案的区别在于,Chakra 更接近“样式原语 + 无头组件”的混合体:提供开箱即用的 Button、Modal、Form 等组件,但所有样式全部通过 theme tokens 和 style props 控制,你不会陷入“覆盖样式比重新写一个还难”的泥潭。
1. 整体设计思路拆解:它到底解决了什么问题
1.1 从“组件闭环”到“样式可组合”的转变
传统组件库(比如 Ant Design、Element UI)的设计思路是“我帮你把一切搞定”:按钮长什么样、弹窗怎么打开、表单怎么校验,全都内聚在组件内部。这种方案对后台管理类的中后台项目很友好,但对追求设计还原度、需要高度品牌定制的产品,反而是一种束缚——你会发现大量的时间花在:deep()选择器穿透和!important战斗上。
Chakra UI 换了个思路:它不试图替你做视觉决策,而是提供一套“可组合的样式原子”。核心是 design tokens(设计令牌),把颜色、间距、字体、阴影、圆角、断点全部抽象成语义化的变量,然后通过 style props(比如bg="brand.500"、p={4}、fontSize="lg")直接写在组件上。这样一来,样式不再藏在组件的 CSS 文件里,而是在组件调用处清晰可见,数据的流动和样式的来源都是显性的。
这个转变带来的实际收益有两个。第一,改版成本大幅下降:换主题色只需要改 token 文件,不需要任何全局查找替换。第二,组件库的视觉存在感极低:你不必“适配组件库的审美”,而是让组件库适配你的设计系统。我用它给三个不同品牌的项目搭过前端,同一个 Button 组件在不同项目里呈现出完全不同的视觉形态,但组件本身的交互逻辑、无障碍支持、键盘导航都是现成的。
1.2 Style Props 为什么能成为核心设计
Style Props 是 Chakra 的灵魂,它的形态很简单:把 CSS 属性映射成 React props,你不需要再为“这个按钮 hover 时变蓝、小屏幕下宽度减半”而单独写 CSS。
<Button bg="primary.500" _hover={{ bg: "primary.600", transform: "scale(1.02)" }} _active={{ transform: "scale(0.98)" }} width={{ base: "100%", md: "auto" }} fontSize={["sm", "md"]} > 立即购买 </Button>这里有个很多人一开始不理解的语法:width={{ base: "100%", md: "auto" }}。Chakra 默认的断点是base(0px)、sm(480px)、md(768px)、lg(992px)、xl(1280px)、2xl(1536px)。你传入对象时,它会在对应断点下生成媒体查询。数组写法fontSize={["sm", "md"]}则是单个断点的简写,索引 0 对应 base,索引 1 对应 sm,依此类推。两套语法效果等价,但对象语义更清晰,数组更省代码,看团队偏好。
它底层用的是 @emotion/react 的 css prop 机制,传入的样式会被序列化成 emotion 的 serialized styles,靠 context 传入主题对象。也就是说,你在 style props 里写color="primary.500"时,它并不是直接把primary.500当作色值传给 CSS,而是先查主题 tokens 表,找到对应色值,再生成 CSS 类。这个查表机制让整个系统的“主题一致性”成为可能——所有颜色、间距、字体都来源于同一个 token 表,不会出现一个组件用橙色、另一个组件用相近但不同的橙色这种事。
1.3 和同类方案放在一起怎么选
现在 React 生态里类似的方案还有 Tamagui、Rainbow UI、Base UI 等,Chakra 能够持续活跃,核心优势集中在三点。
- 上手曲线平缓。用过 styled-components 或者写过普通 inline style 的人,几乎不需要学习成本。Style Props 的属性名和 CSS 一致(除了
bg是background的简写、p是padding的简写这类约定),直觉性很强。 - 主题系统完整。Token 表作用于组件内部的每个样式分支,包括暗色模式、焦点环、占位符、禁用态,这些都是组件库内部实现好的,不需要你额外写样式。
- 可访问性做得到位。弹出的 Modal 默认带焦点陷阱,Tooltip 默认支持键盘触发,Button 默认渲染为
<button>并处理了键盘事件。这些细节平时开发不会注意,但对产品过无障碍审计很关键。
2. 主题系统深度解析:从 tokens 到组件变量的定制路径
2.1 基础 tokens 的可视化与自定义
Chakra 的主题结构我建议你把它想成一个嵌套配置对象:colors管颜色,space管间距,fontSizes管字号,radii管圆角,breakpoints管响应式断点,zIndices管层级。任何时候你通过extendTheme覆盖它,所有组件都会响应式地变化。
import { extendTheme } from "@chakra-ui/react"; const theme = extendTheme({ colors: { brand: { 50: "#f0f9ff", 100: "#e0f2fe", 500: "#0ea5e9", 600: "#0284c7", 700: "#0369a1", }, }, space: { "18": "4.5rem", }, fonts: { body: `'Inter', system-ui, -apple-system, sans-serif`, heading: `'Space Grotesk', 'Inter', sans-serif`, }, });这里有个容易忽略的细节:extendTheme不是简单地浅拷贝覆盖,而是走了一套ChakraTheme的类型系统和合并逻辑。比如你只传入了colors.brand,那 Chakra 原来的gray、blue等配色依然存在,不会整个替换掉。但如果你给space扩展一个"18",那么原有的 1 到 16 的值也都会保留,新值只是追加。这才是extendTheme和直接覆盖主题常量的区别。
自定义品牌色时,我建议最少准备 9 个色阶,从 50 到 900。Chakra 的useColorModeValue、按钮的colorScheme、链接的hover态等都会自动用上这些色阶,如果只定义了 500,很多组件会取不到相邻色阶而表现异常。这里可以通过rgba(2, 132, 199, 0.8)这类 alpha 值来创建半透明版本,不需要额外定义色阶。
2.2 组件变量:给组件“开一个样式变体”的正确姿势
Chakra 组件内部样式的组织方式,遵循一个“部件(parts) + 变体(variants) + 尺寸(sizes)” 的三层结构。比如一个 Input 由field(输入框本体)和addon(前后缀)两个部件构成;一个 Button 则只有单个部件。你在extendTheme的components里能修改它的默认变体,也能自定义全新的变体。
不少人在刚上手时会直接在组件上调 style props 来覆盖样式,这样确实最快,但问题也很明显:组件一多,每个调用处都要重复写相同的覆盖,主题里反而没有统一管控。正确做法是使用defineStyleConfig或更复杂的createMultiStyleConfigHelpers来定义组件变体。
以 Button 为例,我自定义了一个outline风格基础上带“下划线动画”的变体:
import { defineStyleConfig } from "@chakra-ui/react"; const Button = defineStyleConfig({ baseStyle: { fontWeight: "semibold", borderRadius: "full", _focusVisible: { boxShadow: "0 0 0 3px rgba(66, 153, 225, 0.6)", }, }, variants: { "animated-underline": { position: "relative", _after: { content: '""', position: "absolute", bottom: "0.25rem", left: "50%", transform: "translateX(-50%)", width: "0%", height: "2px", bg: "currentColor", transition: "width 0.2s ease-in-out", }, _hover: { _after: { width: "80%", }, }, }, }, defaultProps: { variant: "solid", }, }); const theme = extendTheme({ components: { Button }, });这段代码里有一个非常实用的伪元素技巧:基础样式里给按钮加上position: relative,然后通过_after伪元素画一条线,hover 时让它从 0 宽度过渡到 80%。因为是在变体里定义,所以任何业务页面只需要variant="animated-underline"就能复用同一个动效,不会被分散到各个页面的散装 CSS 里。
_focusVisible是另一个常被忽略的 API,它只在键盘导航时显示焦点环。默认 Chakra 自带了一个 focus ring,但设计系统里常用“鼠标点击不显示焦点环、键盘 Tab 时才显示”的交互模式,Chakra 内部已经处理好这个逻辑,你只需要调整焦点环的样式即可。
2.3 多部件组件的定制与扩展
表单类组件通常不只是一个输入框。NumberInput 包含field、stepper、incrementStepper和decrementStepper四个部件;Tabs 包含tablist、tab、tabpanels和tabpanel;Modal 的结构更复杂,包含overlay、dialog、header、body和footer。针对这类组件,Chakra 提供了createMultiStyleConfigHelpers。
import { createMultiStyleConfigHelpers } from "@chakra-ui/react"; const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(["field", "addon"]); const Input = defineMultiStyleConfig({ baseStyle: definePartsStyle({ field: { borderColor: "gray.200", _placeholder: { color: "gray.400", }, }, addon: { bg: "gray.50", }, }), variants: { "flushed-strong": definePartsStyle({ field: { borderBottom: "2px solid", borderColor: "brand.300", borderRadius: 0, px: 0, _focus: { borderColor: "brand.600", }, }, }), }, });这里的关键在于,definePartsStyle会为每个部件生成类型约束,你只能写该部件能接受的 style props,否则会报类型错误。这样做的好处是,团队协作时自定义主题的代码是可类型检查的,不会出现拼错属性名然后样式失效的问题。
多部件组件的定制逻辑在实际项目中很有价值。比如设计系统要求里弹窗圆角要大、遮罩要带毛玻璃效果,直接在主题的 Modal 组件上覆盖一次,全站弹窗就都统一了,不需要在业务代码里到处传borderRadius="xl"。
3. 响应式、暗色模式与可访问性的实现细节
3.1 响应式断点的映射与数组写法的“坑”
前面提到过 Chakra 的断点对象写法,这里再展开讲几个实际开发中容易出问题的细节。
第一,数组写法索引从 0 开始对应base,但如果不写满所有断点,后面的属性会沿用当前断点的最后一个值。例如fontSize={["sm", "lg"]}在lg及以上断点会保持lg,不会回到未定义的值。很多人以为它会在某个断点以上变成默认值,实际上并没有“重置”机制,你需要给数组末尾补一个最终值或用对象写法。
第二,base并不是一个真实存在的 CSS 断点,而是指“移动端以上所有未命中其他断点时”的兜底样式。理解这一点后,写响应式样式时会自然倾向于“移动优先”:先写base的样式,再用md、lg去覆盖大屏下的表现。
第三,Chakra 生成响应式样式的方式是用@media screen and (min-width: ...)实现,但通过 emotion 的序列化机制,它会把同一元素的所有断点样式合并成一个 CSS 类,实际渲染出的 CSS 规模很小。如果直接用 Tailwind 的sm:、md:前缀,会让 HTML 类名爆炸,而 Chakra 的方式对运行时性能更友好。
3.2 useColorMode 与暗色模式的机制
Chakra 的暗色模式不是简单加一个 class 或者把颜色反转,而是通过ColorModeContext管理一个colorMode状态,然后让所有组件在取色时都依赖这个状态。你在任何组件里调用const { colorMode } = useColorMode(),时机到了切到暗色模式,所有组件的colorScheme变体都会映射到暗色模式下预定义的色值。
这里最关键的细节是“避免闪烁”。如果颜色模式存在 React 状态里,首次 HTML 加载时状态还没有注水,浏览器会用默认颜色模式渲染,随后 React 接管后立刻切换到暗色,用户会看到一次明显的颜色跳动。Chakra 提供的ColorModeScript会在页面启动时把本地存储里保存的颜色模式写到一个 script 标签的全局变量中,让首次渲染就用正确的颜色模式。
import { ColorModeScript } from "@chakra-ui/react"; import { Html, Head, Main, NextScript } from "next/document"; export default function Document() { return ( <Html> <Head /> <body> <ColorModeScript initialColorMode="system" /> <Main /> <NextScript /> </body> </Html> ); }如果使用 Vite + React 的纯前端方案,则把<ColorModeScript>放到index.html的<body>开头。initialColorMode="system"的含义是启动时优先跟随系统偏好,用户在组件里手动切换后,会把选择存进 localStorage,下次启动再读到用户选择。
useColorModeValue是暗色模式下最常用的 API,它在浅色和深色分别取对应的值。
const bg = useColorModeValue("gray.50", "gray.800"); const text = useColorModeValue("gray.800", "gray.100"); <Box bg={bg} color={text}> 响应式暗色内容 </Box>很多项目做了暗色模式但只有部分页面适配,原因就是这些页面用了“硬编码颜色”,比如color="#333"。这类代码不会跟随主题变化。规范做法是:所有颜色都必须走 tokens 或useColorModeValue,不能出现裸色值。可以在代码 review 时用 ESLint 插件检查是否有color="#..."、bg="#..."这类硬编码。
3.3 可访问性:Chakra 帮你省了多少事
Chakra 的可访问性设计不是事后补丁,而是组件内置的。Button 组件底层渲染为<button>并处理了onClick、onKeyDown等逻辑;Modal 打开时自动锁定背景滚动,Esc 键关闭,焦点循环在弹窗内部;Tooltip 默认由 hover 和 focus 触发;Tabs 实现了 ARIA 的role="tab"、aria-selected和键盘左右切换。
实际开发中最常用到的三个可访问性 API 是:
useDisclosure:管理isOpen、onOpen、onClose,用于 Modal、Drawer、Popover、Collapse 的显隐状态,连状态命名都统一了。useMergeRefs:把多个 ref 合并到同一个元素,这个在处理“外部传入 ref + 内部需要 ref”的场景中很实用。useId:生成稳定的唯一 id,用于跨组件关联(比如 label 关联 input)。
无头组件和 Chakra 的取舍在于:Chakra 不会完全放弃 DOM 结构和样式,它提供的是有良好默认样式但可完全定制的组件。如果团队需要极度的视觉自由,可以考虑用 Ark UI 这类无头库配合自定义样式,但大部分业务产品用 Chakra 已经绰绰有余。
4. 实战集成:搭建一个可维护的主题与业务页面
4.1 项目初始化与 ChakraProvider 配置
我建议从create-react-app或 Vite 模板开始,然后安装依赖:
npm install @chakra-ui/react @emotion/react @emotion/styled framer-motion然后建立一个theme/index.ts文件,集中管理主题配置和组件变体。
import { extendTheme } from "@chakra-ui/react"; import { buttonTheme } from "./components/button"; import { inputTheme } from "./components/input"; import { modalTheme } from "./components/modal"; const theme = extendTheme({ config: { initialColorMode: "system", useSystemColorMode: true, }, colors: { brand: { 50: "#eff6ff", 100: "#dbeafe", 200: "#bfdbfe", 300: "#93c5fd", 400: "#60a5fa", 500: "#3b82f6", 600: "#2563eb", 700: "#1d4ed8", 800: "#1e40af", 900: "#1e3a8a", }, }, fonts: { heading: `'Noto Sans SC', sans-serif`, body: `'Noto Sans SC', sans-serif`, }, components: { Button: buttonTheme, Input: inputTheme, Modal: modalTheme, }, }); export default theme;然后用<ChakraProvider theme={theme}>包裹应用。
4.2 组件状态完整性的两个关键 API
业务开发中组件状态经常不只是“受控”或“非受控”二选一。Chakra 的设计里,value与defaultValue的区分做得和 React 原生保持一致:控制组件由外部传入 value 并监听 onChange;非受控组件只传 defaultValue 即可,内部 state 管理显示内容。
比如useControllableState这个 hook,是 Chakra 内部处理这类场景的核心机制,虽然对外不太常直接用,但理解它的原理对你封装通用组件很有帮助。
import { useControllableState } from "@chakra-ui/react"; function Counter({ value, defaultValue = 0, onChange }) { const [current, setCurrent] = useControllableState({ value, defaultValue, onChange, }); return ( <button onClick={() => setCurrent(current + 1)}> 当前计数:{current} </button> ); }这个 hook 会在外部传入value时自动进入受控模式,不传时用内部 state。它还处理了一个细节:如果外部传入的 value 多次变化,内部 state 会跟随最新值,而不是固守最初的值。这里面的“如果外部受控,则内部不维护状态”逻辑让组件的边界很干净。
4.3 国际化与字体加载
Chakra 本身不涉及 i18n,但它通过ChakraProvider的localeprop 提供了内置文本(比如 Modal 的关闭按钮 aria-label)的国际化。中文项目通常要传locale="zh-CN",这样屏幕阅读器读出的描述会是中文。
字体方面,中文网站主要是字体文件太大。Chakra 能帮你切分 font-display、font-weight 等策略,但真正优化字体加载还是要靠next/font或者 CSSfont-display: swap。我个人的习惯是:在浏览器加载阶段用系统字体优先,等网络字体文件通过fontFace加载完成后再无感切换,避免 FOIT(不可见文本闪烁)。
5. 性能优化与按需加载
5.1 打包体积怎么最小化
Chakra UI 本身在支持 tree shaking 方面做得不错,配合 Vite/Rollup 时,没有用到的组件一般不会打进包里。要注意的是@chakra-ui/react的根入口会导出所有内容,如果你用 CommonJS 方式引用,可能会导致整包被打入。正确做法是确保使用 ESM 模块版本,并且开启 sideEffects 优化。
实测一个只使用 Button、Input、Box、Flex 的基础项目,gzip 后 Chakra 相关体积在 35KB 左右,这在组件库中已经算很克制的了。如果再用babel-plugin-import按需加载,体积还能进一步压缩,但 Vite 项目一般不需要额外配置。
数据密集型页面(表格、列表、长文本)更值得花心思的不是组件库体积,而是组件渲染方式。Chakra 的 Box/Stack 只是生成 CSS,本身不会导致重渲染,真正导致卡顿的是那些频繁调useColorModeValue的组件。因为useColorModeValue会订阅颜色模式的变化,使用过多会导致上下文更新触发大范围重渲染。高频更新区域里,建议把useColorModeValue的取值结果缓存到模块顶部,而不是在组件函数内每次都调用。
5.2 避免样式重复注入和 CSS 顺序冲突
Chakra 基于 emotion,会把生成的 CSS 注入到<style>标签中。如果项目里同时使用了其他 CSS 解决方案(比如 Tailwind 或普通 CSS 文件),需要注意样式顺序和优先级冲突。
我遇到过的典型问题:先引入 Chakra 的样式,又加载了一个普通 CSS 文件,结果某些基础样式(如button { background: none; })覆盖了 Chakra 的默认样式。解决方案是把普通 CSS 的 import 放到 Chakra 样式之后,或者给 Chakra 指定一个更高的cssVarsRoot作用域。
另外,如果使用 Next.js 的 App Router 架构,需要关注样式在服务端和客户端的注入顺序。Chakra 官方提供了@chakra-ui/next-js的CacheProvider,它会在服务端渲染时把生成的样式提前注入到 HTML 中,避免浏览器端样式闪烁。
5.3 图标库与图片优化建议
Chakra 没内置图标,推荐配合react-icons使用。但react-icons体积比较大(它包含所有 SVG 图标),建议用@chakra-ui/icons(Chakra 官方精选图标集)或者使用react-icons时配合 tree-shaking 配置只引入需要的图标。
import { FiUpload, FiTrash2 } from "react-icons/fi"; // 这样可以按需引入 Feather 图标如果生产包对体积敏感,建议在package.json里配置module字段让打包工具直接走 ESM 路径,并且开启optimization.usedExports。
6. 常见问题与排查技巧实录
6.1 经典问题速查表
| 问题现象 | 排查路径 | 解决方案 |
|---|---|---|
| 自定义主题色不生效 | 先确认extendTheme是否正确传入 Provider | 检查extendTheme返回值是否被用于<ChakraProvider theme={...}> |
| 暗色模式下局部样式错误 | 排查是否在 style props 里用了硬编码色值 | 用 tokens 或useColorModeValue替代 |
| Button 点击后无 hover 响应 | 检查是否在onClick里同步修改了颜色,导致样式被重渲染 | hover 态放在_hover里,不要用 className 覆盖 |
| Modal 打开时背景可滚动 | Chakra 默认禁用 body 滚动,但若外层容器 overflow 设置不当会失效 | 检查 body 或 html 是否有overflow-y: auto等属性干扰 |
| 服务端渲染样式闪烁 | 没有使用ColorModeScript或 CacheProvider | 在 document 中正确注入ColorModeScript |
| 组件类型报错 | 自定义变体未同步到类型声明 | 使用defineStyleConfig时开启definePartsStyle类型推导,或手动declare module |
6.2 排查案例:Modal 内表单自动对焦失效
Modal 打开后自动对焦到某个输入框,这是很常见的需求。Chakra 的 Modal 默认会在打开时把焦点放到最近的可聚焦元素或 Modal 容器上,但如果你用autoFocus或useEffect手动 focus,可能会被 Modal 自身的焦点管理逻辑抢过。
解决方式有两种:
- 在
ModalContent上加initialFocusRef,让 Modal 打开时把焦点放到指定元素上:
const inputRef = useRef(null); return ( <Modal initialFocusRef={inputRef}> <ModalContent> <Input ref={inputRef} /> </ModalContent> </Modal> );- 用
useEffect加上setTimeout延迟 focus,确保焦点管理逻辑执行完毕后再设置焦点。
6.3 我最想分享的一个避坑经验
如果你在extendTheme里给某个组件变体写了伪元素_after,而且伪元素的内容需要依赖调用方的 props,不要试图在变体里用props动态拼字符串。变体的样式最终会被静态序列化,动态值不会随 props 更新而重新注入。正确做法是把动态部分放在组件调用处直接以 style props 传入,或者把整个变体拆成多个静态变体,通过variant切换。
另一个印象深刻的问题是:多个组件库版本混用时,@emotion/react的实例被重复打包,导致 Chakra 的样式上下文错乱,现象是主题不生效或样式丢失。排查方法是检查npm ls @emotion/react是否只有一个实例,如果有多个,需要统一版本或配置 resolver 指向同一个副本。
7. 从实际项目出发的一些沉淀技巧
7.1 项目结构怎么划分主题文件
项目多了以后,主题文件不能写成一个巨大的theme.ts。我偏好按模块拆分:
theme/ index.ts tokens/ colors.ts spacing.ts typography.ts breakpoints.ts components/ button.ts input.ts modal.ts tabs.tsextendTheme在每个模块里负责导出局部的主题片段,最终在index.ts统一合并。这样的好处是,某天要删掉 Modal 组件的定制,直接删掉components/modal.ts和index.ts里的一行引入即可。
7.2 组件变体命名规范
给变体起名不是小事。我建议遵循“视觉语义”而非“业务名称”。例如variant="mobile-checkout"很糟糕,因为“移动端结算”描述的是场景,而不是视觉状态;variant="compact"好一些,描述的是尺寸形态;variant="primary-soft"也很好,描述的是主色的弱化版样式。这样设计系统迭代时,变体能跨页面复用,而不是只为某个一次性页面写死。
7.3 调试技巧:在浏览器里直接排查生成的样式
Chakra 基于 emotion 的序列化机制,生成的 CSS 类名是一段哈希值,不容易直接定位。调试时我建议直接打开 Chrome DevTools 的 Elements 面板,选中元素看右侧 Computed 样式,改用 style props 后,你可以直接在上面的样式面板里看到它解析出的具体 CSS 属性,这比自己去.css文件里搜索快得多。
另外,如果某些样式硬是没生效,先看看元素计算样式里是不是有all: unset之类的全局重置,Chakra 会在某些基础元素上做 reset,但不应该影响业务组件的样式。如果确认是全局 CSS 干扰,可以给元素加一个isTruncated之类的能力测试,帮助确认是布局问题还是渲染问题。
8. 最后再分享一些个人体会
用了三年 Chakra UI,有一件事我越来越确信:组件库的核心价值不在于它的按钮有多好看、弹窗动画有多顺滑,而在于它能不能帮团队严格遵循一套设计系统,并且让这套系统的迭代成本降到最低。Chakra 通过 design tokens、style props、组件变体和颜色模式这一套机制,把“设计约束”变成了代码层面可执行、可检查、可复用的东西。这一点是我以前用其他组件库时很难做到的。
如果你准备在下一个项目里引入 Chakra,我建议不要上来就追求“完全自定义所有组件”。先用默认风格跑通业务,确认设计系统对组件的核心诉求(颜色、圆角、字体、间距)后,再逐步通过 extendTheme 收敛。这样能避免一开始就被主题定制的细节拖住,也能更直观地感受到 Chakra 的默认设计有多稳。等到你对它的主题机制有了手感,再往深了定制,基本不会遇到什么阻碍。