skip to content

三层交互组件,一页 design system 活样板

notes 不只是文字。站点按「表现复杂度」分三层承载交互,够用就停在最轻的一层——越往下 JS 成本越高。这篇就拿 STEM 里最美的几个理论当题材,把三层挨个演一遍:每个图都只吃 /design-system 那套 token(颜色、间距、圆角、动效都是 CSS 变量),跟着 data-theme 变色、尊重 prefers-reduced-motion、带无障碍标注。换句话说,整页就是一份活的 design system 样板——切右上角主题,下面每张图都会当场重新上色。

先认色:一套 token,几种角色

切主题时每种 token 各有脾气,用错角色就会在某套主题下失真:

  • accent--color-accent)——唯一的主角色,且跨主题换相:浅色胭红、深色玉绿(即 Ember · Jade)。承载主操作、链接、单序列标记、焦点。
  • 分类色--color-cat-1…6)——多序列各占一槽,跨主题稳定:换主题只调明度、不换相,所以一条线在明暗两套里都认得出是「它」。
  • sequential 阶梯--color-seq-accent|steel|neutral-1…6)——表达量级 / 序号 / 浓淡的单色梯。
  • 语义 / 基础设施——盈亏用 --color-positive / --color-negative,网格轴线用 --color-grid / --color-axis

选色一句话:单序列或 UI 用 accent,多序列用分类色,量级 / 序号用 sequential。全部是字面 oklch、明暗两套各写一份——所以 SVG 能用 var() 直接吃,Canvas 也能用 getComputedStyle 读到。

第 1 层 · animated-SVG(零 JS)

几何、坐标、元素有限的图——首选这层。矢量无损缩放,颜色用 currentColorvar(--color-*)切明暗零 JS 自动变色;动画用纯 CSS,在系统「减少动态效果」时自动静止。

先从最朴素的匀速旋转开始——三条同心轨道,accent 描点:

三层轨道示意 围绕中心节点旋转的三条同心轨道,演示零 JS 随主题变色的矢量动画。
三条同心轨道绕中心匀速自转——纯 CSS 动画,切主题零 JS 变色。

把旋转搬到复平面,就是欧拉公式 eiθ=cosθ+isinθe^{i\theta} = \cos\theta + i\sin\theta:复指数即单位圆上的匀速转动。两段「本轮」嵌套(外臂转、内臂绕外臂端点转),末端描出的正是傅里叶级数「圆叠圆」的最小单元——accent 配一抹分类色:

欧拉公式的旋转本轮 单位圆上两段嵌套的匀速旋转臂,演示 e^(iθ) 与傅里叶本轮,纯 CSS 动画、零 JS 随主题变色。
欧拉公式的嵌套本轮:外臂 accent、内臂分类色,纯 CSS 旋转。

频率一旦不只一个,就有了花样。Lissajous 图形是两个垂直简谐振动 x=sin(aθ+δ)x=\sin(a\theta+\delta)y=sin(bθ)y=\sin(b\theta) 的合成,频率比 a:ba:b 决定形状。路径在构建期算好,运行时只用 CSS stroke-dashoffset 让高光沿线巡游——三条曲线三种分类色:

Lissajous 谐波图 三条不同频率比的 Lissajous 封闭曲线,各有一段高光沿线巡游;构建期生成路径、运行时零 JS。
三条不同频率比的 Lissajous 曲线,高光沿线巡游;构建期生成路径、运行时零 JS。

静态几何同样能很美。黄金比 φ 把边长 1,1,2,3,5,8,13,21 的正方形铺成一个 φ 比例矩形,叠加对数黄金螺旋——方块沿 sequential 阶梯由浅到浓,正好展示单色梯:

斐波那契方块与黄金螺旋 边长 1,1,2,3,5,8,13,21 的正方形铺成 φ 比例矩形,叠加对数黄金螺旋;按 sequential 阶梯上色。 1 1 2 3 5 8 13 21
斐波那契方块按 sequential 阶梯上色,叠加对数黄金螺旋(accent)。

