返回博客

设计 Token 的 border-radius 把 Checkbox 变成了圆形

组件库搭到一半,截个图,Checkbox 和 Radio 长得一模一样——全是圆的。DOM 没报错,单元测试全绿,data-checked 属性也正确。只有像素截图把这个坑暴露出来。

现象

某后台管理系统正在接入一套设计 token 驱动的组件库,全局 --radius 设为 0.625rem(10px),统一用 rounded-[var(--radius)] 覆盖所有组件以保持一致性。大尺寸组件(卡片、输入框、对话框)显示正常,但 Checkbox 渲染出来是一个完美圆形,与同页面的 Radio 在视觉上无法区分。

根因

CSS 有一条硬规则:border-radius ≥ min(width, height) / 2 时,元素渲染为圆形(或椭圆)

Checkbox 通常是 size-5(20px 正方形)。全局 token --radius: 0.625rem = 10px,恰好等于 20px / 2——正好踩上圆形阈值。

这个问题对单元测试和 DOM 断言完全透明:

  • 类名已挂上 rounded-[var(--radius)]
  • data-checked 属性正确 ✓
  • 组件功能正常 ✓
  • 视觉形状:圆形 ✗(但没有任何测试捕获这个维度)
坑:token 为大控件设计,小控件直接用会超阈值变圆

--radius 通常是按卡片、输入框等中大尺寸组件标定的。对 16–24px 的小方形控件(Checkbox、图标按钮、方形徽章)直接套用,极易超过 side/2 的圆化临界值。超过之后没有任何视觉过渡——就是一个圆,和 Radio 无法区分。

正解

用 CSS min() 函数对 token 封顶,让圆角仍然跟随 token 变化,但不允许超过小控件的圆化阈值:

// ❌ size-5 (20px) 控件,--radius=10px → border-radius:10px → 完美圆形
"... rounded-[var(--radius)] ..."
 
// ✅ 封顶在 0.375rem (6px),token 值小时跟随,token 值大时保持方角
"... rounded-[min(var(--radius),0.375rem)] ..."

选封顶值的经验法则:cap < min(width, height) / 2。对 20px 控件,0.375rem(6px)有足够余量,视觉上是清晰的圆角方形(squircle)。

Tailwind v4 的任意值语法支持 min() 里的逗号rounded-[min(var(--radius),0.375rem)] 可以正确编译为 border-radius: min(var(--radius), 0.375rem)。Tailwind v3 的任意值解析对逗号处理有时不稳定,需留意。

Radio、Avatar 这类本就该是圆的控件保持 rounded-full 不动——改造目标只是防止方形控件意外变圆。

正解:min(token, 封顶值) 同时保留响应性和正确形状

rounded-[min(var(--radius),0.375rem)] 让圆角跟随 token 缩放,但永远不会超过小控件的圆化临界。比直接硬编码 rounded-[6px] 好——后者会在用户切换主题 radius 时失去响应性。

排查与验证

截图验证,不要只看 DOM。渲染 Checkbox 和 Radio 并排,用真实浏览器截图对比角落形状;聚焦放大区域可以让圆角差异一目了然。

快速在 DevTools 里验证当前是否已成圆:

const el = document.querySelector('[role=checkbox]');
const r = parseFloat(getComputedStyle(el).borderRadius);
const side = el.getBoundingClientRect().width;
console.log(r >= side / 2 - 0.5); // true → 已是圆形(Checkbox 的 bug)

一句话外卖

面向大控件标定的 radius token,在小方形控件上直接用会静默超过圆化阈值;用 min(var(--radius), 封顶值) 封顶,token 响应性和正确形状两者兼得。