目录
这套设计系统从来没有发布过,也没有版本号。它只是这个站点背后的那一层秩序——我在两年里一点一点改出来,改到后来,它开始反过来替我做事。
把它写下来,是因为真正难的部分,和我一开始以为的并不在同一个地方。
一、从四十多个文件里改一个灰色
最开始我把十六进制直接写进组件:边框 #E5E0D8,正文 #2B2A28,背景 #F5F1EA。这样写很快,看起来也完全没有问题——颜色就是颜色,何必再绕一层?
半年后我想加一套暗色模式,才发现自己给自己修了一座迷宫。同一个灰色散落在四十多个文件里,有大写也有小写,还有一处被我手抖写成了 #E4E0D8,谁都没有发现,包括我自己。
更麻烦的是,我分不清哪些灰色是「同一个」,哪些只是「看起来像」。改颜色的时候,我不像在设计,更像在考古。
那一晚我做了一件很笨的事:把全站的颜色一条条找出来,列成一张表,然后盯着它看了很久。表上其实只有十一种颜色,但在代码里,它们有一百多个名字。
二、令牌命名的不是颜色,而是意图
于是颜色被收敛成语义令牌:--bg、--surface、--text、--muted、--border、--accent。
关键不在于数量变少了,而在于它们开始回答另一个问题。它们不再说「这是什么颜色」,而是说「这里要用来做什么」。
一块颜色叫什么,应该由它承担的职责决定,而不是由它在色板上长得像什么决定。
这个转变的好处,在我第一次换强调色的时候立刻显现:需要动的地方从一个下午变成了两行。
也是从那时起,组件里不再出现任何写死的色值。不是为了洁癖,而是因为我终于明白:把决定权收在一个地方,将来才有改的余地。
三、两套色板,和一条拒绝彩虹的规则
亮色的底是旧纸白 #F5F1EA,不是纯白。纯白太干净了,干净得没有时间感,像刚拆封的纸。旧纸白带一点点黄,是你反复展开过的那张信纸的颜色。
暗色的底是夜蓝黑 #0E1018,不是纯黑。纯黑会给每个字切出一圈硬边,看久了眼睛发酸;留一点蓝,暗处看起来才像夜里,而不像屏幕被关掉了。
强调色只有两个:亮色下的暮橘 #C96F5A,暗色下的 #E08B72。后者要亮一些,因为深色背景会吃掉颜色的重量,也会吃掉它的温度。
渐变只做同色系深浅。黄昏那一条是 #0E1018 → #2A1F3D → #4A2C3A:从夜走进紫,再回到一点残留的暖,像天色一点点让出来的次序。
我不做彩虹渐变。不是因为它不好看,而是因为它太响了——在一个安静的页面里,它会成为唯一被人记住的东西。而我想被记住的,是内容。
四、把时间也变成令牌
动效真正的问题从来不是快慢,而是每一处都不一样:有的滑进来,有的弹进来,有的淡进来,像三个人各自装修了同一个房间。
后来所有曲线统一成一条 cubic-bezier(0.22, 1, 0.36, 1)。它先快后缓,收尾绵长,在最后那一小段里还在轻微地移动。像黄昏——天不是一下子黑下去的。
时长也分成两档:微交互 250–350ms,入场 800–1200ms。前者要让人确信「点到了」,所以必须快;后者要让人安静下来,所以必须慢。中间那些五百毫秒左右、说不清在表达什么的动画,全部删掉。
底下还有一条硬规则:只动 transform 与 opacity。它们不触发重排与重绘,也不会在旧手机上掉帧。好看的前提,是它得先跑得动。
五、最难的是决定不做什么
删除比添加难,因为添加有即时回报,而删除的回报要等到很久以后。
我试过让按钮弹跳。它确实可爱,前三次点击也很愉快;但第四次开始,它就变成了噪音。被它打扰的人,恰好是那些只想安静读完一篇文章的人。
所以有了这张清单:不做弹跳,不做旋转,不做闪烁,不做会忽然放大的悬停;移动端关掉视差和鼠标跟随——在没有鼠标的地方,跟随只是一种延迟。
克制不是少做一点,而是为了那件真正重要的事,把其余的都让开。
这句话听上去像自我安慰。但每一次忍住不加,后来的我都会感谢当时的我。
六、中英混排,是排版里真正难的部分
这个站是双语的,同一套样式要同时容得下中文和英文,而它们对「舒服」的定义并不一样。
汉字是方块,密度大,需要放松:行高 1.8,行宽 36–40em。压到 1.5 就会显得拥挤,读两三行就想移开眼睛。
英文靠小写字母的轮廓与词间空隙呼吸,行高 1.6 就够,但行宽要到 65–75ch。短了会不断折行,读起来像一边下楼一边看书。
字体也分开:中文标题用霞鹜文楷,笔画里有手写的松弛,能让标题不那么像一则公告;英文标题用 Newsreader,衬线很细,气质安静。
同一段话,在两种语言里需要两套呼吸的节奏。这件事没有办法靠一个数值解决,只能分别照顾。
七、一次失败:把 max-w-[20ch] 加在容器上
这是我最想写下来的一段,因为它错得非常具体。
为了不让大标题一行拖得太长,我在包裹它的容器上加了 max-w-[20ch]。理由听上去很简单:限制在二十个字左右。
结果标题变成了一座每行只有一个字的高塔。
原因是我忘了 ch 是相对单位——它按元素自身的字号计算。容器是 17px 的正文尺寸,20ch 算下来不到 400px;而里面的大标题是 44px,宽度自然只够放下一个字。
正确的做法,是把宽度约束加在真正承载大字的那一个元素上,让它按自己的字号去算。
宽度约束要加在真正承载大字的那个元素上。
这个 bug 教给我的不只是 ch 的定义,而是一种更通用的直觉:约束要写在它所约束的东西身上。
八、尊重 prefers-reduced-motion 是底线,不是加分项
有人会因为前庭功能的原因,被动效弄到头晕、恶心。这不是偏好问题,是身体反应。
所以在 prefers-reduced-motion 生效时,我会把入场动画全部关掉,只留下一点透明度的变化——让内容出现,而不是让内容动起来。
它不应该被算作加分项。一个页面如果必须动才能用,那它本来就没有想清楚。
这套东西没有发布,没有版本号,也没有文档,只有几个还带着注释的文件。它真正留下的是一种习惯:当某样东西需要改变时,有唯一的一个地方可以改。
这就是全部的回报。不是规则的数量——大部分规则我早就忘了——而是下一次改动比上一次便宜。
暮色听起来很轻,但让它稳住的东西一点都不浪漫:一组名字取得足够无聊的变量,一条从来没被改过的曲线,和一张写着「不做」的清单。
天不会一下子黑下去,页面也不应该。
