デザインシステム
システムそれ自体が成果物です。他のすべてはそこから構築されます。独自の色やスペーシングを発明した成果物は単発的なものに過ぎませんが、トークンに基づいて構築された成果物は拡張、リブランド、一貫性の維持が可能です。
出力
3つのファイルです。分割にこそ意味があります。
design/tokens.css— トークンのみ。プレーンなCSSカスタムプロパティ。design/components.css— コンポーネント。セマンティックトークンからのみ構築されたクラス。design/Design System.html— 見本ページ。両方のファイルを<link>で読み込み、文書化します。文書のみであり、独自の値を宣言することはありません。
この2つのCSSファイルがシステム本体であり、他のアプリがコピーするものです。見本ページはプロジェクト内に留まり、それらの文書として機能します。
見本ページのセクションは以下の順序で構成します。
- 意図 — 3文で:誰のためのものか、どのような印象を与えるべきか、明確に「そうではない」もの。
- 色 — すべてのスウォッチを、そのトークン名、解決される値、およびコントラストを満たす組み合わせとともに表示。
- タイポグラフィ — ファミリー、ウェイト、各ステップに名前付きの役割を持つスケール、および本文サイズの段落サンプル。
- スペースとシェイプ — スペーシングスケール、角丸、シャドウ、ボーダー。
- コンポーネント — それぞれを実際の状態でレンダリングし、アプリがコピーするマークアップを隣に表示。
- ルール — 誰かが間違えやすいであろう5~6の決定事項。
- アプリでの使用方法 — バージョン、コピーする2つのファイル、読み込み順序、フォントの
<link>、ルートクラス、および前回のバージョンからの変更点。
トークン
2層構造です。プリミティブは生の値を保持し、セマンティックトークンは役割に名前を付け、プリミティブを指し示します。コンポーネントは常にセマンティックトークンのみを参照します。これにより、リブランドが1行の変更で完了するようになります。
これを design/tokens.css に記述します。このファイルだけがプロジェクト内で生の値が現れてよいファイルです。acme はシステムの接頭辞を表します(「共有」のセクションを参照)。
/* Acme design system 1.0.0 — tokens. Copy this file as is; change it in its source project. */
:root {
/* primitives */
--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);
/* semantic */
--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);
/* type */
--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;
/* space, on a 4px base */
--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;
/* shape and motion */
--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;
}
このブロックは出発点であり、盲目的にコピーするテンプレートではありません。アクセントの色相、タイプの組み合わせ、インクの暖かみを要件に合わせて変更してください。ただし、構造は維持してください。
トークンのルール
- コントラストは目視ではなく計測する。 本文テキストは実際の背景に対して4.5:1、大きなテキストとUIボーダーは3:1。すべてのスウォッチページでその組み合わせを明記すること。
- ランダムなhexではなくOklchを使用する。 均等な明度ステップによりパレットが濁るのを防ぎます。色相ファミリー全体でクロマはおおむね一定に保つこと。
- 未使用のトークンは作らない。 何も参照していないトークンがあれば削除する。
- このファイルの外に値を記述しない。 hex、
oklch()リテラル、pxサイズ、シャドウを他の場所に書くことは、まだ名前が付けられていないトークンがあることを意味する。 - 2つのタイプファミリー。 ディスプレイ用とテキスト用。フォールバックスタックは
sans-serifのみではなく、実際のフォントを指定すること。 - アドリブではなくスケールを使う。 値がスペーシングまたはタイプスケールにない場合、意図的に追加するか、最も近い値を使用する。
- ダークモードは要求された場合のみ — ただし、後から追加する場合はトークンの交換で済むように、色はセマンティックに保つこと(書き直しは不要)。
コンポーネント
design/components.css に記述し、それぞれを見本ページでデフォルト、ホバー、フォーカス、アクティブ、無効の状態(該当する場合)でレンダリングします。
- ボタン — プライマリ、セカンダリ、ゴースト。通常は1サイズで十分です。
- テキスト入力とセレクトボックス。ラベル、ヘルプテキスト、エラー状態を伴うもの。
- カード。実際のコンテンツの例(「Lorem ipsum」は不可)を伴うもの。
- ナビゲーション。デスクトップ幅と狭い幅の両方で。
- テーブルまたはリスト行。ヘッダーを伴うもの。
- バッジとタグ。
- モーダルまたはダイアログ。
- 空の状態。
- トーストまたはインラインノーティス。
各コンポーネントはセマンティックトークンのみを使用します。コンポーネントがトークン化されていない値を必要とする場合、それはシステムのギャップです。値をハードコードせずにトークンを追加してください。
/* Acme design system 1.0.0 — components. Needs tokens.css from the same version, loaded first. */
.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 をコピーしてシステムを利用します。以下のルールは、コピーがどこでも機能するためのものです。
- すべてに接頭辞を付ける。 最初のトークンの前にシステムの短い名前を1つ選び、すべてのカスタムプロパティとすべてのクラスに付けます:
--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で機能します。 - オーバーライドが容易であること。 可能な限りルールごとに1クラス、IDや
!importantは使用禁止。 - 両方のファイルでバージョンを統一する。 各ファイルの最初の行でシステム名とバージョンを指定します。変更のたびに両方でバージョンを上げます。値の変更はパッチ、新しいトークンまたはコンポーネントはマイナー、削除はメジャーとします。名前変更は、新しいトークンを追加し、古いトークンをエイリアスとして残します(
--acme-old: var(--acme-new);)。アプリが移行し終えたら、エイリアスを削除することがメジャーバージョンアップとなります。 - プロジェクト固有のものは見本ページに残す。 フォントの
<link>タグ、コンテナ幅、アセットパスはトークンではありません。見本ページはフォントの<link>を表示し、アプリがファイルと一緒にコピーできるようにします。
ターゲットプロジェクトに既存のトークンファイルがある場合は、独自の命名を発明する前にその命名に合わせてください。共有された語彙は、より良い名前よりも価値があります。
見本ページ
見本は人間のための文書であり、鑑賞するためのコンポーネントギャラリーではありません。スウォッチの横にトークン名を表示します。各コンポーネントを、アプリがコピーするマークアップとともに表示します。見えにくい状態(フォーカスリング、無効状態)にも注記します。
両方のファイルを、トークンを先にリンクし、ルートクラスを<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; // printed by CSS
el.style.setProperty('--swatch', value); // painted by CSS
}
</script>
スウォッチはトークンから自身を描画し、ブラウザが解決した値を表示します。手入力されたhexは将来の嘘になります。
仕上げ
見本ページで design-verify を実行します。これにより、見本ページまたは components.css 内の色リテラルがリストアップされますが、それらはゼロであるべきです。すべてのスウォッチラベルが読みやすいこと、すべてのコンポーネントが同じベースライングリッドに揃っていること、1024px幅でサンプルテキストがコンテナからはみ出していないことを確認します。
システムを別のアプリに提供する場合は、tokens.css と components.css をそのままコピーし、見本ページからフォントの<link>も一緒にコピーします。見本ページ自体はコピーしません。フォルダなしでどこかで読む必要がある場合にのみ、見本ページのスタンドアロンコピーをエクスポートします。