这篇文章记录的是一次真实的返工:一个已经上线、文章也写了几十篇的静态站点,因为样式越改越乱,被迫停下来重新梳理整套前端样式体系。整个过程花了两周,其中一周在拆自己以前写的代码。
如果你也遇到过「改一个按钮的颜色,结果列表页的边框变了」这种事,这篇大概对你有用。
样式为什么会先崩
样式问题从来不是突然出现的,它有三个非常典型的前兆。
症状一:同一个颜色有四种写法
打开样式表,会看到 #7C3AED、#7c3aed、rgb(124 58 237) 和 hsl(262 83% 58%) 同时存在,都表示同一个紫。它们最初都来自同一个设计稿,只是每个人复制的时候顺手改了写法。
症状二:间距靠感觉
margin: 14px、padding: 13px、gap: 1.1rem 散落在各个组件里。数值本身不算错,但它们之间没有关系,所以页面上到处是「差两像素」的错位。
症状三:改一处影响一片
这是最危险的:某个类名被多个页面复用,但复用的方式各不相同。改动它就像拆炸弹——你永远不知道谁会跟着炸。
Note前两个症状只是难看,第三个症状才是必须重构的信号。当一个改动的影响范围无法预测时,样式体系就已经失效了。
第一步:把设计变量收进一层
重构的第一步不是删代码,而是把所有裸值收进一层变量。原则很简单:组件里只允许出现变量,不允许出现具体的颜色值和尺寸值。
色板:一层色相加明度阶梯
与其定义二十个命名颜色,不如定义「一个色相 + 一套明度阶梯」。这样换主题时只改一个色相数值,整站配色会一起跟着走:
1:root {2 /* 主色:一个色相,若干明度 */3 --primary-hue: 255;4 --primary: hsl(var(--primary-hue) 70% 55%);5 --primary-hover: hsl(var(--primary-hue) 70% 48%);6 --primary-light: hsl(var(--primary-hue) 70% 92%);7
8 /* 中性色:同一色相、低饱和度,比纯灰更耐看 */9 --text-90: hsl(var(--primary-hue) 12% 12%);10 --text-75: hsl(var(--primary-hue) 10% 30%);11 --text-50: hsl(var(--primary-hue) 8% 50%);12 --line-divider: hsl(var(--primary-hue) 12% 88%);13}关键在于中性色也带上色相:纯灰(hsl(0 0% 50%))放在有色调的界面上会显得脏,而带一点点主色相的中性色会自然很多。
间距与圆角:给一套有限的台阶
间距不要自由取值,给一套 6–8 个台阶就够,所有间距从台阶里挑:
| 变量 | 数值 | 典型用途 |
|---|---|---|
--space-1 | 4px | 图标与文字的间隙 |
--space-2 | 8px | 标签内边距 |
--space-3 | 12px | 卡片内元素间距 |
--space-4 | 16px | 卡片内边距 |
--space-6 | 24px | 区块之间的间距 |
--space-8 | 32px | 页面大区块间距 |
--space-12 | 48px | 首屏与页脚 |
圆角同理:--radius-sm(6px)、--radius-md(10px)、--radius-lg(14px)、--radius-full。台阶之外不允许出现新数值,这条纪律比变量本身更重要。
在 Tailwind 里落地
用 Tailwind 的话,不必放弃实用类,只要把变量接到主题配置里:
1// tailwind.config 里的关键部分2export default {3 theme: {4 extend: {5 colors: {6 primary: "var(--primary)",7 "primary-hover": "var(--primary-hover)",8 },9 spacing: {10 // 把台阶映射成 Tailwind 的间距刻度11 13: "var(--space-13, 3.25rem)",12 },13 borderRadius: {14 lg: "var(--radius-lg)",15 },16 },17 },18};这样写有两个好处:组件里仍然用 bg-primary p-4 rounded-lg 这样直观的类名;颜色的真实来源只有一个变量,换主题时不必全站替换。
第二步:断点与栅格
断点不是越多越好
见过最夸张的项目定义了九个断点,结果每个断点都要单独调一次布局,维护成本直接翻倍。实际上大部分内容型站点只需要三到四个:
| 断点 | 宽度 | 覆盖的设备 |
|---|---|---|
| 默认 | 0 起 | 手机竖屏 |
sm | 640px | 手机横屏、小平板 |
md | 768px | 平板竖屏 |
lg | 1024px | 桌面 |
再往上通常不需要新的断点,只需要限制内容的最大宽度——让正文宽度保持在 65–75 个字符之间,比给他加断点更有效。
内容优先的调整顺序
调整布局时,按这个顺序改,返工最少:
- 先让内容能读:正文行宽、行高、段落间距;
- 再让结构能站:栅格换列、侧栏折叠;
- 最后调装饰:圆角、阴影、动效。
反过来先调阴影和圆角,内容一改就又得重来。
Tip测试时不要只拖浏览器宽度,一定要在真机上看一遍。手机上最常见的两个问题是:横向滚动条(多半是某个固定宽度元素溢出)和被虚拟键盘顶起来的输入框。
第三步:组件样式的三条纪律
纪律一:组件内不写魔法数字
1/* 不好:14px 是从哪来的?没人知道 */2.card__title {3 margin-bottom: 14px;4}5
6/* 好:来自间距台阶,改台阶就能全站同步 */7.card__title {8 margin-bottom: var(--space-3);9}纪律二:状态成组出现
一个交互元素至少有四种状态:默认、悬停、激活、禁用;如果它是链接,还有访问过;如果支持键盘,还要有焦点态。写样式时把这几种状态一次写完,不要等出问题再补:
1.button {2 background: var(--primary);3 color: white;4 transition: background-color 150ms ease;5}6
7.button:hover { background: var(--primary-hover); }8.button:active { transform: translateY(1px); }9.button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }10.button:disabled { opacity: .5; cursor: not-allowed; }纪律三:样式随组件走
样式写在组件文件里(scoped style),而不是在全局样式表里按类名遥控组件的内部结构。全局样式表只应该做三件事:定义变量、重置默认样式、处理跨组件的排版规则(例如 Markdown 正文)。
第四步:暗色模式怎么收尾
暗色模式做起来快,做对慢。核心思路是用变量推导,而不是逐个元素覆盖。
只在变量层切换
1:root {2 --card-bg: hsl(var(--primary-hue) 30% 99%);3 --text-90: hsl(var(--primary-hue) 12% 12%);4}5
6.dark {7 --card-bg: hsl(var(--primary-hue) 18% 10%);8 --text-90: hsl(var(--primary-hue) 10% 92%);9}这样一来,组件层完全不需要知道当前是哪种模式。
两件最容易漏的事
- 对比度:暗色模式下把深色文字换成浅色文字时,很容易忽略「次级文字」——它在浅色模式下是 60% 灰,直接搬到暗色背景上会低到看不清。次级文字的对比度建议不低于 4.5<1>1>。
- 媒体素材:纯白背景的图片、透明度为 1 的壁纸、亮色主题的代码高亮,都会在暗色模式下显得刺眼。处理方式是给图片容器加一层极淡的遮罩、给壁纸加上可配置的不透明度,代码高亮则直接切换主题。
Caution不要用 CSS 滤镜去「反色」处理图片——肤色、品牌色和截图里的 UI 都会变得诡异。宁可给图片加遮罩,也不要整体反色。
维护:让体系活下来
重构完成只是开始,体系能不能撑住要看后面几个月的维护。
每周一次的样式巡检
花十分钟做三件事,收益极高:
- 搜一遍裸色值(
#开头的十六进制)与裸像素值,看有没有新出现的漏网之鱼; - 检查是否有新的类名重复定义(同一组件两处样式互相覆盖);
- 在暗色模式下把主要页面点一遍,重点是表格、代码块和表单。
什么时候该重构
出现下面任意一条,就该安排重构了:
- 改一个样式需要同时动三处以上;
- 无法回答「这个颜色变量到底影响哪些页面」;
- 新页面的样式要靠复制旧页面再删改来产出;
- 每次改完都要在四个断点、两种主题下手工检查一遍。
重构不等于重写。这次我做的是「先建立变量层、再逐页替换」,全程没有推翻任何页面结构,也就没有出现长期无法发布的分支。能小步走的重构,不要用大爆炸的方式做。
回头看,样式问题本质上是约定问题:变量只有一个来源、间距只有一套台阶、状态一次写完、样式跟着组件走。这四条守住之后,代码量其实差不多,但改动的可预测性完全不同——你终于能回答「改这里会影响什么」这个问题了。
部分信息可能已经过时