
Tailwind CSS v4の@themeでデザイントークンを管理する方法
Tailwind CSS v4の@themeを使って、色・余白・フォントサイズなどのデザイントークンを管理する方法を解説します。サイト全体のデザインを統一したい場合に役立つ考え方を紹介します。
シリーズ:Tailwind CSS v4 入門
1
v3→v4 変更点まとめ
2
TS + Next.js / Bun セットアップ
3
@theme でデザイントークン管理
4
レスポンシブ・ダークモード実践
5
動的クラスと @source inline()
6
コンポーネント設計パターン集
第2回でセットアップが完了したとき、CSS ファイルに次の2行を書きました。
@import "tailwindcss";
@theme {
--color-brand-500: #0ea5e9;
}このとき「なぜ CSS 変数を書くとクラスが使えるようになるのか」「他にどんなものを定義できるのか」という疑問が残ったはずです。今回はその仕組みを根本から理解して、プロジェクト全体のデザインを CSS だけで一元管理できるようにします。
🎯 この記事のゴール
「デザイントークン」の考え方を理解し、@theme でカラー・フォント・スペーシング・角丸・影を定義して実際のコンポーネントに適用できる状態にします。また、CSS 変数として出力されることで生まれる「テーマの上書き」という応用技術も扱います。
デザイントークンとは何か
デザイントークンとは、 「色・フォント・サイズ・余白などのデザイン上の決定を、名前付きの変数として管理する考え方」 です。
たとえば「このブランドのプライマリカラーは何か?」という問いに対して、#0ea5e9 という色コードを直接コード中に散在させるのではなく、brand-primary という名前で一箇所に定義しておく——それがデザイントークンです。
これをしておくと、後からブランドカラーを変えたいときに 1箇所を書き換えるだけで全体に反映 できます。v3 ではこの役割をtailwind.config.js が担っていましたが、v4 では CSS の@theme ブロックが担います。
@theme
CSS で変数を定義
--color-brand-500
→
CSS 変数として出力
var(--color-brand-500)
ブラウザ DevTools で確認可能
→
ユーティリティクラス生成
bg-brand-500
text-brand-500 など
💡 v3 との最大の違い
v3 のtailwind.config.js で定義したテーマ値は「ビルド時のみ」参照されていました。v4 の@theme で定義した値は CSS カスタムプロパティ(変数)としてブラウザに届く ため、JavaScript や他の CSS から直接参照・上書きできます。これが後述する「テーマの上書き」を可能にする仕組みです。
@theme の基本構文
@theme ブロックの中に CSS 変数(--プレフィックス-名前: 値; )を書くだけです。プレフィックスの種類がユーティリティクラスのカテゴリを決めます。
@import "tailwindcss";
@theme {
/*
* --プレフィックス-名前: 値;
*
* プレフィックスがユーティリティのカテゴリを決める
* 例: --color-* → bg-* / text-* / border-* など
* --spacing-* → p-* / m-* / gap-* など
* --font-* → font-* クラス
*/
--color-brand-500: #0ea5e9; /* → bg-brand-500, text-brand-500 ... */
--spacing-18: 4.5rem; /* → p-18, m-18, gap-18 ... */
--font-sans: "Noto Sans JP", ui-sans-serif; /* → font-sans */
--radius-card: 12px; /* → rounded-card */
} 命名規則とクラス名の対応表
v4 には定められた命名規則があり、それに従って変数名を付けることで自動的にユーティリティクラスが生成されます。よく使うプレフィックスをすべて確認しておきましょう。
| CSS 変数(@theme に書くもの) | 生成されるユーティリティクラス |
|---|---|
| --color-{name}-{shade} | bg-{name}-{shade} text-{name}-{shade} border-{name}-{shade} ring-{name}-{shade} fill / stroke / outline / shadow / accent / caret など |
| -spacing-{name} | p-{name} / px-{name} / py-{name} m-{name} / mx-{name} / my-{name} gap-{name} / space-x-{name} w-{name} / h-{name} / size-{name} top / right / bottom / left / inset なども対応 |
| --font-{name} | font-{name} font-family を変更するクラスが生成される |
| --text-{name} | text-{name} font-size(文字サイズ)を変更するクラス |
| --font-weight-{name} | font-{name} font-weight を変更するクラス |
| --radius-{name} | rounded-{name} border-radius を変更するクラス |
| --shadow-{name} | shadow-{name} box-shadow を変更するクラス |
| --line-height-{name} | leading-{name} line-height を変更するクラス |
| --letter-spacing-{name} | tracking-{name} letter-spacing を変更するクラス |
| --z-index-{name} | z-{name} z-index を変更するクラス |
| --breakpoint-{name} | sm: / md: / lg: などのレスポンシブプレフィックス カスタムブレークポイントを定義できる |
| --animate-{name} | animate-{name} animation ショートハンドを変更するクラス |
⚠️ プレフィックスなし変数はクラスを生成しない
--brand-500: #0ea5e9; のように プレフィックスなし で書いた場合、CSS 変数としては出力されますが Tailwind のユーティリティクラスは生成されません。クラスとして使いたいなら必ず--color-brand-500 のようにプレフィックスを付けてください。
実践① カラーパレットを定義する
実際にブランドカラーを定義してみましょう。カラーパレットは「スケール」(50・100・200…900 の濃淡)を揃えておくと、ホバー色・無効色・背景色の使い分けが自然にできるようになります。
@import "tailwindcss";
@theme {
/* ─── ブランドカラー(スカイブルー系) ─── */
--color-brand-50: #f0f9ff;
--color-brand-100: #e0f2fe;
--color-brand-200: #bae6fd;
--color-brand-300: #7dd3fc;
--color-brand-400: #38bdf8;
--color-brand-500: #0ea5e9; /* メインカラー */
--color-brand-600: #0284c7;
--color-brand-700: #0369a1; /* ホバー時 */
--color-brand-800: #075985;
--color-brand-900: #0c4a6e;
/* ─── セマンティックカラー(意味で命名) ─── */
--color-success: #22c55e; /* 成功・OK → text-success, bg-success */
--color-warning: #f59e0b; /* 注意 */
--color-danger: #ef4444; /* エラー */
--color-muted: #6b7280; /* グレーアウト */
}定義後は以下のようなクラスが使えるようになります。
// ブランドカラーを使ったボタン
<button className="bg-brand-500 hover:bg-brand-700 text-white px-4 py-2 rounded">
送信
</button>
// セマンティックカラーでバッジを作る
<span className="bg-success/10 text-success border border-success/20 rounded px-2 py-0.5 text-sm">
完了
</span>
// /10 は不透明度10%を意味する(v4でもそのまま使える)
<div className="bg-danger/5 border-l-4 border-danger text-danger p-4">
エラーが発生しました
</div>🎨 ブランドカラースケールのイメージ
brand-50
brand-100
brand-200
brand-300
brand-400
brand-500
メインカラー
brand-600
brand-700
brand-800
brand-900
実践② タイポグラフィを定義する
フォントファミリー・フォントサイズ・行間をトークンとして管理します。特に日本語フォントを使う場合、ここをきちんと設定しておくと全ページで一貫した文字組みが維持できます。
@theme {
/* ─── フォントファミリー ─── */
--font-sans: "Noto Sans JP", ui-sans-serif, system-ui, sans-serif;
--font-serif: "Noto Serif JP", ui-serif, Georgia, serif;
--font-mono: "JetBrains Mono", "Fira Code", ui-monospace, monospace;
/* ─── フォントサイズ(カスタムスケールを追加) ─── */
/* 既存の text-sm / text-base / text-lg はそのまま使える */
/* ここでは「見出し専用のサイズ」を追加する例 */
--text-display: 3.5rem; /* → text-display */
--text-headline: 2.25rem; /* → text-headline */
--text-subhead: 1.125rem; /* → text-subhead */
/* ─── 行間 ─── */
--line-height-relaxed-ja: 1.9; /* → leading-relaxed-ja(日本語長文向け)*/
--line-height-tight-ja: 1.3; /* → leading-tight-ja(見出し向け) */
/* ─── 字間 ─── */
--letter-spacing-ja: 0.04em; /* → tracking-ja(日本語本文向け) */
}✨ 日本語コンテンツでの推奨設定
日本語テキストは英語より行間を広くとるのが読みやすいとされています。--line-height-relaxed-ja: 1.9 のように日本語専用のトークンを作っておくと、英語テキストと混在するページでも使い分けが明確になります。
実践③ スペーシング・サイズを定義する
デフォルトの Tailwind スペーシングスケール(4px単位)に加えて、プロジェクト固有の余白・サイズ値を追加できます。既存のスケールは上書きせず、 カスタム名で追加する のがポイントです。
@theme {
/* ─── スペーシング追加 ─── */
/* デフォルトの p-4 (1rem) / p-8 (2rem) はそのまま使える */
--spacing-18: 4.5rem; /* → p-18 / m-18 / gap-18 ... */
--spacing-22: 5.5rem; /* → p-22 など */
--spacing-128: 32rem; /* → max-w-128 など */
/* ─── コンポーネントサイズ(意味のある名前で管理)─── */
--spacing-header: 4rem; /* → h-header(ヘッダーの高さ)*/
--spacing-sidebar: 16rem; /* → w-sidebar(サイドバー幅) */
--spacing-content: 52rem; /* → max-w-content(本文幅) */
/* ─── 角丸 ─── */
--radius-card: 12px; /* → rounded-card */
--radius-button: 8px; /* → rounded-button */
--radius-badge: 4px; /* → rounded-badge */
} 「意味のある名前」でトークンを作ることがポイントです。p-18 と書くよりh-header と書いた方が、後からコードを読んだ人が「これはヘッダーの高さだ」とすぐ理解できます。
実践④ デフォルト値を上書きする:@theme inline
v4 には@theme inline という構文があります。通常の@theme との違いを押さえておくと、柔軟なテーマ管理ができます。
| 構文 | 動作 | 使いどころ |
|---|---|---|
@theme { } | 新しいトークンを追加する | ブランドカラーや独自スペーシングを追加したいとき |
@theme inline { } | 既存のトークンを参照・上書きする | 「白を--color-white に差し替える」など既存値を変えたいとき |
使用例:白・黒をカスタム値で定義し直す
@theme {
/* 追加トークン */
--color-brand-500: #0ea5e9;
}
@theme inline {
/*
* 既存の --color-white を「ほぼ白」に差し替える
* これにより bg-white / text-white の実際の色が変わる
*/
--color-white: #fafaf9; /* 真っ白より少し暖かい白 */
--color-black: #1a1a1a; /* 真っ黒より柔らかい黒 */
}✨ @theme inline の「inline」の意味
inline は「 CSS の出力に変数定義を埋め込む 」という意味です。通常の@theme はビルド後のCSSに変数定義として出力されますが、@theme inline は既存の変数を参照・上書きするために使います。なお第5回で扱う@source inline() とは別の概念なので混同しないよう注意してください。
応用:CSS 変数として出力されることを活かす
v4 の最大の特長は、@theme で定義した値が CSS カスタムプロパティとして実際のブラウザに届くことです。これにより、 ビルドを経由せずに値を参照・上書きできる ようになります。
パターン①:通常 CSS から参照する
Tailwind クラスで表現しにくい複雑なスタイルでも、CSS 変数として定義されているのでそのまま参照できます。
/* @theme で定義した値は var(--変数名) で参照できる */
.custom-gradient {
background: linear-gradient(
135deg,
var(--color-brand-400),
var(--color-brand-700)
);
}
.page-container {
max-width: var(--spacing-content); /* --spacing-content: 52rem と定義したもの */
padding-top: var(--spacing-header); /* ヘッダーの高さ分だけ余白 */
} パターン②:特定のコンポーネントだけテーマを上書きする
v4 の CSS 変数出力の最大の恩恵がこれです。 特定のセクションだけ別のカラーテーマを適用する ことが、追加の JavaScript なしで実現できます。
@theme {
--color-brand-500: #0ea5e9; /* デフォルトは青 */
}
/* .theme-warm クラスが付いた要素の中では brand-500 が緑になる */
.theme-warm {
--color-brand-500: #22c55e; /* 緑に上書き */
--color-brand-700: #15803d;
}// 通常セクション:bg-brand-500 が青になる
<section>
<button className="bg-brand-500 text-white px-4 py-2">青いボタン</button>
</section>
// .theme-warm セクション内:同じ bg-brand-500 が緑になる
<section className="theme-warm">
<button className="bg-brand-500 text-white px-4 py-2">緑のボタン</button>
</section>✨ v3 では実現できなかったこと
v3 のtheme.extend で定義した値はビルド時に解決される固定値でした。そのため「特定のコンポーネントだけブランドカラーを変える」には別クラスを用意したり JavaScript で切り替えたりする必要がありました。v4 の CSS 変数ベースのアーキテクチャなら、CSSのカスケードの仕組みをそのまま使えます。
パターン③:JavaScript から CSS 変数を読み書きする
React / TypeScript のコードから直接テーマ値を参照したり、動的に変更したりすることもできます。
// CSS 変数の値を JavaScript から読み取る
const brandColor = getComputedStyle(document.documentElement)
.getPropertyValue("--color-brand-500")
.trim();
// → "#0ea5e9"
// CSS 変数の値を JavaScript から書き換える(ランタイムでテーマを切り替える例)
document.documentElement.style
.setProperty("--color-brand-500", "#8b5cf6"); // 紫に変更
// React での実用例:テーマ切り替えボタン
function ThemeSwitcher() {
const applyTheme = (color: string) => {
document.documentElement.style
.setProperty("--color-brand-500", color);
};
return (
<div className="flex gap-2">
<button onClick={() => applyTheme("#0ea5e9")}>青</button>
<button onClick={() => applyTheme("#8b5cf6")}>紫</button>
</div>
);
} 完成形:実際のプロジェクトで使える @theme テンプレート
ここまでの内容をまとめた、実際のプロジェクトに貼り付けて使えるテンプレートです。コメントを読みながら、プロジェクトに合わせて値を書き換えてください。
@import "tailwindcss";
@theme {
/* ══════════════════════════════════════════
* カラー
* ══════════════════════════════════════════ */
/* ブランドカラー:プロジェクトに合わせて変更する */
--color-brand-50: #f0f9ff;
--color-brand-100: #e0f2fe;
--color-brand-300: #7dd3fc;
--color-brand-500: #0ea5e9;
--color-brand-700: #0369a1;
--color-brand-900: #0c4a6e;
/* セマンティックカラー:UIの状態を表す */
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-danger: #ef4444;
--color-muted: #6b7280;
/* ══════════════════════════════════════════
* タイポグラフィ
* ══════════════════════════════════════════ */
--font-sans: "Noto Sans JP", ui-sans-serif, system-ui, sans-serif;
--font-serif: "Noto Serif JP", ui-serif, Georgia, serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--line-height-relaxed-ja: 1.9;
--line-height-tight-ja: 1.3;
--letter-spacing-ja: 0.04em;
/* ══════════════════════════════════════════
* スペーシング・レイアウト
* ══════════════════════════════════════════ */
/* Tailwind のデフォルトにない値だけ追加する */
--spacing-18: 4.5rem;
--spacing-22: 5.5rem;
/* コンポーネント固有のサイズ(意味のある名前で管理) */
--spacing-header: 4rem;
--spacing-sidebar: 16rem;
--spacing-content: 52rem;
/* ══════════════════════════════════════════
* 角丸・シャドウ
* ══════════════════════════════════════════ */
--radius-card: 12px;
--radius-button: 8px;
--radius-badge: 4px;
--shadow-card: 0 1px 3px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.04);
--shadow-dropdown: 0 4px 16px rgba(0,0,0,0.12);
/* ══════════════════════════════════════════
* ブレークポイント(デフォルトを上書きする場合のみ)
* ══════════════════════════════════════════ */
/* デフォルト:sm=640px md=768px lg=1024px xl=1280px 2xl=1536px */
/* 変更が必要な場合のみ記載する */
/* --breakpoint-tablet: 900px; */
}
/* デフォルト値を上書きする場合は @theme inline を使う */
@theme inline {
--color-white: #fafaf9; /* 少し暖かい白 */
--color-black: #1a1a1a; /* 少し柔らかい黒 */
} よくあるトラブルと解決策
❓ @theme に変数を書いたのにクラスが使えない
最も多い原因はプレフィックスのミスです。--brand-500 (プレフィックスなし)ではなく--color-brand-500 (--color- プレフィックス付き)と書いてください。対応表を見て、どのプレフィックスを付けるべきか確認してください。
❓ --spacing-header を定義したのに w-header が効かない
--spacing-* の変数はp-* ・m-* ・w-* ・h-* ・gap-* など幅広いクラスを生成しますが、幅(w-* )として使うにはw-header と書く必要があります。また、開発サーバーを再起動しないとクラスが認識されないケースがあるため、一度npm run dev /bun run dev を再起動してみてください。
❓ @theme と @theme inline の使い分けがわからない
シンプルに覚えるなら 「新しいものを追加するときは@theme 、既存のデフォルト値を変えたいときは@theme inline 」です。たとえばカスタムカラーを追加するのは@theme 、Tailwind の--color-white を別の値にしたいのは@theme inline を使います。
❓ @theme に変数を書いたのにクラスが使えない
最も多い原因はプレフィックスのミスです。--brand-500 (プレフィックスなし)ではなく--color-brand-500 (--color- プレフィックス付き)と書いてください。対応表を見て、どのプレフィックスを付けるべきか確認してください。
第3回のまとめ
今回学んだこと
- デザイントークンとは「色・フォント・余白などのデザイン決定を名前付き変数で管理する考え方」。一箇所を変えれば全体に反映される
@themeブロックに CSS 変数を書くとユーティリティクラスが自動生成される。プレフィックス(--color-・--spacing-・--font-など)がクラスのカテゴリを決める- 新しいトークンを追加するには
@theme、Tailwind のデフォルト値を上書きするには@theme inlineを使う @themeの値は CSS カスタムプロパティとしてブラウザに届くため、CSS のvar()・JS のgetPropertyValue()で参照・変更できる- セクションに CSS クラスを当てて変数を上書きするだけで、JS 不要のローカルテーマ切り替えが実現できる
📌 第3回 まとめ
プロジェクト全体のデザインを@theme で一元管理できるようになりました。次回は定義したトークンを使って「レスポンシブデザイン」と「ダークモード」を実装します。v4 での書き方は v3 と一部異なるため、実際のコードを書きながら違いを確認していきましょう。
📝 ▶ 次回(第4回)
次は「 」です。
