本文へスキップ

技術

Tailwind v4 の @theme inline で、1つのサイトに複数の世界を置く

1つのサイトの中に、明るい紙面と、アプリごとに色の違う暗いページが同居しています。CSS は1本のままです。Tailwind v4 の @theme inline で、トークンの名前は共通にして値だけを差し替えています。

前は、レイアウトごと分けていました

--background のような同じ名前の変数が衝突するので、最初はページの種類ごとに CSS とルートレイアウトを分けていました。動きはします。ただ、代償がありました。

  • 世界をまたぐ移動が毎回フルリロードになる
  • アプリが増えるほど、ルート要素とフォント読み込みがN 重になる

アプリが3つになった時点で、この方式は続けられないと分かりました。

@theme inline は「値」ではなく「参照」を宣言する

@theme に値を書くと、Tailwind はその値をユーティリティに焼き込みます。inline を付けると、素の CSS 変数への参照として宣言されます。

@theme inline {
  /* 値を焼き込まず、素の CSS 変数への「参照」として宣言する */
  --color-background: var(--background);
  --color-foreground: var(--foreground);
}

:root                  { --background: #ffffff; }  /* 既定の世界 */
[data-theme='resuru']  { --background: #060e20; }  /* この中だけ別の世界 */

これで bg-backgroundbackground-color: var(--background) になります。どこで宣言された値でも、後から効きます。 器に data-theme を立てるだけで、その内側だけ世界が変わります。

CSS が1本のままなので、レイアウトを分ける必要がなくなり、世界をまたいでもソフト遷移で切り替わるようになりました。

角丸だけは、各段を独立した変数にする

ここで一度崩しました。角丸を1つの基準値から計算する形にしていたのですが、アプリごとに各段の値が違います(アプリ側の実装からそのまま写しているため)。

/* 角丸は各段を独立した変数にする。計算式を焼き込むと崩れる */
--radius-sm: var(--r-sm);
--radius-md: var(--r-md);
--radius-lg: var(--r-lg);

計算式を @theme に焼き込むと、差し替えられるのは基準値だけになり、段ごとの違いが再現できません。各段を独立した変数にしておく必要があります。

書体だけは、器の側でも指定し直す

もう1つ、器を作ったときに気づいたことがあります。フォントは「継承された計算済みの値」として降りてきます。ルート要素の時点で var(--font-body) が解決されてしまうので、内側で変数を差し替えても書体は変わりません。

器の側でもう一度 font-sans を当てて、そこで解決し直しています。

日本語の書体は定義だけで数百KBあります。そのアプリでしか使わない書体は、ルートではなくそのページの層で読み込むようにしました。使わないページに配信しないためです。

OS の設定では見た目を変えない

明るさは「いまどの世界にいるか」だけで決まります。紹介ページは常に暗く、それ以外は常に明るい。OS のダークモードには反応させません。

既定のままだと、外部から持ってきたコンポーネントが持つ dark: 付きの指定が OS 設定に反応して勝手に効いてしまうので、その条件を決して一致しないものに縛ってあります。

Notes 一覧へ戻る