Tailwind には arbitrary values(w-[346px] のような任意値を設定できる機能)があります。
運用する上でルールを定めていたのですが、運用してみて見えてきた失敗を踏まえて、ルールを書き直した話を記事にしました。
今後みなさんが TailwindCSS を利用する際にルールを決めないといけない時の糧にしていただけたら幸いです。
Tailwind の arbitrary values 利用時の懸念点
arbitrary values と呼ばれる機能があり、角括弧の中に任意の値を書くことで、Tailwind が用意していない値を直接指定できます。
<div className="w-[346px] mt-[18px] text-[#1a73e8]" />
非常に便利な機能ですが、プロジェクトのデザイントークンから逸脱しやすい という側面があり、多用しすぎるとデザインルールを無視したデザインが実装される原因になります。
今回題材にしたい運用方針
最初に定めた方針はシンプルでした。
- 新規コードでは arbitrary values を使わない
- デザイントークン(
@themeで定義された値)または Tailwind 標準スケールを使う - やむを得ず必要な場合は デザイナーと相談して
@themeにトークンを追加 する
「禁止 → どうしても必要ならトークン化」というスタンスです。
デザイントークンの一貫性を保つには良いルールに見えました。
実はこの方針、すぐに問題があることに気がつきました…。
3つの問題
「全面禁止」方針で運用していて、3 つの問題が見えてきました。
問題① @theme の変数が増え続けてしまう
「使わない代わりに @theme に追加する」というルールは、裏を返せば 使うたびにトークンが増えていく ということです。
/* @theme に追加されていく値たちがどんどん増えます */
--width-button-sm: 9.6rem;
--width-card-md: 21.6rem;
--width-card-lg: 34.6rem;
--width-modal: 48rem;
しかも、1 回しか使わない値もトークン化される ため、トークン一覧を見ても「これ何のため?」と分からなくなります。
基盤となる Primitive 層が肥大化して、本来の役割であるはずの「デザインの骨格」が見えなくなる本末転倒な状態です。
当然ネームスペースは奪われ、名称も長くなるためどんどん苦しくなります…。
問題② デザイナーとの擦り合わせコストが想定より重い
「やむを得ず必要な場合はデザイナーと相談」というのも、毎回やると重い作業でした。
- 「この
15.4remって、16remに丸めていい?」 - 「この値、他の画面でも使いそう?」
- 「トークン名は何にする?」
開発を急ぎたい場面で コミュニケーションが足枷になり、ついついarbitrary valuesが利用されてしまいます。
ルールが守られない原因の多くは「実装者の意識」ではなく「ルールが現場の速度に合っていないこと」にあると考えています。
問題③ 例外として書いた後のルールが無い
開発を急ぎたい場面で、やむなく arbitrary values を書くこともあります。
// とりあえず急ぎで実装
<div className="w-[15.4rem]" />
問題は、「とりあえず書いた」後の扱いが決まっていない ことです。
- いつトークン化するのか
- 誰が判断するのか
- そのまま残しても良い基準は何か
例外として書いた arbitrary values が、誰にも昇格されないまま塩漬けになり、気付けばコードベース全体に散らばります。
どうしたか?3層構造で値を管理する方針に変更
arbitrary values は「使うな」と一律で禁止せず、3層構造(Primitive / Semantic / Arbitrary) と ファイル数による段階的な判断 で運用することにしました。
3層構造で値を管理する
Layer 1: Primitive(基盤) 少数のスケール(spacing-8, color-blue-500 など)
Layer 2: Semantic(意味) purpose-driven(gap-card, padding-section など)
Layer 3: Arbitrary(緊急避難) 真に1回限りの逸脱
ポイントは、Layer 1(基盤)を増やさないことを徹底するところです。
新しい値が必要になったら、Layer 2(意味で名付けたトークン) で追加します。
/* ❌ Before: Primitive を肥大化させる */
--width-15-4rem: 15.4rem;
/* ✅ After: Semantic として「何のための値か」を名前に込める */
--width-card-md: 15.4rem;
これで、トークン一覧を見たときに「これは何のための値か」が分かるようになります。
ファイル数で段階的に判断
「いつトークン化するか」を、使用ファイル数でルール化しました。
| 使用ファイル数 | 扱い |
|---|---|
| 1 ファイル目 | arbitrary value を許容(コメントで理由を残す) |
| 2 ファイル目 | レビューで semantic token 化を検討 |
| 3 ファイル目 | 必ず semantic token 化する |
最初から「全部トークン化」を強制せず、「広がってきたら昇格させる」 という運用です。
これなら、本当に 1 回しか使わない値はそのままで済むし、よく使う値は自然と semantic token に育っていきます。
迷う場面が減って、レビューでも「これはルール上どうあるべきか」を即答できるようになりました。
ルール変更後の現場の変化
旧方針と新方針で、現場での挙動がどう変わったかを並べてみます。
| ケース | Before(全面禁止) | After(3層運用) |
|---|---|---|
| 1 回しか使わない値 | デザイナーと相談 → トークン化(重い) | arbitrary value で OK |
| 複数箇所で使い回す値 | 都度 @theme に追加(命名が場当たり的) |
semantic token として 意味で命名 |
| 急ぎの実装 | ルールを破って arbitrary value | ルール内で arbitrary value を許容 |
| 例外の追跡 | 仕組みなし → 塩漬け化 | ファイル数で自動的に昇格判断 |
禁止ではなく、グラデーションを持たせることで無理なく守れるルールとしました。
開発の速度を落とさず、開発者が無理のないルールの中で運用できる土台を整えられます。
ルールは「現場のフィードバックで進化させる」
最初のルールが間違ってはいなかったと思っています。
問題は、ルールを定めた時点では見えなかった「運用してみないと分からない壁」を、ルールに反映する仕組みがなかったこと。
ルールを作ったら終わりではなく、現場のフィードバックを受けて育てるものだと痛感しました…。
ルールを作るときには、
- このルールは「現場の速度に合っているか」
- 例外が出たときの「次のアクション」が定義されているか
理想だけで作ったルールは、現場を混乱させるだけでなく大きな負債を残す結果につながりかねません…。
初めから全てが守られるように作れることは理想ですが、プロジェクトの状況は千差万別です。
リリーススケジュール、チームのスキル熟練度、運用体制さまざまな状況を考慮して無理のないルールを作れることが安定運用の基盤となると実感しました。