设计 token 系统与主题
YouTeacher 落地页把每一个视觉决策——颜色、字体、间距、圆角、阴影——都收在一个 CSS 自定义属性文件里,src/styles/design-tokens.css,声明在 :root 上。这个文件自称是唯一真相来源(single source of truth),其余样式都从它读取,而不是各自硬编码数值。
Token 层
token 分成几族:
- 品牌色。 主色是暖橙(
--color-brand),配有更浅的 hover 与更深的 active 变体;另有一个板岩蓝的次要色和它的浅色版本。 - 中性色。 正文、次要文字、反白文字;页面与卡片背景,各自都有独立的深色对应值;边框色;以及一对「赞助/精选」色。
- 排版。 两个字体族——
--font-brand(Rubik,用于标题)与--font-body(Inter),各带系统回退字体。一套固定的字号阶梯从--font-size-h1一路到正文、小正文、caption;三档字重(regular/medium/semibold);一组行高;以及两个负值字间距,用于收紧大标题。 - 间距。 一套编号阶梯(
--space-1…--space-20),以像素为单位,从 4px 到 80px。 - 圆角与阴影。 圆角从小到全圆(一个用于胶囊形的大值),以及三级投影。
如何被消费
src/app/globals.css 先引入 Tailwind,再引入 token 文件,在 :root 上用 token 设定基础的 font-family、background-color 与 color,做 border-box 重置,并把标题元素(h1–h4)映射到字号与字重 token 上——其中最大的两级标题(h1、h2)还用上行高与字间距 token。组件则通过 CSS Modules 消费同一批变量:例如 Header.module.css 引用的是 var(--color-bg)、var(--space-4)、var(--radius-md) 和字体 token,而不是字面值。这正是设计能「一处修改、处处生效」的原因——token 是设计指南与组件之间的接口。
深色模式
主题是分层的,操作系统偏好与用户显式选择都被尊重,且用户选择优先:
- 自动。 一个
@media (prefers-color-scheme: dark)块覆盖中性色 token——但它被限定在:root:not([data-theme="light"]),所以只有在用户没有手动锁定浅色时,跟随系统才生效。 - 手动。 另一条
:root[data-theme="dark"]规则无条件设置同样的深色值,因此显式选择深色时,即使在浅色系统下也会胜出。
手动路径依赖根元素上的 data-theme 属性;页头带有一个主题切换控件来驱动它。深色块重定义中性色 token(文字、背景、边框),并把板岩蓝的次要色换成它的浅色值;主品牌橙色和结构性 token(字体、间距、圆角)在两种主题下保持不变。
响应式字号
字号阶梯以「移动优先 + 覆盖」的方式实现:token 文件里的 @media (max-width: 767px) 块缩小标题字号并收紧两个较大的间距档,于是每个读取标题 token 的组件都会一起缩小,无需逐组件写媒体查询。组件仍会在布局需要处加自己的断点——例如页头在 480px 以下收紧内边距与 logo,而 globals.css 在 pointer: coarse 下强制 44px 的最小触控目标。
为什么是这个形状
一个 token 文件、处处读取,意味着改一个颜色或间距只需一处编辑;三路主题规则让应用默认尊重操作系统、又允许用户覆盖;而移动优先的覆盖让一套字号阶梯在各种屏幕上保持一致,而不是把尺寸散落到各个组件里。