设计哲学
这不是一份组件规范,也不是教程。
它讲的是这个站对界面的看法:相信什么、拒绝什么、碰到没写过的情况怎么判断。 具体数值是这套看法的当前实现,会变;看法本身是判据,不变。
文末附「一页参照」,够你把同一套观感复制到别的项目、做出大差不差的页面。 先读看法,再看数字 —— 反过来只会得到一堆"像,但不对"的东西。
1. 它相信的一件事
视觉是编码,不是装饰。
界面上的每一个可见属性 —— 形状、底色、阴影、间距、动效、字重 —— 都在向用户传递信息。 既然在传递信息,就必须只传递一件事,而且那件事必须是用户需要的:"这里能点"、 "这块更重要"、"刚才发生了什么"。传不出信息的视觉,就是噪音。
这条信念有个推论:一个样式说不出它在表达什么,就该被怀疑。 不是所有可疑项都要删 —— 有一种东西叫"性格"(见 §2.5),它是合法的; 但"性格"必须先承认自己不是信息,别把自己打扮成道理。
2. 七条信念
每条都按同一副骨架写:它在说什么 · 它在屏幕上长什么样 · 违反之后坏在哪。
2.1 层级靠底色,不靠线条
页面、卡片、卡内的块,是三阶底色:纸 → 纸上的卡片 → 卡片里的内容区。 越往里越"被压进去",越往外越"浮起来"。线条、边框、投影都不是层级工具 —— 它们只在表达状态时才出现(能点、聚焦、浮层)。
坏在哪: 描边是最贵的一层 —— 它同时吃掉视觉预算和布局预算。一旦人人都在描边, 就没人能靠它区分任何东西;页面看起来"有结构",实际上层级已经消失了。
2.2 形状是分类,不是风格
圆角在这个站里不是"哪档顺眼",是名词的性:胶囊是单行可操作的东西,大圆角是容器, 小圆角是容器里的次级条目。头像是 8% 方角,因为"人"不是按钮;带淡色底座的小图标 则一律圆形 —— 圆是"符号",方角是"人",同一页里也不会混。
坏在哪: 形状一旦按页面随手调,它就不再是信号。用户看到的每个圆角都要重新学一遍 "这个能不能点"。规范也就退化成审美偏好。
2.3 状态属于颜色,不属于尺寸
悬停、聚焦、按下、报错、加载 —— 这些都不是内容,所以不许改动内容占的地方。 反馈只用颜色、阴影、透明度、位移表达,因为它们不参与布局计算。
坏在哪: 一个 translateY(-2px) 的悬停会让整排卡片抖一下;聚焦时给输入框加边框,
字段会变高、下面的内容会往下跳。状态一动尺寸,用户手指就是在打移动靶。
2.4 一形一义
同一个形状、同一个颜色、同一个时长,全站只有一个含义。要表达新含义,就换形式 —— 不能借用。红只能是"危险"和"撤销",不能拿来当"这是蓝色的兄弟";绿色不能变成万能的 "已选中"。
坏在哪: 两处同形不同义,用户就开始靠上下文猜。猜错一次是困惑,猜错三次是 不再相信界面。
2.5 动效长在组件上
动效不是给组件盖的一层效果,而是组件在两种形态之间变形。判断一个动效,先看四条轴: 覆盖(哪些状态要动)、归属(这个动效属于谁)、语义(它说明什么关系)、 性格(它动起来是什么手感)。
覆盖:所有交互状态都要有过渡。 悬停、按下、聚焦、选中、展开、加载、报错、禁用、数值变化、增删排序 —— 只要状态变了, 用户就应该看得见它是怎么变的。没有过渡不是"克制",是还没做;瞬时跳变只允许出现在 减少动态效果里,或首屏的测量 / 水合修正上(那不是用户操作)。下面先讲归属与语义, 它们回答的是"这个过渡该长什么样"。
归属:动效必须长在组件上。
- 同壳:收起态与展开态共享同一套外壳几何与材质 —— 同一宽度基准、同一锚定边、 同一圆角族、同一底色和阴影。面板从触发器的边界长出,中途不出现第二个独立浮层。
- 同源:生长点必须是用户按下的那个东西。分类面板从分类胶囊展开,悬浮球从右下角 那颗球向左上长,顶栏从汉堡向下铺。用户不需要寻找"它从哪来"。
- 对位:收起态与展开态都存在的部件要一一对应 —— 加号变成"创建"、头像从右端移到 左上、关闭钮从头像上移到右上,都是同一颗元素在变形或位移;展开态新增的内容只能 由外壳内部让出,不能从壳外飞入或原地淡入。数量可以变,血缘不能断。
- 利落:一次手势只有一条主几何时间线,其余运动都是它的从属;走最短路径;内容跟着 外壳走,不能先于壳出现(会溢出),也不能明显滞后(会空壳);裁剪始终在外壳圆角内; 关闭时内容先退、外壳后收;不加回弹、弹簧和与部件无关的并行运动;反向操作从当前值 续接,不跳回起点。
语义:在归属之上,动效有三种职务,按优先级排。
第一层:同一性("刚才那个东西就是现在这个")。
手机端目录那颗悬浮胶囊:收起时是个加号,展开后变成"创建"按钮 —— 同一条边、同一个锚点,
宽度从 44 长到 105,图标旁边长出文字。它不是"图标消失、按钮出现",是同一个东西
长开了。抽掉这 480ms,用户会以为换了个控件,得重新找"创建"在哪。
第二层:因果与顺序("从哪来、先去后谁")。
面板从触发它的那条边展开,不是从天上掉下来;顶栏收起时内容先退(150ms)、空壳后收
(220ms)—— 否则读起来是"点完还赖着不走";导航项依次入场,说明它们是一组。
第三层:性格("这个站快,还是糯")。
曲线与时长决定手感。顶栏展开 480ms、起步快、末段缓,收起只要 220ms;这个不对称
是性格。但注意它选曲线的理由仍然是可用性:"对称的 ease-in-out 前半段几乎不动,
读起来像点了没反应。"
没有第四种职务。 三层都不承担、又不是组件状态变化的孤立动画,是装饰。装饰可以存在,
但它不是交互反馈,也不能拿来代替反馈。装饰要成立,得同时满足四条附加条件:
低频或一次性;不承载唯一信息;不改变布局;能被 prefers-reduced-motion 关掉、
且关掉之后功能不减。
装饰的典型是无限重复又没有信息增量的运动 —— 一个永远在呼吸的小圆点。它不解释 任何关系,只是在替你着急。它不该被当成"所有交互状态都要有过渡"的反例:那条要求管的是 状态反馈,不是氛围。
2.6 颜色各司其职
品牌蓝是"本站",红是"坏",绿是"好",黄是"收藏"。这四种之外的颜色只有一个职务: 把并列的东西分开(分类色)。它们不承载褒贬,也不许借用前四种的职务。
坏在哪: 一旦红色也用来"区分板块",用户看到红色就不知道该紧张还是该继续读。 颜色的信息价值被摊薄,最后所有颜色都只是色块。
2.7 文字只做功能
功能名、必要说明、操作提示、状态反馈。不写抒情句、不写凑气氛的副标题、不替用户 表达情绪。
坏在哪: 每一句"氛围文案"都是读者要过滤一次的噪音。写的人觉得温暖,读的人 每次都要多花半秒判断"这句要不要看"。
3. 这套看法在屏幕上长什么样
如果只看截图,它的性格是这样的:
- 浅灰的纸,白色的卡片;卡片不描边、不投影,安安静静躺着。
- 区域之间也没有分栏线:侧栏、主区、输入区靠底色与留白分开; 线只留给有内容语义的分隔(引用块、日期、新消息)。
- 全站只有一束光,是品牌蓝;它只有两个用途:托起(可点的卡片悬停、浮层) 和聚焦(正在输入的字段)。没有黑色阴影。
- 所有能点的小东西都是胶囊;不能点的东西没有手形指针、不发光的边框。
- 字号差距很大:页标题可以到
3.2rem,正文0.9rem。层级靠这个落差建立, 不靠加粗、不靠颜色堆叠。 - 深色不是浅色的反相:关系不变,明度重排。所以两套主题看起来是同一个站。
- 数字(金额、页码、日期)等宽 —— 它们会跳动,跳动时不该带着版面一起跳。
- 所有交互状态之间都有连续的过渡:颜色反馈
150–250ms,形变480 / 220ms; 点下去不会突然换一张脸。首屏的测量与水合修正不在此列。 - 一张页面读起来像排好的一张纸,不像拼贴。
一个顶栏里同时住着这三层:展开的先后是逻辑,480ms / 220ms 的不对称是性格,
而"讨论"角上那颗红点的呼吸 —— 是氛围装饰。它不承担交互反馈,也不在"所有交互状态都要
有过渡"的覆盖要求之内;但它同样要能在减少动态效果时停下。正确的关系不是"消灭第三个",
而是知道自己在用第几个。
4. 它拒绝什么
这些不是"不建议",是明确不做。顺手加进来之前,先想清楚代价。
| 拒绝 | 因为 | 常见的诱惑 |
|---|---|---|
| 用边框表达层级 | 层级该由底色承担,边框会吃掉两种预算 | "这卡片和背景太像了,描个边吧" |
| 用位移/缩放表达可用 | 状态一吃布局,用户就在打移动靶 | "加个上浮会更有质感" |
| 两态同亮 | 看着像选中、其实没选中,等于界面说谎 | "悬停时也给点选中色吧" |
| 给一个控件加第四档 | 三档已经用尽"重要程度",第四档必然和某一档重义 | "这个按钮比较特殊" |
| 第三种阴影 | 全站只有"托起"和"聚焦"两束光 | "这里需要更重的阴影" |
| 无家的动效 | 动效必须长在组件上;同壳、同源、对位缺一条,就只是盖在界面上的效果 | "页面切换顺滑一点" |
| 凭空淡入的面板 | 没有空间来源,用户要重新寻找它从哪来 | "加个淡入显得柔和" |
| 旧控件淡出、新控件淡入 | 同一性断了,读起来像换了个控件 | "两个状态各做一个组件更好写" |
| 外壳先长、内容迟到 | 中途出现空壳,一次手势被切成两段 | "先长壳再淡入更稳" |
| 回弹 / 弹簧 / 装饰性错峰 | 与部件变化无关的运动只会抢注意力 | "加一点弹性会更有质感" |
| 只靠颜色传达状态 | 色弱、深色主题、截图转述都会丢信息 | "红绿已经够明显了" |
| 把旧实现写成通用规则 | 现状不是标准,只是现状 | "代码里就是这么写的" |
| 界面替用户抒情 | 文字只做功能 | "加一句温暖的话吧" |
5. 碰到没写过的情况,怎么判断
先试减法,再试加法。 优先级是固定的:
底色 → 留白 → 形状 → 线条/阴影 → 装饰动效
永远先试前面的。试完了还不够,才轮到后面的,并且要能说出为什么前面的不够。 这条减法序列管的是装饰。交互状态反馈是基线:悬停、按下、聚焦、选中、展开、加载、 报错、禁用都不能靠瞬时跳变,也不需要先证明"前面的不够"。
再找轴。 这个站的所有控件都落在三根正交轴上:
- 层级轴:主要行动 / 辅助操作 / 导航切页 —— 由底色梯度表达。
- 严重性轴:常规 / 危险 —— 由色相表达。
- 形态轴:单行 / 容器 / 次级 —— 由圆角表达。
"危险按钮"是层级 × 严重性的组合,不是第四档;"已点赞"是激活度 × 功能色的组合, 不是另一种控件。新东西先往这三根轴上放:放得下,就复用;放不下,才说明它是新语义, 需要新形式 —— 而新形式要付出"用户重新学一遍"的代价。
最后过四问:
- 它在编码什么语义?
- 去掉它,用户会误解什么?
- 它和已有的哪个形状 / 颜色 / 时长同义?
- 它是不是长在这个组件上?同壳、同源、对位吗?
有一个答不出,就先别加。
6. 它不管什么
这套看法只管界面怎么表达。以下它不表态,各项目自己定:
- 文案的具体内容(只要求遵守 §2.7)。
- 业务规则、信息架构、页面上放哪些模块。
- 技术栈。它是 CSS 与交互的立场,不是框架选型。
- 品牌插画、海报、图表的数据着色 —— 那些按内容自己定义,只要求"外壳仍走令牌"。
- 具体像素。数值属于实现,见附录。
7. 六个自检问题
发布前拿这六条过一遍,比对着规范逐条核对有效:
- 覆盖:这个交互状态有没有过渡?没有的话,用户能不能看出刚才发生了什么?
- 归属:动效是不是长在组件上 —— 同壳、同源、对位?还是从别处淡入的一层效果?
- 语义:它在说明什么关系(同一性 / 因果 / 顺序)?说不出来,它是不是只是装饰?
- 删减:去掉它,用户会误解什么?状态反馈不能删;只有装饰才适用"不会误解就删"。
- 布局与双重状态:状态变化会不会推动布局?有没有两个状态同时发亮? 颜色是不是唯一的信息来源?
- 极端条件:换到深色主题、窄屏、纯键盘、减少动态效果,它还是同一个意思、 同一套操作吗?不是,说明它编码了自己没打算编码的东西。
8. 这些看法是踩出来的
它们不是审美偏好,是一份故障清单。每一条背后都有一次"看起来一切正常"的事故:
- 黑色阴影退役 —— 卡片层级的活儿交给底色之后,中性阴影就没有职务了。留着它, 等于允许两种方式表达同一件事。
- 按钮悬停的
translateY(-1px)被整批清除 —— 十几处各自加的"上浮",按下时 整排按钮一起跳。 - 输入框聚焦不再加边框 —— 边框参与布局,字段随焦点长高,下方内容位移。
- 结构性分隔线退役 —— 讨论区与通知页的分栏线、区段横线删掉之后,层级依然成立: 它本来就是底色承担的,那些线只是把同一句话说第二遍。
- 变量名拼错,整条声明静默失效 —— 未读底色被拼成
-unrend,浅色主题下永远是transparent,只有深色是好的。没有任何报错。 - 规范宣布退役、代码还留着残骸 —— 文档比代码先退役,谁也不知道另一边改了没。
最后两条特别能代表这套哲学:静默失败比报错贵得多。 这个站很多条款长得像洁癖 ("用变量前先确认它存在"、"同一规则只定义一处"、"差异必须写明对象和用途"), 本质上都是在把静默失败改成显式失败。
9. 它自己也没做到的地方
哲学最容易变成自我表扬。所以要说清楚:下面这些是站上还没对齐的地方。
- 覆盖尚未铺开。 不少交互状态仍是瞬时切换:
/explore的旧栏目侧栏还在用max-height+transition: all .3s,顶栏部分图标容器也用transition: all。 这是待补清单,不是设计上的"克制"。旧版文档曾把四个已实现的动效当成全站范围, 把"还没加"读成"只保留必要";这一版明确:覆盖是轴,不是由现有实现反推的边界。 - 顶栏三颗小圆点的无限脉冲(
2s infinite:讨论未读、通知数、在线状态)。它不解释 任何关系,只持续吸引视线 —— 按 §2.5 属于装饰,不是交互反馈。更要紧的是:它是全站 少数没有在prefers-reduced-motion里关掉的动画。 - 首页的入场与日月旋转(
480ms)更接近氛围而不是因果。它们在减少动态效果时会被关掉, 所以合规;但严格说,它们属于第三层"性格",不该被叫做"逻辑"。 - 同一份规范里仍有口径漂移:收藏用的星色写着"仅用于收藏",行内代码也在用它;
通用卡片类是
10px圆角,而正文说普通卡片是30px。每次发现这种漂移,要么改实现、 要么改看法,不能两边都当没看见。
这一节的意义是:这套哲学必须能被代码反驳。 一段永远背离实现的规范,最后会变成 没人读的吉祥物;一段承认自己哪里没做到的规范,才有资格要求别人照做。
附录 A:一页参照(实现,不是哲学)
数值会变,哲学不变。下面这些够你把观感复制走。
颜色(浅 / 深)
| 职务 | 浅色 | 深色 |
|---|---|---|
| 品牌 / 本站 | #2563EB |
#23A5FF |
| 纸 / 卡片 / 卡内 | #F8FAFC / #fff / #F4F6F9 |
#131517 / #181A1D / #21252A |
| 主要 / 辅助文字 | #0F172A / #64748B |
#F1F5F9 / #94A3B8 |
| 危险 / 成功 / 收藏 | #ea3b3b / #10b981 / #f1c40f |
#FF3535 / #10b981 / #ffd93d |
淡底一律是同色 10%–12%。品牌色只有三束光:
0 2px 25px rgba(37,99,235,.15)(托起)、0 0 25px rgba(37,99,235,.28)(聚焦)与
0 2px 14px rgba(37,99,235,.35)(开启态:开关 / 勾选项处于开启时)。
右上角 Toast 是例外:它不投影 —— 状态色 secondary 淡底 + 背景模糊,文字用状态色。
形状:胶囊 999 / 卡片 30 / 多行字段 20 / 次级块 10 / 内联标记 5 /
头像 8%(方角,不走圆形底座)/ 带淡色底的小图标与圆形按钮 50%。
字级:页标题 3.2rem/700、卡片标题 1.3rem/600、正文 0.9rem、辅助 0.8rem、
最小 0.75rem;数字等宽。字体 Kumbh Sans / Outfit / JetBrains Mono,均为 SIL OFL,可自托管。
动效:换色 250ms、按下 150ms、悬停与聚焦 200ms、滑块 250ms、
内容淡入 180ms(可延后 55ms)。形变只有两条节奏:
展开 480ms cubic-bezier(0.16, 1, 0.3, 1)、收起 220ms cubic-bezier(0.4, 0, 0.2, 1) ——
外壳、宽度、位移、旋转、网格展开、入场与退场都用这两条;主题换色与首页日月同为
480ms、同一条展开曲线。顶栏展开 480ms / 收起 220ms(内容 150ms 先退,导航项
45ms + 35ms×n 依次入场)。
归属:所有动效长在组件上 —— 同壳、同源、对位、利落;展开态与收起态都存在的部件 一一对应,新增内容只能由外壳内部让出。覆盖:所有交互状态都要有过渡;缺动画是待补, 不是设计意图。
布局:容器 960 / 1140,左右 16;断点 576 / 768 / 992 / 1200;
两栏时侧栏 280 + 间距 30。
三档按钮
| 档 | 空闲 | 悬停 | 按下 |
|---|---|---|---|
| 主要 | 品牌淡底 + 品牌字 | 品牌实底 + 白字 | 压暗 |
| 次级 | 透明 + 辅助文字色 | 变成主要档的空闲态 | 变成主要档的悬停态 |
| 切页 | 同次级;当前项常驻淡底 | 当前项保持 | 保持淡底,不闪实底 |
它落到一页上(目录页;只画关系,不画细则)
┌ 固定顶栏 62 ────────────────────────────────────┐
├ 满幅标题带(卡片底色,内容仍套容器)─────────────┤
│ ┌────────┐ ┌──────────────────────────────┐ │
│ │侧栏 280 │ │ 胶囊工具条(sticky,控件 42) │ │
│ │吸顶 74 │ │ 卡片 · 卡片 · 卡片 │ │
│ └────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────┘
≤992:侧栏移出文档流,收成右下角一颗 129×70 的悬浮胶囊;搜索独占工具条首行。
一张卡片:白底、30 圆角、内距 20 23 23 28、间距 16;标题与统计同一行;
摘要两行截断;底部一行是头像 20 + 作者 + 栏目 + 日期;整卡可点时悬停只亮"托起"那束光。
一次展开:触发器与面板共用同一个外壳,收起是胶囊、展开保持 21 圆角,
沿内容高度伸展(展开 480ms / 收起 220ms,同那两条节奏),内容随后淡入
(这是内容在出现,不是状态在改尺寸);按钮、头像、关闭钮在前后两态是同一颗元素,
面板内容由外壳裁出,不从别处淡入。关掉动画时直接到位,功能不变。
附录 B:与站点实现的关系
这份文档是 docs/frontend-styles.md 的可移植提炼:那份是站内唯一权威,写着每个
组件落在哪个文件、被哪条测试钉住;这份讲的是"为什么"和"没有本站代码时怎么判断"。
- 两者数值一致;改数值要两处一起改。
- 这份刻意不写文件路径、组件文件与测试名 —— 那正是它搬得走的原因。
- 如果两份打架,以那份为准:它是实现,这份是看法。