zudo-css-wisdom
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

マルチネームスペーストークン戦略

問題

小さなウェブサイトから始まったとします — 記事ページを持つブログです。デザインチームは単一のネームスペースの下にトークンを作成します。

@theme {
  /* myweb- namespace: article-focused design */
  --spacing-myweb-hsp-sm: 20px;
  --spacing-myweb-hsp-md: 40px;
  --spacing-myweb-vsp-sm: 24px;
  --spacing-myweb-vsp-md: 48px;

  --font-size-myweb-body: 1.125rem;
  --font-size-myweb-h1: 2.5rem;
  --font-size-myweb-h2: 1.75rem;
  --font-size-myweb-caption: 0.875rem;
}

これはうまく機能します。スペーシングは可読性のために余裕を持たせてあり、フォントサイズは長文コンテンツ向けに最適化されています。すべてのコンポーネントが同じデザイン言語を共有しています。

やがてサイトは成長します。ログインページが追加され、続いてユーザー設定パネル、そしてデータテーブルや高密度なツールバー、コンパクトなナビゲーションを持つ管理ダッシュボードが追加されます。管理画面には、より狭いスペーシング、より小さいフォントサイズ、より高密度なレイアウトが必要です — 記事用トークンが提供するものとは正反対です。

ここでありがちな判断は、同じネームスペースにトークンを追加していくことです。

@theme {
  /* Original article tokens */
  --spacing-myweb-hsp-sm: 20px;
  --spacing-myweb-hsp-md: 40px;
  --spacing-myweb-vsp-sm: 24px;
  --spacing-myweb-vsp-md: 48px;

  /* ...now add admin-density tokens into the same namespace */
  --spacing-myweb-hsp-dense-sm: 8px;
  --spacing-myweb-hsp-dense-md: 16px;
  --spacing-myweb-vsp-dense-sm: 6px;
  --spacing-myweb-vsp-dense-md: 12px;

  --font-size-myweb-body: 1.125rem;
  --font-size-myweb-body-dense: 0.8125rem;
  --font-size-myweb-h1: 2.5rem;
  --font-size-myweb-h1-dense: 1.25rem;
  --font-size-myweb-h2: 1.75rem;
  --font-size-myweb-h2-dense: 1rem;
  --font-size-myweb-caption: 0.875rem;
  --font-size-myweb-caption-dense: 0.75rem;
}

トークンセットは倍に膨れ上がります。すべての軸に「記事用」と「高密度用」の両方のバリアントが存在するようになり、開発者はスペーシングを決定するたびに hsp-smhsp-dense-sm のどちらを使うか選ばなければなりません。選択肢を絞り込むために設計されたはずのタイトなトークン戦略が、今では以前の倍の選択肢を提供してしまっています。

サイトにさらにコンテキスト(マーケティングのランディングページ、メールテンプレート、埋め込みウィジェットなど)が加わるにつれて、この問題は悪化していきます。単一のネームスペースがあらゆるデザインコンテキストに対応しようとした結果、トークンセットはバリエーションが膨れ上がったリストになってしまいます。

単一ネームスペース: コンテキストが増えるほど膨れ上がるトークン

解決方法

トークンを別々のネームスペースに分割します。デザインコンテキストごとに1つです。各ネームスペースは、ちょうど1種類の UI にのみ対応する、絞り込まれた最小限のトークンセットを持ちます。

/* ── Article/content context ── */
@theme {
  --spacing-myweb-hsp-sm: 20px;
  --spacing-myweb-hsp-md: 40px;
  --spacing-myweb-vsp-sm: 24px;
  --spacing-myweb-vsp-md: 48px;

  --font-size-myweb-body: 1.125rem;
  --font-size-myweb-h1: 2.5rem;
  --font-size-myweb-h2: 1.75rem;
  --font-size-myweb-caption: 0.875rem;
}

/* ── Admin/dashboard context ── */
@theme {
  --spacing-myadmin-hsp-sm: 8px;
  --spacing-myadmin-hsp-md: 16px;
  --spacing-myadmin-vsp-sm: 6px;
  --spacing-myadmin-vsp-md: 12px;

  --font-size-myadmin-body: 0.8125rem;
  --font-size-myadmin-h1: 1.25rem;
  --font-size-myadmin-h2: 1rem;
  --font-size-myadmin-caption: 0.75rem;
}

トークンの総数は同じですが、構造が異なります。

  • 各ネームスペースは、ちょうど4つのスペーシングトークンと4つのフォントサイズトークンを持ちます — 単一コンテキストのプロジェクトと同じくらいタイトな制約です

  • 記事ページを担当する開発者は myweb- トークンだけを扱います

  • 管理ダッシュボードを担当する開発者は myadmin- トークンだけを扱います

  • どのトークンを使うべきかという曖昧さはありません — ネームスペースがコンテキストを教えてくれます

