設計系統
系統本身即是成品。其他一切皆由此構建。一個自行發明色彩和間距的交付物是一次性的;而一個基於 token 構建的交付物則可以擴展、重新品牌化,並保持一致。
輸出
三個檔案,而分割是重點:
design/tokens.css— token,僅此而已。純 CSS 自訂屬性。design/components.css— 元件,作為僅從語意 token 構建的類別。design/Design System.html— 展示頁。它<link>了兩個檔案並加以說明。僅供文件使用:它本身不宣告任何數值。
這兩個 CSS 檔案就是系統:它們是其他應用程式複製的對象。展示頁則留在專案中作為它們的文件。
展示頁中的區塊,依此順序:
- 意圖 — 三句話:這是為誰設計的、應該給人什麼感覺、明確不是什麼。
- 色彩 — 每個色票及其 token 名稱、解析後的值,以及通過對比的配對。
- 字體 — 字體家族、字重、每個階層有指定角色名稱的縮放比例,以及以正文大小顯示的段落範例。
- 空間與形狀 — 間距縮放比例、圓角半徑、陰影、邊框。
- 元件 — 每個元件以其真實狀態呈現,旁邊是應用程式複製的標記。
- 規則 — 五或六個容易出錯的決策。
- 在應用程式中使用 — 版本、要複製的兩個檔案、載入順序、字型
<link>、根類別,以及自上一版本以來的變更。
Tokens
兩個層級。原始變數持有原始值;語意 token 命名一個工作並指向一個原始變數。元件永遠只引用語意 token — 這就是讓重新品牌化只需修改一行程式碼的關鍵。
將以下內容寫入 design/tokens.css。這是專案中唯一允許出現原始值的檔案。acme 代表系統的前綴(請參閱分享):
/* Acme 設計系統 1.0.0 — tokens。按原樣複製此檔案;在其來源專案中修改。 */
:root {
/* 原始變數 */
--acme-ink-900: oklch(0.22 0.02 260);
--acme-ink-600: oklch(0.48 0.02 260);
--acme-paper-50: oklch(0.99 0.005 90);
--acme-paper-100: oklch(0.96 0.008 90);
--acme-line-200: oklch(0.90 0.006 90);
--acme-accent-500: oklch(0.55 0.14 35);
--acme-accent-600: oklch(0.45 0.15 35);
--acme-accent-100: oklch(0.94 0.03 35);
/* 語意 */
--acme-text: var(--acme-ink-900);
--acme-text-muted: var(--acme-ink-600);
--acme-bg: var(--acme-paper-50);
--acme-surface: var(--acme-paper-100);
--acme-border: var(--acme-line-200);
--acme-accent: var(--acme-accent-500);
--acme-accent-hover: var(--acme-accent-600);
--acme-accent-wash: var(--acme-accent-100);
/* 字體 */
--acme-font-display: "Fraunces", Georgia, serif;
--acme-font-text: "Public Sans", system-ui, sans-serif;
--acme-step--1: 0.875rem;
--acme-step-0: 1rem;
--acme-step-1: 1.25rem;
--acme-step-2: 1.5625rem;
--acme-step-3: 1.953rem;
--acme-step-4: 2.441rem;
--acme-step-5: 3.052rem;
/* 空間,以 4px 為基礎 */
--acme-s-1: 4px; --acme-s-2: 8px; --acme-s-3: 12px; --acme-s-4: 16px;
--acme-s-5: 24px; --acme-s-6: 32px; --acme-s-7: 48px; --acme-s-8: 64px; --acme-s-9: 96px;
/* 形狀和動畫 */
--acme-radius-sm: 4px; --acme-radius: 8px; --acme-radius-lg: 16px;
--acme-border-width: 1px; --acme-ring-width: 2px;
--acme-shadow-1: 0 1px 2px oklch(0.2 0.02 260 / 0.06);
--acme-shadow-2: 0 4px 16px oklch(0.2 0.02 260 / 0.08);
--acme-ease: cubic-bezier(0.2, 0.7, 0.3, 1);
--acme-dur: 160ms;
}
該區塊是一個起點,不是要盲目複製的模板。根據需求更改強調色調、字體配對和墨水色溫 — 但要保持結構。
Tokens 規則
- 對比度是測量出來的,而不是目測的。 主體文字與其實際背景的對比度需達到 4.5:1;大文字和 UI 邊框達到 3:1。每個色票頁面都應說明其配對。
- 使用 Oklch,而不是隨機的十六進制。 均勻的亮度階梯可以避免色板看起來髒亂。在同一色調系列中保持色度大致恆定。
- 無未使用的 token。 如果沒有東西引用某個 token,就刪除它。
- 此檔案之外無數值。 其他任何地方的十六進制、
oklch()字面量、px 大小或陰影都是一個尚未命名的 token。 - 兩個字體家族。 一個顯示用,一個文字用。後備字體堆疊必須是真實的,不能只是
sans-serif。 - 使用縮放比例,而非即興創作。 如果某個數值不在間距或字體縮放比例上,要麼有意識地添加它,要麼使用最接近的數值。
- 僅在被要求時才加入暗色模式 — 但保持色彩的語意性,以便日後添加時只需交換 token,無需重寫。
元件
將它們寫入 design/components.css,並在展示頁中呈現每個元件的預設、懸停、焦點、啟用和禁用狀態(如果適用):
- 按鈕 — 主要、次要、幽靈。通常一種大小就足夠。
- 文字輸入框和選取框,附帶標籤、說明文字和錯誤狀態。
- 卡片,帶有真實的內容範例,而非「Lorem ipsum」。
- 導航,包括桌面和窄寬度兩種樣式。
- 表格或列表行,帶有標題。
- 徽章和標籤。
- 模態框或對話框。
- 空狀態。
- 提示訊息或內聯通知。
每個元件僅使用語意 token。如果某個元件需要的數值沒有對應的 token,那就是系統的缺口 — 添加該 token,不要硬編碼數值。
/* Acme 設計系統 1.0.0 — 元件。需要來自相同版本的 tokens.css,且需先載入。 */
.acme {
font-family: var(--acme-font-text);
font-size: var(--acme-step-0);
color: var(--acme-text);
background: var(--acme-bg);
}
.acme-button {
font: inherit;
padding: var(--acme-s-2) var(--acme-s-4);
border: var(--acme-border-width) solid transparent;
border-radius: var(--acme-radius);
background: var(--acme-accent);
color: var(--acme-bg);
transition: background var(--acme-dur) var(--acme-ease);
}
.acme-button:hover { background: var(--acme-accent-hover); }
.acme-button:focus-visible { outline: var(--acme-ring-width) solid var(--acme-accent); outline-offset: var(--acme-ring-width); }
.acme-button--ghost { background: transparent; color: var(--acme-accent); border-color: var(--acme-border); }
分享
其他應用程式透過複製 tokens.css 和 components.css 來使用此系統。以下規則確保複製的檔案能在任何地方運作。
- 所有名稱加上前綴。 在第一個 token 之前為系統選擇一個簡短的名稱,並將其放在每個自訂屬性和每個類別上:
--acme-accent、.acme-button。這樣複製的檔案就可以與應用程式自己的 CSS 並存而不會發生衝突。前綴是 API 的一部分;永遠不要更改它。 - 兩個檔案都使用純 CSS。 沒有 Tailwind 指令、沒有預處理器語法、沒有巢狀、沒有
@layer要求、兩者之間沒有@import。一個 React 應用程式、一個靜態頁面或另一個設計工具必須能透過直接複製獲得完全相同的結果。 - 一切都在執行時解析。 沒有建置時的顏色運算,沒有編譯後的輸出。
components.css中沒有全域性的東西。 沒有重置、沒有元素選擇器、沒有作用在html或body上的樣式:將檔案放入應用程式不應重新設定其樣式。基本文字樣式位於根類別.acme上,應用程式可以將其新增到需要的地方。- 狀態來自標記。 使用
:hover、:focus-visible、:disabled、[aria-invalid="true"]、[aria-current]和[aria-expanded]來設定樣式,而不是應用程式必須手動切換的狀態類別,這樣元件就能與任何框架的 HTML 相容。 - 易於覆寫。 在可能的情況下,一個規則只使用一個類別,沒有 ID,沒有
!important。 - 兩個檔案使用同一個版本。 每個檔案的第一行都指定系統名稱及其版本;每次變更時都應同時更新兩個檔案的版本號。修改一個值為修補版本(patch),新增一個 token 或元件為次要版本(minor),移除一個則為主要版本(major)。重新命名時,新增新的 token 並保留舊的 token 作為別名,例如
--acme-old: var(--acme-new);,直到應用程式遷移完成;移除別名即為 major 版本變更。 - 專案相關的東西留在展示頁中。 字型
<link>標籤、容器寬度和資源路徑不是 token。展示頁展示了字型<link>,以便應用程式可以連同檔案一起複製。
如果目標專案已經有 token 檔案,請先符合其命名慣例,再自行發明。共享的詞彙比一個更好的名稱更有價值。
展示頁
展示頁是供人類閱讀的文件,而不是一個供人欣賞的元件庫。在色票旁邊顯示 token 名稱。顯示每個元件以及應用程式複製的標記。註明難以看到的狀態(焦點環、禁用)。
連結兩個檔案,先連結 tokens,並將根類別放在 <body> 上。使用系統自己的字體和間距來排版展示頁 — 這是系統運作的第一個證明。
從檔案中讀取值,而不是手動輸入,這樣文件就不會與實作脫節:
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="components.css">
...
<body class="acme">
<figure class="swatch" data-token="--acme-accent-500"></figure>
...
<script>
for (const el of document.querySelectorAll('[data-token]')) {
const name = el.dataset.token;
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
el.dataset.value = value; // 由 CSS 顯示
el.style.setProperty('--swatch', value); // 由 CSS 繪製
}
</script>
色票從 token 繪製自己,並顯示瀏覽器解析後的值。手動輸入的十六進制是未來的一個謊言。
完成
在展示頁上執行 design-verify。它會列出展示頁或 components.css 中的任何顏色字面量;應該沒有。檢查每個色票標籤是否清晰可讀、每個元件是否對齊到相同的基線網格,以及沒有範例文字在 1024px 寬度下溢出其容器。
當系統被導入另一個應用程式時,它會複製 tokens.css 和 components.css(不修改),以及來自展示頁的 <link> — 但不包括展示頁本身。僅在需要在沒有資料夾的環境中閱讀時,才匯出獨立的展示頁副本。