デザインシステムビルダー は何をしますか?
これは一般的なベストプラクティスのチェックリストではありません。以下のルールは、それをスキップすると実際に出荷されたバグ(エラーやビルド失敗はなく、ブラウザ上でのみ誤った表示となるサイレントバグ)が発生したからこそ存在します。references/bug-catalog.md には、各ルールの背後にある具体的なインシデントが記載されています。重要なシステムを開始する前に、一度読んでおいてください。
一貫した指針: コメントやドキュメントにのみ存在するルールは、いずれ偶然に違反され、その違反はエラーを引き起こしません。 各フェーズの最後には、「これをどのように自動的に強制するか?」と問いかけます — 答えが「誰かが覚えている」であれば、そのフェーズは完了していません。
2つのエントリーポイント:
- 新規構築 → フェーズ1~7を順に実行。
- 既存システムの監査または進化 → フェーズ8から開始し、必要に応じてフェーズ1~7に戻ります。
フェーズ1 — 発見
構築する前に質問すること。ここで推測すると、誰も望まないシステムが生まれ、それが明らかになる頃には修正に多大なコストがかかっています。
- 参考資料は、形容詞ではなく具体的な例として。 「現代的でクリーン」は何も特定しません。3枚のスクリーンショットがあれば十分です。
- 色は意味を持つのか、それとも単なる美的要素か? パレットにカテゴリカルな値(要素、ステータス、ブランドライン)がある場合、それぞれが動作(動き、形状、重み)も兼ねるのか、それとも純粋に視覚的なものなのか、事前に決定します。外見のために選ばれた色に後から意味を追加するのは、最初にこれを決定するよりもはるかに困難です。
- コンテンツは何の上に配置されるのか? ページの背景とパネルの背景が「ほぼ同じ」だと仮定しないでください。コミットする前に、候補となる組み合わせを測定してください — フェーズ2.3を参照。
- スコープ: 新規構築、既存システムの改良、または以前/兄弟システムとの比較?完全な新規構築でない場合は、フェーズ8へのルート。
- 配布: 単一プロジェクト、または複数プロジェクトで再利用?この答えだけで、フェーズ6(パッケージ化)が適用されるかどうかが決まります。これを誤ると、単発のプロジェクトを過剰に設計するか、複数のリポジトリに拡散しようとしているものを過小設計することになります。
フェーズ2 — トークンの作成
優先順位順 — これらを間違えると、その上に構築されるすべてのものが間違った基盤を継承します。
2.1 — 色は、生のチャネルトリプレットとして一度だけ作成され、裸のhex値としては決して使用しません。
--fire-rgb: 170 0 0; /* 唯一作成される値 */
--fire: rgb(var(--fire-rgb)); /* 派生 */
--fire-soft: rgb(var(--fire-rgb) / 0.18); /* 派生 */
これはスタイル上の問題ではありません。Tailwind(およびほとんどのユーティリティシステム)の不透明度修飾子(bg-fire/40)は、不透明な var(--x) hex値にアルファチャンネルを合成できません — クラスはエラーも警告もなく、静かに何も解決しません。このパターンの元となった強化パスを開始したのは、まさにこのバグです。色ごとに作成された数値が1つであるということは、それを変更する場所が常に1つしかないことも意味します。
2.2 — テキストとしてレンダリングできるすべての色には -text バリアントがあり、グリフは基本色ではなく常にそれを使用します。
コントラストは、目視ではなく、WCAG相対輝度(assets/check-tokens.template.mjs の式)を計算して検証します。あるサーフェスに対して4.5:1をクリアするティントでも、別のサーフェスに対しては不合格になる可能性があります — これこそが2.3が重要な理由です。
2.3 — 複数のサーフェス階層(ページ/パネル/埋め込み)がある場合は、実際の高低差スケールとして定義し、コミットする前に階層間の分離を測定します。 「もう少し明るく」は決定ではなく、推測です。実際のシステムでは、出荷後に誰かが実際の合成コントラストを計算した(一方方向で1.04:1、もう一方で1.19:1)という理由だけで、高低差モデル全体(インクをページとして → ボイドをページとして)を逆転させました。その計算は、選択の上に構築する前に行うべきであり、関係者が「カードが平面的に見える」と言った後ではありません。
スケールができたら、すべてのテキストセーフトークンのコントラストを、それが表示される可能性のあるすべての階層に対してチェックする必要があります。設計された階層だけでなく。
2.4 — モーション: 1つのイージングカーブと、少数の固定された持続時間。
JSアニメーション(framer-motion、GSAP、手動の requestAnimationFrame)がこれらを消費する場合は、カーブと持続時間をCSSと並んでJS定数としてエクスポートします — CSS変数はJavaScriptからは見えず、手入力されたベジェや持続時間はすぐに気付かれないうちにずれます。CSSとJSのバージョンが互いに一致していることを確認してください(フェーズ4)。
2.5 — グロー/グラデーションのようなものに関する数値の上限は、実際に出荷される値に対してチェックされた実数でなければなりません。 グラデーションのストップも含み、単一のアルファカラーだけではありません。「微妙なグロー」に強制される上限がないと、ページ全体のアンビエントグローが、一度に1コンポーネントずつ、それぞれ個別には妥当に見える形で戻ってきます。
2.6 — すべてのトークンには、それが何をレンダリングするかだけでなく、いつそれを使用するかについての1行コメントを付けます。
フェーズ3 — プリミティブの構築
- コンポーネントはトークンのみを参照します。hex値、任意の値、
white/10スタイルのリテラルは使用しません — これらはまさにフェーズ4のガードがチェックするものなので、今それらを書くと、後でチェックに失敗するだけです。 - 不透明度修飾子(
bg-x/10)の使用は、2.1に従って実際にアルファ合成可能なトークンをターゲットとする必要があります — チェック可能なので、目視せずにチェックしてください。 - プロップがそれをオーバーライドすることを意図している場合、バリアントをコンポーネントのベースクラスにハードコードしないでください。
className="shadow-lg"がすべてのカードに組み込まれ、それをshadow-root-earthに置き換えるためのプロップがある場合、等しい詳細度で2つのボックスシャドウユーティリティが生成されます — 勝者は生成されたスタイルシートのソース順序によって静かに決定されます。バリアントをプロップをキーとするルックアップテーブル(Record<Variant, string>)にし、ベースクラスと期待されるオーバーライドの組み合わせにしないでください。 - スタイリングプロップは、実際のインタラクティブ要素の代わりにはなりません。
interactiveプロップを介してクリック可能に見える(cursor-pointer、ホバーリフト)が<div>であるカードは、マウスユーザーにはアフォーダンスを提供しますが、キーボードユーザーには決して提供しません — フォーカスリング、role、アクティベーションがありません。何かをクリック可能にする場合は、パッシブバージョンのスタイリングトグルではなく、<a>または<button>(CardLink/CardButtonのツイン)をレンダリングします。 - 本当に複雑なインタラクティブコンポーネント(モーダル、タブ、トースト)の場合は、プラットフォームに頼ります。 ネイティブの
<dialog>は、フォーカストラップとEscapeキーでのクローズを無料で提供します。手作りのバージョンは、それらのいずれかを確実に間違えます。タブのロービングtabindexは、手作りのタブ実装で最もよくある間違い(アクティブでないすべてのタブがTabストップのままになる)です — 明示的に検証し(tabindexが矢印キーナビゲーションで[0,-1,-1,-1] → [-1,0,-1,-1]と移動することを確認)、想定しないでください。 - システムにカスタムアイコン/マークアセットが必要な場合は、描画ルール(グリッド、ストロークウェイト、キャップスタイル)を事前に修正し、カラールールと同じ方法で強制します — これもフェーズ4の仕事です。
フェーズ4 — ガード
小さな、理想的には依存関係のないチェックスクリプトを作成します。これはプロセス全体で最もレバレッジの高い単一の成果物です — references/bug-catalog.md のすべてのバグは、誰かがここでエンコードするまで散文にのみ存在していたルールでした。
最低限のカバレッジ(assets/check-tokens.template.mjs を適応):
- コンポーネントまたはプリセットが参照するすべてのトークンが実際に存在すること。
- コンポーネントソースに手入力されたhex値やリテラルの不透明度カラーがないこと。
- すべてのテキストセーフトークンについて、それが表示される可能性のあるすべてのサーフェス階層に対するWCAGコントラスト(2.3)。
- 文書化された数値の上限が、実際に出荷される値に対して数値として強制されていること — 単純な色だけでなく、グラデーションの最も明るいストップも含む(2.5)。
- JSモーション定数がCSSトークン対応物と一致していること(2.4)。
- 不注意で違反しやすい構造ルール — 例:「ページ全体の背景グロー禁止」:グローユーティリティがページレベルの要素に適用されないことをチェックし、単にトークンのアルファが上限内であることをチェックするだけではない。
- 通常の散文に一致する可能性のあるルール(例:「2番目のイージングカーブ」に関するルール)のスコープを、実際のコードコンテキスト(クラス属性、CSS値)に限定し、任意のテキストには一致させないこと。独自のドキュメントにフラグを立てるリンターは無効にされるため、目的が損なわれます。
チェックは最終的に、システム自身のリポジトリだけでなく、それを消費するプロジェクトに対しても実行可能でなければなりません。 これがここで得られる最大の教訓です。実際のシステムでは、これまでに実際に発生したすべてのルール違反(24px未満の11のタイプサイズ、1つのサイトでの3つの異なる <h2> サイズ、5つの競合するレタースペーシング値、2番目のイージングカーブ、作成された持続時間)は、システムを消費するプロジェクトで発生し、内部チェックはそこを決して見ていませんでした。長い間、ルールを破ったことのないコードをガードしていました。チェッカーを任意のソースに対して実行可能に(npx your-check ./src)出荷し、真の例外(your-check-allow: <rule> — <reason> コメント規則)に対するスコープ付きのエスケープハッチを提供し、人々にチェック全体を無効にさせるのではなく、提供してください。
チェックを信頼する前に、各チェックをネガティブテストしてください。 スクラッチコピーで各ルールを意図的に破り、スクリプトが実際にそれに対して失敗することを確認します。失敗することが観察されたことのないチェックは未検証です — それが機能するのか、静かに無効なのかがわかりません。
フェーズ5 — ドキュメント作成
対象者別に分割された3つのファイル — 4つ目を追加したくなる衝動を抑えてください。4つ目のドキュメント(集約された「design.md」、2つ目のスタイルガイドとしてのドキュメント)は、同じ値がずれるための2つ目の場所になります。それぞれが異なる質問(なぜ/何をすべきか/何が決定されたか)を担当する3つで十分です。
philosophy.md— なぜ、アイデンティティ、選択の背後にある意味。一度読めば、めったに読み返さない。foundations.md— 運用ルール。新しいものを作成する前に誰かが確認するもの。フェーズ4のガードが強制するものとおおよそ1対1で対応する必要があります — ルールが一方にあり他方にない場合は、どちらが欠けているかを選択して修正します。decisions.md— 特定の判断とその理由の実行ログ。新しいエントリが一番上。置き換えられたエントリを決して削除しないでください。 それを置き換え済みとしてマークし、何が変わったか、なぜ変わったかを説明します。実際の決定(高低差モデル、アクセント階層)を証跡なしで元に戻すと、次の人(多くの場合未来の自分)がゼロから推論を導き出さざるを得なくなるか、元の決定が修正したものを静かに再び壊すことになります。- 実際のトークンとコンポーネントをレンダリングする生きたショーケース — それらの説明ではありません。パッケージを文字通り
importできない場合(静的単一ファイルHTMLドキュメントなど)、すべての値の2番目のインラインコピーを保持しており、そのコピーはずれます。実際のシステムのショーケースは、実際のパッケージをインポートするものとして再構築される前に、独自のトークンより2バージョンも静かに遅れていました。静的なショーケースがしばらく避けられない場合の最速の修正は、インラインコピーを実際のソースに対してガードするフェーズ4です — ただし、システムが実際のコンシューマーを持つほど安定したらすぐにそれを廃止する計画を立ててください。 - ショーケースがデプロイ可能な場合は、実際のURLにデプロイし、実際の狭いビューポートで表示します。 特定のセクションがコンテンツよりも狭い場合にのみ表示されるレイアウトバグ(例えば、意図的にオーバーフローする装飾要素がドキュメント全体を広げる)は、すべての静的チェックには見えません — トークン違反でも、コントラスト失敗でも、lintエラーでもありません。これは、実際のブラウザで実際の幅でのみ存在します。そのようなバグの1つが出荷され、3バージョンも存続しました。システムが表示される場所がローカルファイルだけだったからです。
- バージョニングポリシーと、複数のドキュメントがある場合の読み順を書き留めてください。1パラグラフのコストで、誰かがそれを導き出す手間を省けます。
フェーズ6 — パッケージ化と配布
フェーズ1で「複数プロジェクトで再利用」と言われた場合のみ。
- インストール可能なパッケージとして出荷する(
exportsマップを持つpackage.json)。「これらのファイルをプロジェクトにコピー」は決してしないでください — コピーには、下流の修正が上流にフィードバックされるパスがなく、システム自身のソースオブトゥルースが静かに2つのコピーのうちより壊れたものになります。これは、この系統全体で最初のバグの根本原因でした。 - Tailwindプリセットを配布する場合: プリセットの
content配列はTailwindによってマージされません。 コンシューマー自身のcontent配列がプリセットのものを完全に置き換えます。これはカタログで2番目にコストのかかるバグであり、「明白な」修正 — プリセットでグロブを宣言し、それらが結合されると想定する — は機能しません。ビルドはバイト単位で同一に戻ってきます。これが兆候です。プリセットはそのグロブをエクスポートする必要があり、すべてのオンボーディング(README、存在する場合はスキャフォールドコマンド)は、コンシューマーがそれらを明示的に展開することを示さなければなりません:content: [...preset.content, ...yourGlobs]。これをバンドラーの独自の設定解決APIで検証し、想定しないでください — 「修正」後のビルドアーティファクトが変更されていないことは、偶然ではなくバグのシグナルです。 - タグでバージョン管理し、ブランチだけでは管理しないでください。 ブランチピンは、ソースリポジトリのブランチが進むと、コンシューマーの下で静かに移動します — これはまさに、誰もそれを決定していないのに、コンシューマーがテストされていないバージョンを出荷することになる方法です。コンシューマーが新しいタグに移動する必要がある場合、ロックファイルは
package.jsonのタグが何と言っているかに関係なく、解決された正確なコミットをピンできることを覚えておいてください — 再解決を強制し、再ビルドを信頼する前にnode_modulesが実際に新しいバージョンを報告することを確認してください。 - パッケージが新しいコンシューマー向けにスキャフォールド/initコマンドを提供する場合、初日から正しい、展開されたコンテンツ設定を出力するようにしてください — 決定ログにエントリがあるほど深刻なフットガンについて、すべてのコンシューマーのREADMEを読む習慣に依存しないでください。
フェーズ7 — 検証
継続的に適用されます。最後に一度だけではありません。
- 新規インストールまたはクローンに対してテストし、ワーキングディレクトリに対してはテストしないでください。 ワーキングディレクトリは、実際のコンシューマーが初日に遭遇する、欠落したエクスポート、コミットされていない修正、古いキャッシュを隠します。
- 「修正」後のビルドアーティファクトが変更されていないことは、修正が機能しなかったシグナルであり、偶然ではありません。 実際の出力を差分して、何かが変わったと信じる前に確認してください。
- 型チェックは検証ではありません。 このカタログで最も深刻なバグ — フォントが完全に間違った書体でレンダリングされる — は、利用可能なすべての静的チェックを通過し、実際のコンシューマーでコンポーネントをレンダリングして見ることでのみ捕捉されました。完了と呼ぶ前に、すべてを少なくとも一度は実際の何かでレンダリングしてください。
- 検証の主張について、便利に見えるプラットフォームAPIを信用しないでください。
document.fonts.check()は、「このフォントは実際にこのグリフを含んでいるか」という質問に正確に答えるツールのように見えますが、嘘をつきます(avail...