ファイル構成

別々のネームスペースは、それぞれが独自の CSS ファイルに収まっているときに最も機能します。

styles/
├── tokens-myweb.css      /* Article/content tokens */
├── tokens-myadmin.css    /* Admin/dashboard tokens */
├── tokens-shared.css     /* Color, font-family, breakpoints — shared */
└── app.css               /* Imports all token files */

すべてのコンテキストに共通する共有トークン(色、フォントファミリー、ブレークポイント)は共有ファイルにまとめます。コンテキスト固有のトークン(スペーシング、フォントサイズ)だけをネームスペースに分割します。

/* app.css */
@import "tailwindcss/preflight";
@import "tailwindcss/utilities";
@import "./tokens-shared.css";
@import "./tokens-myweb.css";
@import "./tokens-myadmin.css";
分離されたネームスペース: 各コンテキストが独自の絞り込まれたトークンセットを持つ

デモ

ビジュアル対比: 記事 UI と管理画面 UI

同じページの構造を、2つの異なるトークンネームスペースでレンダリングした例です。記事用トークンは余裕のある読みやすいスペーシングを生み出します。管理画面用トークンは高密度で効率的なレイアウトを生み出します。

記事コンテキスト (myweb-) vs 管理画面コンテキスト (myadmin-)

分割の判断基準: どこで線を引くか

すべてのページのバリエーションに専用のネームスペースが必要なわけではありません。分割が意味を持つのは、スペーシングのリズム、フォントサイズのスケール、密度といったデザインルールが、コンテキスト間で根本的に異なる場合です。

同一ネームスペース vs 別ネームスペース: 分割すべきタイミング

コンポーネントでのネームスペース利用

各ネームスペースは独自の Tailwind ユーティリティクラスのセットを生成します。特定のコンテキストで作業する開発者は、そのコンテキストのネームスペースプレフィックスだけを使います。

Tailwind ユーティリティクラスにおけるネームスペースプレフィックス

共有トークンとコンテキスト固有トークン

すべてのトークンカテゴリがネームスペースの分割を必要とするわけではありません。ルールはこうです — 密度とスケール(スペーシング、フォントサイズ)を定義するトークンは分割し、アイデンティティ(色、フォントファミリー、ボーダー半径)を定義するトークンは共有のままにします。

トークンカテゴリ分割 or 共有?理由
スペーシング (hsp, vsp)コンテキストごとに分割コンテキストが異なれば必要な密度も異なる
フォントサイズコンテキストごとに分割記事のテキストとダッシュボードのテキストではスケールが異なる
共有ブランドアイデンティティはコンテキストをまたいで一貫させる
フォントファミリー共有タイポグラフィの選択はブランドの意思決定事項
ボーダー半径共有ビジュアルスタイルはコンテキストをまたいで一貫している
ブレークポイント共有レスポンシブの挙動は同じビューポートのルールに従う
light-dark() によるスキーム切り替えネームスペースごとの判断画面専用の chrome はデフォルトで使う。印刷優先のネームスペースは使わない
/* tokens-shared.css — applies everywhere */
@theme {
  --color-brand: oklch(55.5% 0.163 48.998);
  --color-surface: hsl(0 0% 100%);
  --color-text: hsl(222 47% 11%);
  --color-muted: hsl(215 16% 47%);
  --color-border: hsl(214 32% 91%);

  --font-sans: system-ui, sans-serif;
  --font-mono: ui-monospace, monospace;

  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
}

印刷優先ネームスペースは light-dark() を使わない

パレット自体が共有されている場合でも、1つの色の決定はネームスペースごとに変わります — それは、トークンが light-dark() を経由するかどうかです。アプリの chrome 用ネームスペースは画面上でのみレンダリングされるため、パレットはデフォルトで light-dark() を使います — 1つのトークンが両方のスキームに対応します。一方、スライドや請求書、証明書のような印刷優先のネームスペースは、画面上と紙の上で同一にレンダリングされる必要があります。印刷パイプラインはライトの color-scheme を強制するため、light-dark() トークンは紙の上ではライトの値に解決される一方、画面上のプレビューではダークの値が表示されることがあります — プレビューと印刷されたページが食い違ってしまうのです。印刷優先のパレットは、light-dark() もスキーム切り替えも使わない、プレーンな値として定義します。

/* ── App-chrome namespace: screen-only, scheme-aware ── */
:root {
  color-scheme: light dark; /* required — without it light-dark() always picks the first value */
}

@theme {
  --color-mychrome-surface: light-dark(hsl(0 0% 100%), hsl(222 20% 12%));
  --color-mychrome-text: light-dark(hsl(222 47% 11%), hsl(210 20% 88%));
}