第 2 层 · Canvas + JS

大量点、连续曲线、需要逐帧重算的图(波形、粒子、混沌)——SVG 节点会爆炸,改用 Canvas。代价是 Canvas 不自动变色:得用 getComputedStyle 取 CSS 变量、用 MutationObserver 监听 data-theme 重新取色重画,并做 devicePixelRatio 适配。取色只读「两套主题都是字面 oklch」的 token——这恰好覆盖整套调色板:accent、分类色 cat-*、sequential 阶梯都行;别读 --color-foreground / --color-background,它们在浅色下是 var() 链,getComputedStyle 可能返回未解析值。

最基础的连续曲线是波。两条叠加正弦波分别取 accent 与 cat-5(steel)——切主题时 accent 波翻红↔绿、steel 波守住钢蓝(只随主题调明度),正是「主角换相、分类色稳定」:

随主题变色的叠加波形动画
accent 波随主题翻红↔绿、steel 波守住钢蓝;逐帧重算,离屏自动暂停、reduced-motion 下只画一帧静态。

点一多,就能让 sequential 阶梯大显身手。叶序排布把第 ii 颗种子放在极角 i×137.5i \times 137.5^\circ(黄金角)、半径 i\propto\sqrt{i} 处——自然界最省空间的堆叠。几百颗点按序号在 oklch 数值上插值上色,得到平滑的单色渐变:

黄金角叶序排布,按序号沿 sequential 阶梯上色
黄金角叶序排布,几百颗种子按序号沿 sequential 阶梯平滑上色。

同样是逐帧,洛伦兹吸引子展示确定性混沌之美:三条耦合微分方程积出永不自交、对初值极度敏感的「蝴蝶」。沿轨迹进度用冷色的 seq-steel 阶梯上色,彗头点一抹 accent:

洛伦兹混沌吸引子,沿轨迹用 sequential 阶梯上色
洛伦兹混沌吸引子,沿轨迹用 sequential(steel)阶梯上色、accent 彗头领航。

第 3 层 · React 19 island

多个输入互相联动、状态驱动的实时重算、需要图表库或较深组件树——才升到 React。它在 MDX 里以 client:visible 注水,进入视口才加载 JS。配色用随 dark: 翻转的 Tailwind 工具类,或直接内联 var(--color-*)(需按序号动态取色、避免被 purge 时尤其好用);过渡用 motion-safe: 变体。

傅里叶级数说:任何周期函数都是正弦谐波之和。拖滑块加项,方波 4πkoddsinkxk\frac{4}{\pi}\sum_{k\,\text{odd}} \frac{\sin kx}{k} 的部分和逐步逼近——每条谐波一种分类色、部分和用 accent,边沿的 Gibbs 过冲不随项数消失:

5 项 · 最高 9

5 项谐波叠加,逼近方波;边沿的 Gibbs 过冲不随项数消失。

最后回到日常的「状态联动」。这个复利小工具拖动任一滑块,数值与增长条实时联动(刻意不引图表库,纯 state + 内联条形),盈亏走语义色:

1,000
5%
10 年

10 年后:1,629

收益 +62963%)

怎么选

先挑层,再挑色:

  • :能用 SVG 就别上 Canvas(零运行时、自动变色、可访问性最好);点多到 SVG 卡、或要逐帧动画 → Canvas(自己接主题与 DPR);真要状态联动 / 图表库 / 深组件树 → React(默认 client:visible)。
  • :单序列 / UI → accent;多序列 → 分类色(身份稳定);量级 / 序号 → sequential;盈亏 → positive / negative

三层的完整约定(目录、主题联动、client:* 默认、a11y、import 路径)写在仓库 AGENTS.md 的「Interactive component layers」一节,对照色板与全部 token 见 /design-system;这也是自动化生成 note 时遵循的规范。