文档索引

设计哲学

这不是一份组件规范,也不是教程。

它讲的是这个站对界面的看法:相信什么、拒绝什么、碰到没写过的情况怎么判断。 具体数值是这套看法的当前实现,会变;看法本身是判据,不变。

文末附「一页参照」,够你把同一套观感复制到别的项目、做出大差不差的页面。 先读看法,再看数字 —— 反过来只会得到一堆"像,但不对"的东西。

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. 碰到没写过的情况,怎么判断

先试减法,再试加法。 优先级是固定的:

底色 → 留白 → 形状 → 线条/阴影 → 装饰动效

永远先试前面的。试完了还不够,才轮到后面的,并且要能说出为什么前面的不够。 这条减法序列管的是装饰。交互状态反馈是基线:悬停、按下、聚焦、选中、展开、加载、 报错、禁用都不能靠瞬时跳变,也不需要先证明"前面的不够"。

再找轴。 这个站的所有控件都落在三根正交轴上:

  • 层级轴:主要行动 / 辅助操作 / 导航切页 —— 由底色梯度表达。
  • 严重性轴:常规 / 危险 —— 由色相表达。
  • 形态轴:单行 / 容器 / 次级 —— 由圆角表达。

"危险按钮"是层级 × 严重性的组合,不是第四档;"已点赞"是激活度 × 功能色的组合, 不是另一种控件。新东西先往这三根轴上放:放得下,就复用;放不下,才说明它是新语义, 需要新形式 —— 而新形式要付出"用户重新学一遍"的代价。

最后过四问:

  1. 它在编码什么语义?
  2. 去掉它,用户会误解什么?
  3. 它和已有的哪个形状 / 颜色 / 时长同义?
  4. 它是不是长在这个组件上?同壳、同源、对位吗?

有一个答不出,就先别加。

6. 它不管什么

这套看法只管界面怎么表达。以下它不表态,各项目自己定:

  • 文案的具体内容(只要求遵守 §2.7)。
  • 业务规则、信息架构、页面上放哪些模块。
  • 技术栈。它是 CSS 与交互的立场,不是框架选型。
  • 品牌插画、海报、图表的数据着色 —— 那些按内容自己定义,只要求"外壳仍走令牌"。
  • 具体像素。数值属于实现,见附录。

7. 六个自检问题

发布前拿这六条过一遍,比对着规范逐条核对有效:

  1. 覆盖:这个交互状态有没有过渡?没有的话,用户能不能看出刚才发生了什么?
  2. 归属:动效是不是长在组件上 —— 同壳、同源、对位?还是从别处淡入的一层效果?
  3. 语义:它在说明什么关系(同一性 / 因果 / 顺序)?说不出来,它是不是只是装饰?
  4. 删减:去掉它,用户会误解什么?状态反馈不能删;只有装饰才适用"不会误解就删"。
  5. 布局与双重状态:状态变化会不会推动布局?有没有两个状态同时发亮? 颜色是不是唯一的信息来源?
  6. 极端条件:换到深色主题、窄屏、纯键盘、减少动态效果,它还是同一个意思、 同一套操作吗?不是,说明它编码了自己没打算编码的东西。

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 的可移植提炼:那份是站内唯一权威,写着每个 组件落在哪个文件、被哪条测试钉住;这份讲的是"为什么"和"没有本站代码时怎么判断"。

  • 两者数值一致;改数值要两处一起改。
  • 这份刻意不写文件路径、组件文件与测试名 —— 那正是它搬得走的原因。
  • 如果两份打架,以那份为准:它是实现,这份是看法。

本页内容来自仓库 docs/design-philosophy.md·在 GitHub 上查看

设计哲学 · 聪明山