/* ── Print-first namespace: identical on screen and paper ── */
@theme {
  --color-myslides-surface: hsl(0 0% 100%);
  --color-myslides-text: hsl(222 47% 11%);
  --color-myslides-accent: hsl(30 80% 45%);
}

「階層の順序」の命名トラップ

ネームスペースはしばしば、二層構造のトークン構造を使います — 実際の値を保持する生のパレット層と、それを意味にマッピングするセマンティック層です(強制の仕組み側については Design Token Lint を参照してください)。この2つの層は、ネームスペースを逆の順序で綴ります。この逆転は見落としやすいポイントです。

生の層はプレフィックスが先に来ます。これは @theme の外側にあるプレーンなカスタムプロパティで構成されます — ユーティリティを生成しないため、衝突を防ぐためにネームスペースを先頭に置きます。

/* Raw tier — prefix-first: --myns-palette-* */
:root {
  --myns-palette-accent-4: hsl(30 80% 50%);
  --myns-palette-neutral-2: hsl(215 15% 88%);
}

フレームワーク向けのセマンティック層はカテゴリが先に来ます。Tailwind はカテゴリプレフィックスを基準にユーティリティ生成を行うためです — --color-*bg-*text-* のユーティリティを生成します。

/* Semantic tier — category-first: --color-myns-* */
@theme {
  --color-myns-accent: var(--myns-palette-accent-4);
  --color-myns-border: var(--myns-palette-neutral-2);
}

myns は生の層では先頭に来て、セマンティック層ではカテゴリの後に続きます。記憶を頼りに書かれたコードは、どちらの層にも一致しない第三の綴りを発明しがちです。

/* NG — a third spelling: connects to neither tier */
.myns-chrome__panel {
  border-color: var(--myns-color-accent); /* nothing ever defines this */
}

第三の綴りは、切り離されたネームスペースを意味します。実際にあったレビューでは、chrome の CSS が --myns-color-accent を参照している一方で、セマンティック層は --color-myns-accent を定義していました — パネルに関するトークンの調整はすべて静かに機能しなくなっていたのです。chrome が、誰も書き込んでいない変数を読んでいたためです。CSS は未定義のカスタムプロパティを許容するため、エラーは発生しません。スタイルがトークン層に反応しなくなるだけです。

逆順の綴りをドリフトチェックとして grep します。

# Drift check — the reversed spelling must return zero hits
grep -rn -- "--myns-color-" src/    # 0 hits expected
grep -rn -- "--color-myns-" src/    # semantic tier (legitimate)
grep -rn -- "--myns-palette-" src/  # raw tier (legitimate)

クイックリファレンス

