设置页要重做,定下的方向是用 beercss(一个 Material Design 3 风格的 CSS 框架)。
第一版我是这么做的:import beer.min.css,拿它的 CSS 变量当配色来源,然后在自己的样式表里把输入框、按钮、开关、下拉——所有控件的样式完整重写了一遍,DOM 用自己的类名。
交付之后得到的反馈只有一句:「我并没看到这里有用上 beercss。」
对。我把一个组件框架降级成了一个变量字典,然后手绘了一套自己的组件。
「用某个框架」是什么意思
用它的原生组件类和外观,让框架的视觉语言透出来。 不是把它当变量来源,再自己重画一遍。
落到实处是两条:
① DOM 结构改成框架期望的样子。 去查官方文档或源码确认嵌套关系——比如 beercss 的输入框是 div.field.border > input,开关是 label.switch > input + span。
② 自己的 CSS 只做「薄覆盖层」:框架没有的布局、品牌色覆盖、少量微调。不重写组件本体。
验证也要换方法:别只看「编译过了、截图大致对」,去 DevTools 里确认框架的组件类真的在 DOM 里生效——querySelector('.field.border') 能找到东西,才算用上了。
改成真用之后,下面这些坑才一个个冒出来。
beercss 是个全局样式框架
这是理解后面所有问题的前提:它除了提供组件,还会用通配符和常见类名,悄悄改写你以为自己完全控制的属性。
下面五条都是同一轮实测踩到并修掉的。判定一律以 node_modules 里那份 CSS 的实际选择器为准——网上的文档经常是过时的。
① 圆角容器里别用 border 画直线
beercss 有这么一条:
* { border-radius: inherit; }每个元素都继承祖先的圆角。 卡片是 12px 圆角,于是卡片里的所有元素都拿到了 12px 圆角。
谁用 border-bottom 画分隔线,那条线的两端就被掰成了一段弧。实测中招三处:列表行之间的分隔线两端翘起;折叠卡片标题下的分隔线(设置页每张卡都有,存在了很久);用 border-bottom 伪造的链接下划线,弯得像给链接套了个圆角框。
规矩:
- 文字下划线用
text-decoration,别用border-bottom伪造; - 真要用 border 画线,显式写
border-radius: 0; - 或者改用
<hr>/ 伪元素。
排查时别逐个肉眼看。在 Console 里扫一遍全页「有直线 border 且 borderTopLeftRadius > 0」的元素,比看截图可靠得多。
② 起类名之前,先 grep 框架里有没有同名
我拿 .chip 做了一个静态只读的小徽标。而 beercss 的 .chip 是一个 2rem 高的可交互组件——自带定高、内边距、一圈边框、点击涟漪、焦点环。
两条规则特异性相同,我只靠「写在后面」赢了颜色和 padding——赢不掉框架已经设上、而我没写的那些属性。于是徽标比预期高一截,还多了一圈边框。
靠加 !important 一条条盖回去是错的解法。换个不撞车的名字才是。
beercss 占用的常见词远比想象的多:.chip .field .button .border .small .large .fill .active .row .grid .list .menu .tabs .circle .max .min .responsive .transparent……
起类名之前先 grep 一下框架的 CSS。
③ 覆盖了 --x,就必须一起覆盖 --on-x
Material 的变量是成对的:底色,以及它上面的前景色。
我把 --error 映射到了自己的危险色,却没动 --on-error——于是暗色下,警告按钮变成了「浅红底 + 框架自带的白字」。对比度 2.77:1,不达 AA。补上 --on-error 之后是 5.56:1。
同类的错误:拿「主色」当文字颜色。主色是给图标和线条用的,深底配主色字只有 3.03:1。
凡是「底色 + 其上文字」的组合,一律取成对设计的那两个变量。 改完用对比度公式实测,别靠眼睛——浅底浅字在亮色下看着还行,暗色下就废了。
④ 类名要真的存在——空类是静默失效
.muted 这个类在设置页里被用了 15 处。它从来没有被定义过。
beercss 里对应的是 .secondary-text(grep muted 零命中),我自己的 CSS 里也只有一个复合选择器 .warning.muted。于是那 15 处「次要说明」一直和正文同色,层级从来没生效过,也从来没报过错。
没有报错 ≠ 生效了。 新加的类名,去构建产物里确认它是一条能被解析器认出的规则,而不只是搜得到这段文本。
⑤ 组件有固定的子元素契约,别往里面塞东西
beercss 开关的画法是 .switch > span 的两个伪元素(轨道 + 滑块),选择器匹配的是每一个直接子 span。
于是我「顺手把说明文字也放进 .switch 里」之后,文字的那个 span 也被画上了一套轨道和滑块——在文字后面叠出了第二个开关。
<!-- ❌ 叠影:两个直接子 span,各画一套 --><label class="switch"><input type="checkbox"><span></span><span>发完邀评</span></label>
<!-- ✅ .switch 里只放 input + 空轨道;文字放在外面 --><label class="my-switch"> <span class="switch"><input type="checkbox"><span></span></span> <span class="my-switch-text">发完邀评</span></label>外层普通的 <label> 照样把点击转给 input——点文字也能拨,大热区不受影响。
用一个组件之前,先看它的 CSS 是按什么结构画的(
.x > span、:first-child、相邻兄弟……),照那个结构放节点。想加自己的东西,放在组件根元素外面。
还有一个配套的:开关的真 input 是 opacity: 0 藏起来当点击热区的,视觉体是后面那个 span。想做「禁用变淡」,给 input 设透明度是无效的(它本来就透明),反而可能和框架的 CSS 叠加成「淡到看不见」。
验证:走解析器,不走眼睛
改完 CSS,跑三样:
- 解析器验证:把构建产物的 CSS 切成规则数组,确认新类是真的规则对象,不是一段死文本;
- 计算样式:
getComputedStyle读实际生效值——亮暗两套各读一次; - 对比度:文字 / 底色配对算 WCAG 比值,正文需要 ≥ 4.5:1。
两个预览时的小坑:扩展页面脱离扩展环境预览时,chrome.storage 不存在,要注入一个 stub(写在构建产物目录里,下次构建自动覆盖,别进仓库);浏览器面板被隐藏时视口宽度是 0,这时量出来的尺寸全是假的——先把面板唤出来再量。
回头看,第一版那个「只取变量、重写全部控件」的做法,本质上是不信任框架:觉得自己写的更可控。
结果是既没得到框架的一致性,也没真的更可控——因为框架是全局的,它的通配规则照样会作用在你自己写的组件上。
要么真用它,要么别引它。 半用是两头的代价都付了,两头的好处都没拿到。