在 React 中做主题颜色选择器,可以把每个设计令牌连接到一个 ColorInput。实时修改写入 CSS 自定义属性,交互结束时再保存最终值。
"use client";
import { ColorInput } from "chroma-panel";
import "chroma-panel/style.css";
const TOKENS = [
{ id: "brand", property: "--brand", label: "Brand", initial: "#3366cc" },
{
id: "surface",
property: "--surface",
label: "Surface",
initial: "#ffffff",
},
{ id: "text", property: "--text", label: "Text", initial: "#1c1c1e" },
];
function saveToken(property: string, hex: string) {
const saved = JSON.parse(
window.localStorage.getItem("theme") ?? "{}"
) as Record<string, string>;
window.localStorage.setItem(
"theme",
JSON.stringify({ ...saved, [property]: hex })
);
}
export function ThemeEditor() {
return (
<div>
{TOKENS.map((token) => (
<p key={token.id}>
<label htmlFor={token.id}>{token.label}</label>
<ColorInput
id={token.id}
defaultValue={token.initial}
onChange={(color) => {
document.documentElement.style.setProperty(
token.property,
color.hex
);
}}
onChangeComplete={(color) => saveToken(token.property, color.hex)}
/>
</p>
))}
</div>
);
}样式表中所有读取 var(--brand) 的地方,都会随着选择器的拖动实时变化。
修改时绘制,修改完成时保存
这里两个修改事件各有分工。
onChange 在拖动过程中持续触发,大约每帧一次。这个频率正适合 setProperty,它只是一次开销很小的 DOM 写入,预览也正是因此才有实时的感觉。
onChangeComplete 在拖动结束时只触发一次。开销大的工作放在这里:写入 localStorage、向你的 API 发送 PATCH 请求、往撤销栈里加一条记录。如果改在 onChange 里保存,每帧都会写一次,而这些结果还没有人需要。
不要在 onChange 中保存
在色轮上拖动一次,会产生数百次 onChange
调用。每一次写到存储或发到网络的操作,在下一帧就作废了。
写入哪个字段
两个回调收到的值都以多种形式表示同一个颜色:hex、hexa、rgba、hsva 和 css。如果令牌是不透明的品牌色,写入 hex。如果令牌可能是半透明的,写入 hexa,因为 hex 会丢掉透明度 (alpha) 通道。css 给出的字符串采用你在选择器上设置的 format,所以 format="hsl" 加上 color.css,写入属性的就是 hsl(...)。
恢复已保存的主题
选择器在挂载时读取 defaultValue。所以要先加载已保存的值,把它们应用到文档上,然后再挂载编辑器。
"use client";
import { useEffect, useState } from "react";
export function ThemeEditorLoader() {
const [saved, setSaved] = useState<Record<string, string> | null>(null);
useEffect(() => {
const raw = window.localStorage.getItem("theme");
const theme =
raw === null ? {} : (JSON.parse(raw) as Record<string, string>);
for (const [property, hex] of Object.entries(theme)) {
document.documentElement.style.setProperty(property, hex);
}
setSaved(theme);
}, []);
if (saved === null) return null;
return <ThemeEditor />;
}在 effect 中读取存储,可以让服务端渲染和客户端首次渲染的结果保持一致。如果希望每个令牌都由 React state 管理,就用 value 代替 defaultValue。请参阅受控与非受控。
给选择器也设置主题
上面这些令牌是你自己的。选择器也有自己的一套令牌,都以 --cp- 为前缀,用它们可以让编辑器和它所编辑的界面风格一致。完整列表见主题。触发器的其他 prop,包括用于把主题作为表单提交的 name,见 ColorInput 参考文档。