這點(diǎn)小小 CSS 升級(jí),悄悄救了無(wú)數(shù)快要崩潰的設(shè)計(jì)系統(tǒng)
最近有個(gè)場(chǎng)景很典型。
一個(gè)組件庫(kù)在排查主題 BUG 的時(shí)候,突然發(fā)現(xiàn)了一件讓人當(dāng)場(chǎng)沉默的事:
這個(gè)庫(kù)里居然有 180 個(gè)顏色變量。
品牌主色改一下,要在 3 份文件里同步 15 種深淺、hover、透明度……
在做設(shè)計(jì)系統(tǒng)的人,大多都聽(tīng)說(shuō)過(guò)“設(shè)計(jì) token”:
- 顏色、間距、字號(hào),全都變量化
- 常見(jiàn)做法就是:一個(gè) token 對(duì)應(yīng)一個(gè)固定的十六進(jìn)制或 RGB 值
問(wèn)題是——只要產(chǎn)品一說(shuō)“全站換個(gè)主題色”, 這套“死值系統(tǒng)”立刻變成噩夢(mèng)現(xiàn)場(chǎng)。
下面的這個(gè) CSS 特性,正好卡在這個(gè)痛點(diǎn)上:
可以基于一個(gè)“基色”,動(dòng)態(tài)算出各種深淺、明暗、透明度變化。
以后只要改一個(gè)變量,整套 UI 跟著自動(dòng)聯(lián)動(dòng)。 顏色體系終于不再靠人肉維護(hù)。
傳統(tǒng)做法:靠人肉復(fù)制的“顏色農(nóng)場(chǎng)”
絕大多數(shù)設(shè)計(jì)系統(tǒng)的配色,大概長(zhǎng)這樣:
- 先定一個(gè)基礎(chǔ)色
- 然后給按鈕、邊框、Hover、Active、背景再各配一堆“手工調(diào)色”版本
:root {
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-primary-active: #1d4ed8;
--color-primary-light: #93c5fd;
--color-primary-dark: #1e40af;
--color-secondary: #8b5cf6;
--color-secondary-hover: #7c3aed;
--color-secondary-active: #6d28d9;
/* ... 后面還會(huì)繼續(xù)長(zhǎng)下去 */
}一個(gè)主色系,往往就要十幾二十個(gè)變量。 調(diào)色板一擴(kuò),全局顏色 token 數(shù)量輕松破百。
設(shè)計(jì)同學(xué)輕飄飄地一句:
“主色想從偏藍(lán)一點(diǎn),調(diào)成更紫一點(diǎn)。”
工程側(cè)就得:
- 改 15 個(gè)變量
- 對(duì)著設(shè)計(jì)稿比 hover、active 是否協(xié)調(diào)
- 還要小心半透明背景有沒(méi)有漏改
漏一個(gè),hover 怪異; 漏兩個(gè),整套主題就開(kāi)始“臟”。
這種模式:
- 十六進(jìn)制來(lái)回復(fù)制
- 手動(dòng)算 RGBA
- 一遍遍打開(kāi)取色器微調(diào)
最終只有一個(gè)評(píng)價(jià):費(fèi)神又不可靠。
CSS 相對(duì)顏色:把所有“深淺變化”交給瀏覽器算
所謂“相對(duì)顏色(relative colors)”,指的就是:
新顏色不是寫(xiě)死,而是“從一個(gè)基準(zhǔn)色算出來(lái)”。
先聲明一個(gè)基色,再用 CSS 按規(guī)則派生出各種變化版。 改一次基色,全家跟著變。
語(yǔ)法核心是一個(gè) from 關(guān)鍵字,用起來(lái)像這樣:
color-function(from origin-color channel1 channel2 channel3 / alpha)拆開(kāi)看就很清晰:
- color-function:輸出格式,比如
rgb()、hsl()、oklch() - from:關(guān)鍵字,表示“下面這顏色是來(lái)源”
- origin-color:基準(zhǔn)顏色,hex / rgb / hsl 都行
- channel1 ~ 3:可訪問(wèn)和修改的通道值
- alpha:可選透明度,寫(xiě)在斜杠后面
最常用的場(chǎng)景,大概是這樣:
:root {
--primary: #3b82f6;
}
.button {
background: var(--primary);
}
.button:hover {
background: hsl(from var(--primary) h s calc(l - 10));
}只多寫(xiě)了一行 hsl(from ...), 卻把 hover 效果徹底從“寫(xiě)死”變成“相對(duì)基色、自動(dòng)聯(lián)動(dòng)”。
以后品牌色只要改一個(gè) --primary, 所有 hover、active、淺色版,統(tǒng)統(tǒng)自己跟上。
from:讓顏色“拆開(kāi)來(lái)用”的魔法詞
from 做的事情只有一件:
把一個(gè)顏色,轉(zhuǎn)成當(dāng)前色彩空間下的各個(gè)通道, 然后把這些通道值“暴露”出來(lái)給后面用。
比如:
rgb(from green r g b) /* 綠會(huì)被轉(zhuǎn)成 r=0 g=128 b=0 */得到通道值之后,就可以為所欲為:
rgb(from green g g g) /* rgb(128 128 128) - 拿綠色通道當(dāng)灰度用 */
rgb(from green b r g) /* rgb(0 0 128) - 通道順序隨便換 */剛看到會(huì)覺(jué)得有點(diǎn)怪, 但一旦接受這種“通道是樂(lè)高”的設(shè)定,就會(huì)發(fā)現(xiàn)好玩得離譜。
自帶色彩空間轉(zhuǎn)換:源格式隨意,結(jié)果統(tǒng)一
from 后面跟的顏色格式隨意:RGB、HSL、hex 都行。 瀏覽器會(huì)先自動(dòng)轉(zhuǎn)換到指定的色彩空間,再拆成通道。
hsl(from rgb(255 0 0) h s l) /* 把紅色從 RGB 轉(zhuǎn)成 HSL */
oklch(from #3b82f6 l c h) /* 把 hex 藍(lán)轉(zhuǎn)成 OKLCH */好處是:
- 設(shè)計(jì) token 可以是 hex
- 業(yè)務(wù)側(cè)使用可以統(tǒng)一為 HSL 或 OKLCH
- 中間的轉(zhuǎn)換全部交給瀏覽器,無(wú)需心算
對(duì)設(shè)計(jì)系統(tǒng)而言,這一點(diǎn)尤其舒服:源頭怎么存不重要,使用端永遠(yuǎn)用的是同一套可計(jì)算空間。
calc():對(duì)色彩通道做“加減乘除”的那一層
真正能落地的地方,是 calc():
- 通道值可以被
calc()接管 - 通道本身可以當(dāng)變量來(lái)算
比如:
/* 變亮:提高 lightness */
hsl(from blue h s calc(l + 20))
/* 變暗:降低 lightness */
hsl(from blue h s calc(l - 20))
/* 半透明:動(dòng) alpha 通道 */
rgb(from blue r g b / calc(alpha * 0.5))
/* 調(diào)色:旋轉(zhuǎn) hue */
hsl(from blue calc(h + 180) s l)大部分常見(jiàn)的“顏色衍生邏輯”, 其實(shí)都是:
- 通道 + 某個(gè)量
- 通道 * 某個(gè)系數(shù)
把這些操作寫(xiě)進(jìn) CSS, 顏色體系就從“靠感覺(jué)”變成“有公式”。
OKLCH:比 HSL 更接近人眼的“真實(shí)亮度”
HSL 這套模式,很多前端都用得很順手:
- H — 色相
- S — 飽和度
- L — 亮度
問(wèn)題是:它的“亮度”不是真的人眼感知亮度。
比如下面兩種顏色,HSL 里都是 50% 光度:
hsl(220 80% 50%) /* 藍(lán) */
hsl(120 80% 50%) /* 綠 */理論上亮度一樣, 但人眼看過(guò)去,綠色明顯更亮。
OKLCH 就是為解決這類問(wèn)題而生的“感知均勻色彩空間”:
- 同樣的 L 值,對(duì)不同色相來(lái)說(shuō),肉眼看上去亮度更一致
- 做“程序化調(diào)色”時(shí),效果更可控
比如:
oklch(55% 0.15 260) /* 藍(lán) */
oklch(55% 0.15 140) /* 綠 */兩個(gè)顏色在人眼中的亮度更接近。
OKLCH 的三要素
結(jié)構(gòu)看起來(lái)有點(diǎn)像 HSL,但語(yǔ)義不同:
- L(Lightness):0–1 或 0%–100%,0 是黑,1 是白
- C(Chroma):0–約 0.37,指顏色的“純度/強(qiáng)度”
- H(Hue):0–360,色相角度
舉例:
oklch(0.6 0.2 265) /* 中等亮度、中等純度、偏藍(lán) */可以把 chroma 理解成“離灰色有多遠(yuǎn)”:
0是純灰- 數(shù)值越高越艷麗
為什么 OKLCH 對(duì)設(shè)計(jì) token 尤其重要?
原因就一句話:
在 OKLCH 里,通道變化更可預(yù)期。
比如 L 增加 0.1:
- 不管是藍(lán)、綠、黃,整體“看起來(lái)變亮”的感覺(jué)接近
- 不會(huì)出現(xiàn)某些色系“過(guò)曝”,某些色系“沒(méi)變多少”的情況
對(duì)于一套用算法生成的配色體系來(lái)說(shuō),這點(diǎn)太關(guān)鍵了:自動(dòng)算出來(lái)的深淺層級(jí),不再依賴運(yùn)氣。
用相對(duì)顏色 + OKLCH,搭一套真正“聰明”的設(shè)計(jì) token 系統(tǒng)
下面是一套可落地的 token 模式。
第一步:只定義“品牌基色”
:root {
/* 基礎(chǔ)品牌色,用 OKLCH 存 */
--brand-primary: oklch(0.55 0.2 265);
--brand-success: oklch(0.65 0.18 145);
--brand-error: oklch(0.6 0.25 25);
--brand-warning: oklch(0.75 0.15 85);
}四個(gè)顏色,就夠當(dāng)整套系統(tǒng)的“根”。 別的全部相對(duì)它們推導(dǎo)。
第二步:按規(guī)則生成完整色板
:root {
/* Primary 體系 */
--primary: var(--brand-primary);
--primary-hover: oklch(from var(--brand-primary) calc(l - 0.1) c h);
--primary-active: oklch(from var(--brand-primary) calc(l - 0.15) c h);
--primary-light: oklch(from var(--brand-primary) calc(l + 0.2) calc(c * 0.5) h);
--primary-lighter:oklch(from var(--brand-primary) calc(l + 0.3) calc(c * 0.3) h);
--primary-alpha-10:oklch(from var(--brand-primary) l c h / 0.1);
--primary-alpha-20:oklch(from var(--brand-primary) l c h / 0.2);
/* Success 體系 */
--success: var(--brand-success);
--success-hover: oklch(from var(--brand-success) calc(l - 0.1) c h);
--success-light: oklch(from var(--brand-success) calc(l + 0.2) calc(c * 0.5) h);
--success-alpha-10: oklch(from var(--brand-success) l c h / 0.1);
/* Error 體系 */
--error: var(--brand-error);
--error-hover: oklch(from var(--brand-error) calc(l - 0.1) c h);
--error-light: oklch(from var(--brand-error) calc(l + 0.2) calc(c * 0.5) h);
--error-alpha-10: oklch(from var(--brand-error) l c h / 0.1);
}四個(gè)基色,擴(kuò)展出一整板:
- 默認(rèn)色
- Hover / Active
- 淺色版 / 更淺的提示色
- 不同透明度的覆蓋層
以后品牌主色要改?只動(dòng)四個(gè)基變量。
暗色模式:不再需要復(fù)制一套“深色 token”
暗色模式的寫(xiě)法,這一段很出圈:
:root {
--surface: oklch(0.98 0.02 240);
--text: oklch(0.25 0.03 240);
}
[data-theme="dark"] {
/* 通過(guò)“反轉(zhuǎn)亮度”做暗色 */
--surface: oklch(from oklch(0.98 0.02 240) calc(1 - l) c h);
--text: oklch(from oklch(0.25 0.03 240) calc(1 - l) c h);
}淺色主題里:
- 背景亮、文字暗
- 到暗色主題里,兩者的 L 值反轉(zhuǎn)
結(jié)果是:
- 文字和背景的對(duì)比關(guān)系整體保持一致
- 只通過(guò)公式完成“明 / 暗模式”的整體遷移
- 不再需要寫(xiě)兩套完全獨(dú)立的 token
實(shí)戰(zhàn)里的幾個(gè)高級(jí)模式
這些模式在真實(shí)項(xiàng)目里解決過(guò)不少麻煩。
1)半透明遮罩:用同一個(gè)基色延伸
模態(tài)框背景的遮罩層:
.modal-backdrop {
background: oklch(from black l c h / 0.7);
}也可以把 black 換成主題色, 快速生成帶品牌色調(diào)的半透明遮罩。
2)動(dòng)態(tài)陰影:卡片背景變了,陰影自動(dòng)跟隨
.card {
--card-bg: var(--primary);
background: var(--card-bg);
box-shadow:
0 4px 6px oklch(from var(--card-bg) l c h / 0.2),
0 10px 15px oklch(from var(--card-bg) l c h / 0.15);
}陰影顏色不再寫(xiě)死,而是:
- 自動(dòng)從卡片背景里“偷”一份顏色
- 換膚 / 換主題時(shí),陰影會(huì)自然跟著變
3)可讀性友好的對(duì)比色:自動(dòng)拉開(kāi) 60% 光度差
.tag {
--tag-bg: var(--primary);
background: var(--tag-bg);
/* 文本比背景亮 0.6,保證對(duì)比度 */
color: oklch(from var(--tag-bg) calc(l + 0.6) c h);
}
.tag--dark {
--tag-bg: oklch(0.3 0.15 200);
/* 若背景偏亮,就反向壓暗文本 */
color: oklch(from var(--tag-bg) calc(l - 0.6) c h);
}L 通道的差值大約在 0.6 左右, 通常能得到相對(duì)可靠的可讀對(duì)比度。
瀏覽器支持與回退策略
截至 2025 年 10 月,相對(duì)顏色的支持情況大致是:
- Chrome 119+
- Firefox 128+
- Safari 16.4+
- Edge 119+
覆蓋率大約在 83% 左右。
老舊環(huán)境可以:
- 先寫(xiě)一層“靜態(tài)顏色”的 fallback
- 再用相對(duì)顏色覆蓋一層
保證老環(huán)境能“看到”,新環(huán)境能“用好”。
初次上手最容易踩的坑
幾個(gè)容易犯錯(cuò)的點(diǎn),提前列出來(lái):
- 鏈?zhǔn)脚缮鄬?/span>
- 一層顏色從另一層推導(dǎo),再?gòu)耐茖?dǎo)色繼續(xù)推導(dǎo)……
- 層級(jí)越深,后期越難排查
- 建議“從基色直接推導(dǎo)”,最多兩層
- 把 chroma 拉超過(guò)設(shè)備能表示的范圍
- OKLCH 在 sRGB 上 chroma 一般到 ~0.37
- 再往上加,顏色可能會(huì)“怪異溢出”
- 寫(xiě)錯(cuò) alpha 位置
- 正確寫(xiě)法是:
oklch(0.6 0.2 265 / 0.5) - 而不是:
oklch(0.6 0.2 265 0.5)
提前知道這些坑,能少掉很多“為什么和設(shè)計(jì)稿不一樣”的困惑。
最后這點(diǎn)小升級(jí),解決的是大系統(tǒng)的病
從設(shè)計(jì)系統(tǒng)的角度看,這個(gè)特性實(shí)際上解決了幾件“大事”:
- 主題切換不再是大手術(shù)
- 顏色 token 可以真正“集中管理”
- 深淺、Hover、Active、透明度,統(tǒng)統(tǒng)變成可復(fù)用的“規(guī)則”而不是手工調(diào)的“結(jié)果”
下一次配色系統(tǒng)要重構(gòu)的時(shí)候,不妨試著把:
- 基準(zhǔn)品牌色
- 暗色模式
- 狀態(tài)色
- 半透明覆蓋層
全部交給相對(duì)顏色來(lái)算一遍。 那種“改一個(gè)變量,全站自洽”的感覺(jué),非常上頭。
來(lái)自發(fā)起人的一小段話
這份內(nèi)容背后,是一個(gè)長(zhǎng)期在前端與設(shè)計(jì)系統(tǒng)領(lǐng)域打磨的小團(tuán)隊(duì)。 Sunil 在結(jié)尾留過(guò)一句話,大意是:
這些內(nèi)容背后,沒(méi)有外部資金, 只是想為每月 350 萬(wàn)讀者的技術(shù)社區(qū)多加一點(diǎn)實(shí)用的東西。
如果這些“微小的 CSS 小技巧”有幫上忙, 最好的反饋方式,就是把知識(shí)在自己的項(xiàng)目里用起來(lái),再分享給下一位還在為設(shè)計(jì)系統(tǒng)抓狂的同事。


