シナリオアプローチ
単一のデザインコンテキスト(ブログ、マーケティングサイト)ネームスペース1つ — 分割は不要
明確に異なる2つの UI 密度(コンテンツ + 管理画面)コンテキストごとに1つずつ、合計2つのネームスペース
3つ以上のコンテキスト(コンテンツ + 管理画面 + 埋め込みウィジェット)コンテキストごとに1つのネームスペース、共有トークンは共通ファイルに
色、フォントファミリー、ボーダー半径共有ネームスペースに置く — これらは密度ではなくアイデンティティを定義するもの
スペーシングとフォントサイズコンテキストごとに分割 — これらは密度とスケールを定義するもの
チームの所有境界(フロントエンドチーム vs 管理画面チーム)明確な所有権のためにネームスペースをチーム境界に合わせる
印刷向けコンテキスト(スライド、請求書、証明書)プレーンな色の値を持つ専用ネームスペース — light-dark() は使わない
二層トークン(生のパレット + セマンティック)生の層はプレフィックスが先(--myns-palette-*)、セマンティック層はカテゴリが先(--color-myns-*
ネームスペース立ち上げ時の検証ビルド後の CSS を grep する — ユーティリティが生成され、トークンが消費されているか

AIがよくやるミス

  • 異なるネームスペースのトークンを1つのコンポーネントで混在させる — あるコンポーネントが水平方向のパディングに px-myweb-hsp-sm を、フォントサイズに text-myadmin-body を使っている場合、それは2つのデザインコンテキストから引っ張ってきていることになります。各コンポーネントはちょうど1つのネームスペースのトークンだけを使うべきです

  • ページごとにネームスペースを作る — ネームスペースはデザインコンテキスト(コンテンツ vs 管理画面)を表すものであり、個々のページを表すものではありません。ブログ記事とAboutページは同じデザインコンテキストを共有します

  • 色をネームスペースに分割する — 色はブランドアイデンティティを定義するものであり、共有のままにすべきです。密度に関わるトークン(スペーシング、フォントサイズ)だけがネームスペースの分割を必要とします

  • ネームスペースプレフィックスなしの汎用的なトークン名を使う — マルチネームスペースのプロジェクトでは hsp-sm は曖昧です。常に完全なプレフィックス(myweb-hsp-smmyadmin-hsp-sm)を使ってください

  • 事前にネームスペースを作る — まず1つのネームスペースから始め、実際に異なる密度要件を持つ本当に異なるデザインコンテキストが現れたときにだけ分割しましょう

  • 第三のネームスペースの綴りを発明する — 生の層はプレフィックスが先(--myns-palette-accent-4)、セマンティック層はカテゴリが先(--color-myns-accent)です。記憶頼りの --myns-color-accent はどちらの層にもつながらず、それへの参照はすべて静かに機能しなくなります

  • ソースだけを見てトークンの配線を検証する — theme が定義していないトークンを参照するユーティリティは、エラーなしに CSS を一切出力しません。ビルド後のスタイルシートを grep して、ユーティリティとトークンが実際に出力に現れているか確認してください

使い分け

適している場合

  • 明確に異なる UI コンテキストを持つサイト — コンテンツ中心のサイトに管理ダッシュボードが追加される場合や、マーケティングサイトにアプリケーション UI が追加される場合

  • 所有権が分かれた大規模チーム — 記事チームと管理画面チームが独立して作業する場合、別々のネームスペースはデザイン判断の相互汚染を防ぎます

  • タイトトークン戦略を使っているプロジェクト — 単一のネームスペースにバリアントが増えすぎたとき、ネームスペースの分割は自然な拡張です

不要な場合

  • 単一コンテキストのウェブサイト — ブログ、ドキュメントサイト、ポートフォリオには複数のネームスペースは不要です

  • 小規模なプロジェクト — トークンセットが小さく管理しやすい場合、複数ネームスペースの複雑さは利益なしにオーバーヘッドを増やすだけです

  • 開発初期のプロジェクト — まず1つのネームスペースから始め、実際に2つ目のデザインコンテキストが現れたときに分割しましょう

他のトークン戦略との対比

この戦略はタイトトークン戦略の上に成り立っています。それを置き換えるものではなく、マルチコンテキストのプロジェクト向けに拡張するものです。

戦略スコープいつ使うか
タイトトークン戦略単一ネームスペース、制約されたトークンすべてのプロジェクトのデフォルト
2階層サイズ戦略幅・高さ用のテーマトークン + 任意の値どのネームスペース内であっても要素のサイズ指定を行うとき
マルチネームスペーストークン戦略異なる UI コンテキスト向けの複数ネームスペースプロジェクトが根本的に異なるデザインコンテキストを提供するとき

ビルド時の強制

マルチネームスペースのトークンシステムには、単一ネームスペースのシステムと同じ強制の仕組みが必要です — むしろそれ以上に重要とも言えます。ネームスペースが増えるごとに、生の値が紛れ込む余地のある表面積が広がるためです。コンポーネントコード内の生のリテラルを検出し、セマンティックトークンが var() 経由でパレットトークンを参照していることを検証する、マルチパスのリンターパターンについては Design Token Lint を参照してください。

ビルド後の CSS を監査する。ソースだけでは不十分

2つの失敗モードは、ソースレビューでは見えず、出力されたスタイルシートにしか現れません。1つ目は、ユーティリティクラスが静かに何もしなくなるケースです。タイトな theme では、theme が --tracking-widest を定義していない場合、マークアップ中の tracking-widest は CSS を一切出力しません。クラスはマークアップ上では意図的に見えますが、Tailwind は何も生成せず、エラーも発生しません。2つ目は、セマンティックトークンが定義されていても一切消費されないケースです — これは前述の「切り離されたネームスペース」のケースで、コンポーネントが第三の綴りを参照し、本当のトークンはどこにも供給されません。

ビルド後の grep を、ネームスペース立ち上げ時の手順に組み込みましょう。

# 1. Every utility used in markup must appear in the built CSS
grep -o "tracking-widest" dist/assets/*.css | wc -l
# 0 = the class no-ops: the theme lacks --tracking-widest,
#     so Tailwind generated no rule for it

# 2. Every semantic token must be defined AND consumed
grep -o -- "--color-myns-accent" dist/assets/*.css | wc -l
# 1 = defined once, referenced nowhere — dead or disconnected
# (grep -o counts occurrences — minified CSS puts everything on one line)

トークンリントはコンポーネントコード内の生のリテラルを検出しますが、theme が定義していないトークンを参照するユーティリティは検出できません。そのマークアップの中には「生の値」は何もなく、ユーティリティが単に未定義であるだけであり、それはビルド後の出力にしか現れません。この2つのチェックは異なる失敗のクラスをカバーしており、互いを代替するものではありません。

参考文献

Revision History

作成更新