三层交互组件,一页 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)
几何、坐标、元素有限的图——首选这层。矢量无损缩放,颜色用 currentColor 与 var(--color-*),切明暗零 JS 自动变色;动画用纯 CSS,在系统「减少动态效果」时自动静止。
先从最朴素的匀速旋转开始——三条同心轨道,accent 描点:
把旋转搬到复平面,就是欧拉公式 :复指数即单位圆上的匀速转动。两段「本轮」嵌套(外臂转、内臂绕外臂端点转),末端描出的正是傅里叶级数「圆叠圆」的最小单元——accent 配一抹分类色:
频率一旦不只一个,就有了花样。Lissajous 图形是两个垂直简谐振动 、 的合成,频率比 决定形状。路径在构建期算好,运行时只用 CSS stroke-dashoffset 让高光沿线巡游——三条曲线三种分类色:
静态几何同样能很美。黄金比 φ 把边长 1,1,2,3,5,8,13,21 的正方形铺成一个 φ 比例矩形,叠加对数黄金螺旋——方块沿 sequential 阶梯由浅到浓,正好展示单色梯:
第 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 波守住钢蓝(只随主题调明度),正是「主角换相、分类色稳定」:
点一多,就能让 sequential 阶梯大显身手。叶序排布把第 颗种子放在极角 (黄金角)、半径 处——自然界最省空间的堆叠。几百颗点按序号在 oklch 数值上插值上色,得到平滑的单色渐变:
同样是逐帧,洛伦兹吸引子展示确定性混沌之美:三条耦合微分方程积出永不自交、对初值极度敏感的「蝴蝶」。沿轨迹进度用冷色的 seq-steel 阶梯上色,彗头点一抹 accent:
第 3 层 · React 19 island
多个输入互相联动、状态驱动的实时重算、需要图表库或较深组件树——才升到 React。它在 MDX 里以 client:visible 注水,进入视口才加载 JS。配色用随 dark: 翻转的 Tailwind 工具类,或直接内联 var(--color-*)(需按序号动态取色、避免被 purge 时尤其好用);过渡用 motion-safe: 变体。
傅里叶级数说:任何周期函数都是正弦谐波之和。拖滑块加项,方波 的部分和逐步逼近——每条谐波一种分类色、部分和用 accent,边沿的 Gibbs 过冲不随项数消失:
5 项谐波叠加,逼近方波;边沿的 Gibbs 过冲不随项数消失。
最后回到日常的「状态联动」。这个复利小工具拖动任一滑块,数值与增长条实时联动(刻意不引图表库,纯 state + 内联条形),盈亏走语义色:
10 年后:1,629
收益 +629(63%)
怎么选
先挑层,再挑色:
- 层:能用 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 时遵循的规范。