3層フォントサイズ戦略
問題
UI を構築する際、フォントサイズの値を直接使いたくなりがちです。あるコンポーネントでは font-size: 1.25rem、別のコンポーネントでは font-size: 20px、さらに別のコンポーネントでは font-size: 1.3rem と、すべて「やや大きいテキスト」を意味しています。これでは生の値がコードベース全体に散らばり、一元管理できるポイントがありません。
よくある最初の改善策は、セマンティックトークンを定義することです。--font-heading、--font-body、--font-caption のように名前を付けます。しかし、これは2つの関心事を1つのレイヤーに混在させています。生のサイズ値とそのセマンティックな役割です。見出しを 28px から 24px に縮小する場合、トークンを変更します。しかし、たまたま同じサイズだったために --font-heading を使っていたナビリンクも、意図せず縮小されてしまいます。
反対のアプローチとして、text-lg のような抽象的なスケールのみを使う方法があります。役割との結合問題は避けられますが、セマンティックな明確さが失われます。開発者はコードベース全体に散らばった text-lg を見て、それが見出しなのか、サブタイトルなのか、単に強調テキストなのかを判断しなければなりません。
どちらのアプローチも単独では不十分です。必要なのは、どのくらいの大きさか(生の値)と_何のためか_(セマンティックな役割)の分離です。
解決方法
フォントサイズを3つのティアに整理し、それぞれに明確な目的を持たせます。
| ティア | 名前 | 目的 | 例 |
|---|---|---|---|
| 1 | スケール | 抽象的なサイズ値 — 利用可能なステップ | --scale-lg → 1.25rem |
| 2 | テーマ | セマンティックな役割 — 各サイズの_意味_ | --font-heading → var(--scale-xl) |
| 3 | コンポーネント | スコープ付きオーバーライド — 1つのコンポーネント固有のサイズ | --_card-title → var(--font-subheading) |
重要なポイントは、各ティアはその上のティアのみを参照するということです。コンポーネントはテーマトークンを使います。テーマトークンはスケール値を指します。スケールは実際の rem/px 値を保持します。
これは 3層カラー戦略(パレット → テーマ → コンポーネント)と同じアーキテクチャを、フォントサイズに適用したものです。
コード例
ティア 1: スケール
スケールは素材そのものです。システムで利用可能なすべてのフォントサイズが含まれます。これらの値はコンポーネントで直接使いません。絵の具のチューブのようなものです。用意はしておきますが、計画なしにキャンバスに直接絞り出すことはしません。
Tailwind プロジェクトでは、ティア 1 は @theme ブロックに配置します。
@theme {
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */
--font-size-base: 1rem; /* 16px */
--font-size-lg: 1.25rem; /* 20px */
--font-size-xl: 1.75rem; /* 28px */
--font-size-2xl: 2.5rem; /* 40px */
}
Tailwind の @theme 設定の詳細(ペアとなる line-height や weight トークンを含む)については、タイポグラフィトークンパターンを参照してください。
ティア 2: テーマ
テーマトークンはスケール値にセマンティックな意味を与えます。コンポーネントから見ると「lg」ではなく「subheading」や「heading」になります。タイポグラフィの調整を容易にするレイヤーです。--font-heading を --scale-xl から --scale-lg に一箇所で変更すれば、プロジェクト内のすべての見出しが更新されます。
CSS では、ティア 2 は :root(または共有スコープ)に配置します。
:root {
--font-display: var(--scale-2xl);
--font-heading: var(--scale-xl);
--font-subheading: var(--scale-lg);
--font-body: var(--scale-base);
--font-secondary: var(--scale-sm);
--font-caption: var(--scale-xs);
}
ティア 3: コンポーネントスコープのサイズ
コンポーネントによっては、グローバルテーマに収まらないフォントサイズの判断が必要になることがあります。テキストが小さいコンパクトなサイドバー、金額が特大の料金カード、密度の高い管理画面などです。これらがティア 3 の変数です。スコープが狭く、コンポーネント自体に定義され、テーマまたはスケールのトークンを参照します。
ℹ️ アンダースコア命名規則
コンポーネントスコープのカスタムプロパティには、先頭にアンダースコア(--_)を付けてローカルスコープであることを示します。
.pricing-card {
--_card-amount: var(--scale-2xl);
--_card-period: var(--font-caption);
}--_ プレフィックスは「この変数はこのコンポーネントにローカルスコープされている」ことを読み手に伝えます。他の言語で _privateMethod がプライベートスコープを示すのと同様の規則です。これは CSS のルールではなく、プロジェクト内における命名規則の例です。
ℹ️ Tailwind + コンポーネントファーストの例外
Tailwind + コンポーネントファーストのプロジェクト(React、Vue、Astro でユーティリティクラスを使用)では、ティア 3 のコンポーネントスコープ CSS カスタムプロパティが必要になることはほとんどありません。コンポーネントフレームワーク自体がスコープを提供するため、これらの変数を定義する別の CSS ファイルが存在しません。ティア 3 は主に一般的な CSS アプローチ(BEM、CSS Modules、バニラ CSS)で必要になります。
各コンポーネントが独自のフォントサイズ変数を定義しつつ、テーマまたはスケールのティアを参照している点に注目してください。料金カードは --_card-amount: var(--scale-2xl)(スケール)と --_card-desc: var(--font-secondary)(テーマ)を使っています。サイドバーナビはすべてのサイズにテーマトークンを参照しています。グローバルなタイプスケールが変更されると、これらのコンポーネントも自動的に更新されます。
3つのティアの連携
このデモは、CSS に3つのティアすべてが表示された完全なページレイアウトを示しています。ティア 1 が生のスケールを定義し、ティア 2 がそれをセマンティックな役割にマッピングし、ティア 3 が stat カードに独自のローカルサイズ変数を与えています。
ティア 2 の威力: サイズテーマの切り替え
このアーキテクチャの最も強力な特徴は、同じマークアップがまったく異なるサイズテーマで動作することです。ティア 2(セマンティックトークン)を再マッピングするだけで、UI 全体が調整されます。コンパクトモード、大きい/アクセシブルモード、密度設定などを、コンポーネントの CSS に一切触れずに実装できます。
マークアップは3つのカラムすべてで同一です。変わるのはティア 2 のマッピングだけです。
- Default: heading → xl、body → base、caption → xs
- Compact: heading → lg、body → sm、caption → xs(すべて1ステップ下にシフト)
- Large: heading → 2xl、body → lg、caption → sm(すべて1ステップ上にシフト)
ティア 1 とティア 3 はまったく同じです。変わるのはティア 2 だけです。
完全な CSS コード構造
3つのティアが実際のプロジェクトでどのように組み合わさるかを示します。
/* ── Tier 1: Scale ── */
/* In Tailwind, this goes in @theme */
:root {
--scale-xs: 0.75rem;
--scale-sm: 0.875rem;
--scale-base: 1rem;
--scale-lg: 1.25rem;
--scale-xl: 1.75rem;
--scale-2xl: 2.5rem;
}
/* ── Tier 2: Theme ── */
/* Semantic roles — change these to adjust the entire UI */
:root {
--font-display: var(--scale-2xl);
--font-heading: var(--scale-xl);
--font-subheading: var(--scale-lg);
--font-body: var(--scale-base);
--font-secondary: var(--scale-sm);
--font-caption: var(--scale-xs);
}
/* ── Tier 3: Component ── */
/* Scoped overrides — only when a component needs its own size logic */
.pricing-card {
--_card-amount: var(--scale-2xl);
--_card-label: var(--font-caption);
}
.sidebar-nav {
--_nav-link: var(--font-secondary);
--_nav-category: var(--font-caption);
}
Tailwind CSS との統合
Tailwind v4 プロジェクトでは、3つのティアは既存のパターンに自然にマッピングされます。
ティア 1 → Tailwind の @theme ブロック。制約付きスケールを定義する場所です。
@theme {
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.25rem;
--font-size-xl: 1.75rem;
--font-size-2xl: 2.5rem;
}
これにより text-xs から text-2xl のユーティリティが使えるようになります。ペアとなる line-height や weight トークンを含む完全な Tailwind 設定については、タイポグラフィトークンパターンを参照してください。
ティア 2 → :root 上の CSS カスタムプロパティ。Tailwind ユーティリティではなく、CSS で使用するセマンティックトークンです。
:root {
--font-heading: var(--font-size-xl);
--font-subheading: var(--font-size-lg);
--font-body: var(--font-size-base);
--font-secondary: var(--font-size-sm);
--font-caption: var(--font-size-xs);
}
コンポーネントは CSS でこれらを参照します: font-size: var(--font-heading)。
ティア 3 → コンポーネントスコープの CSS カスタムプロパティ。一般的なパターンと同じです。
AI がよくやるミス
- ティア 2 を省略する — スケール値をコンポーネントで直接使う(
font-size: var(--scale-lg)やtext-lgを至るところで使う)と、セマンティックレイヤーがなくなります。見出しサイズの変更にすべてのコンポーネントの更新が必要になります - ティア 1 にセマンティックな名前を使う —
@themeで--font-size-headingを定義すると、スケールが特定の役割に固定されます。見出し以外に同じサイズが必要な場合、トークン名が誤解を招きます - スケールとテーマを分離しない —
--font-heading: 1.75remのようにハードコードされた値で定義すると、スケール全体を比例的に調整できません。見出しサイズがタイプシステムの他の部分と切り離されてしまいます - ティア 3 の変数が多すぎる — コンポーネントが 10 個以上のローカルフォントサイズ変数を定義している場合、テーマレイヤーを再発明している可能性があります。ティア 2 に昇格させましょう
- ティア 1 が小さすぎる — サイズが 3 段階しかないスケールでは、コンポーネントが独自の生の値(ハードコードされた rem 値のティア 3 変数)を発明せざるを得なくなり、システムが壊れます
使い分け
- コンポーネントが数個以上あるプロジェクト — 一貫したタイポグラフィが必要になった時点で、3層構成のコストは回収できます
- マルチ密度またはアクセシビリティモード — ティア 2 により、コンパクト/ゆったり/アクセシブルの切り替えが容易になります
- デザインシステムやコンポーネントライブラリ — コンポーネントは生のスケール値ではなくテーマトークンを参照すべきです
- 段階的な導入 — ティア 1 + 2 から始めて、コンポーネントにスコープ付きオーバーライドが必要になったらティア 3 を追加できます
3層構成が過剰なケース
- タイプスケールが1つで密度バリエーションのないシングルページサイト
- メンテナンス性よりスピードが重要なクイックプロトタイプ
- CSS を1人の開発者だけが触るプロジェクト
関連記事
- タイポグラフィトークンパターン — Tailwind でのティア 1 の実践的な
@theme設定 - 3層カラー戦略 — 同じ3層アーキテクチャをカラーに適用したもの
- 行の高さのベストプラクティス — タイプスケールに合わせた line-height の選び方
- clamp()を使った流体フォントサイズ —
clamp()でティア 1 の値をレスポンシブにする方法