Tailwind CSSでdark:を使わずにダークモードを実現する

## この記事の概要

Tailwind CSSの公式ドキュメントには[ダークモードの実現の仕方](https://tailwindcss.com/docs/dark-mode)が記載されています。

ドキュメント内にあるサンプルコードはこのようになっています。

```html
<div class="bg-white dark:bg-slate-800 rounded-lg px-6 py-8 ring-1 ring-slate-900/5 shadow-xl">
  <h3 class="text-slate-900 dark:text-white mt-5 text-base font-medium tracking-tight">Writes Upside-Down</h3>
  <p class="text-slate-500 dark:text-slate-400 mt-2 text-sm">
    The Zero Gravity Pen can be used to write in any orientation, including upside-down. It even works in outer space.
  </p>
</div>
```

ダークモード時に色が変わる要素に`dark:`をつけて色を指定しているのですが、正直面倒ではありませんか？

というわけで`dark:`をつけずにダークモードを実現できるやり方を記事にしました。

:::note
この記事を投稿した2024年12月現在、Tailwind CSSはv4がベータバージョンとして提供されています。
stableなのはv3ですが、もう間も無くv4へ移行すると考え、v3とv4両方でのやり方を紹介します。
:::

## 完成物のイメージ

次のようなコードを書くと

```html
<div className="flex flex-col gap-6 text-4xl bg-background p-8">
  <div className="bg-surface rounded p-6">
    <p className="text-text">Default color text</p>
  </div>
  <div className="bg-surface rounded p-6">
    <p className="text-primary">Primary color text</p>
  </div>
</div>
```

次のような分岐を実現できます。

| ライトモード | ダークモード |
| --- | --- |
| ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/5aeb54e3-d08c-5f31-1d9c-96bb6f18a70b.png) | ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/d1e1afd5-2f63-b4c1-b7b2-d4af1daaf8ad.png) |

`dark:`をつける必要はなく`surface`や`text`など自分で作成したエイリアスを指定するだけで済みます。

## v3でのやり方

基準となるCSSファイル（`index.css`, `main.css`, `out.css`などに該当する）ものにCSSカスタムプロパティを定義します。

グローバルトークンを定義した上で、ライトモード用とダークモード用、それぞれのエイリアストークンを定義します。

```diff_css:index.css
  @tailwind base;
  @tailwind components;
  @tailwind utilities;

+ @layer base {
+   :root {
+     /* グローバルトークン */
+     --color-gray-0: 255 255 255;
+     --color-gray-10: 245 245 245;
+     /* 以降、必要なものを定義 */
+
+     /* ライトモードでのエイリアストークン */
+     --color-background: var(--color-gray-10);
+     --color-surface: var(--color-gray-0);
+     --color-text: var(--color-gray-100);
+     --color-primary: var(--color-red-60);
+     /* 以降、必要なものを定義 */
+   }
+
+   @media (prefers-color-scheme: dark) {
+     :root {
+       /* ダークモードでのエイリアストークン */
+       --color-background: var(--color-gray-100);
+       --color-surface: var(--color-gray-90);
+       --color-text: var(--color-gray-0);
+       --color-primary: var(--color-red-50);
+       /* 以降、必要なものを定義 */
+     }
+   }
+ }
```

次に`tailwind.config.js`で`colors`を定義します。
先ほど定義したエイリアストークンを指定します。

```diff_javascript:tailwind.config.js
  /** @type {import('tailwindcss').Config} */
  export default {
    content: ["./index.html", "./src/**/*.tsx"],
    theme: {
+     colors: {
+       background: "rgb(var(--color-background))",
+       surface: "rgb(var(--color-surface))",
+       text: "rgb(var(--color-text))",
+       primary: "rgb(var(--color-primary))",
+       // 以降、必要なものを定義
+     },
    },
    plugins: [],
  };
```

これで`完成物のイメージ`に載せたものが実現できます。

## v4でのやり方

:::note warn
若干ハック気味の方法です。もしかしたらより良い方法があるかもしれません。あるいは今後のアップデートで素直な実装が可能になるかもしれません。
:::

- カスタムプロパティを宣言するのが`@leayer base`の中ではなく`@theme`の中に変更
    - `@theme`の中では`:root`は不要
- 色指定において`rgb`や`hsl`などが必要になった
    - v3ではつけると上手く動いてくれない

ただし`@theme`の中にはカスタムプロパティか`@keyframes`しか入れられないため、同名のレイヤーにダークモード時の値を入れています。
推奨されたやり方ではなく、今後どうなるか分からないため、お気をつけください。

https://github.com/tailwindlabs/tailwindcss/blob/2cbc915193a79a7865aa63488ec742cf17d53545/packages/tailwindcss/src/index.ts#L460

```diff_css:index.css
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";

- @layer base {
-   :root {
-     --color-gray-0: 255 255 255;
-     --color-gray-10: 245 245 245;
-     --color-gray-90: 45 45 45;
-     --color-gray-100: 30 30 30;
-     --color-red-50: 255 105 105;
-     --color-red-60: 233 0 0;  
-     --color-background: var(--color-gray-10);
-     --color-surface: var(--color-gray-0);
-     --color-text: var(--color-gray-100);
-     --color-primary: var(--color-red-60);
-   }
- }
+ @theme {
+   --color-gray-0: rgb(255 255 255);
+   --color-gray-10: rgb(245 245 245);
+   --color-gray-90: rgb(45 45 45);
+   --color-gray-100: rgb(30 30 30);
+   --color-red-50: rgb(255 105 105);
+   --color-red-60: rgb(233 0 0);
+   --color-background: var(--color-gray-10);
+   --color-surface: var(--color-gray-0);
+   --color-text: var(--color-gray-100);
+   --color-primary: var(--color-red-60);
+ }
  
+ @layer theme {
    @media (prefers-color-scheme: dark) {
      :root {
        --color-background: var(--color-gray-100);
        --color-surface: var(--color-gray-90);
        --color-text: var(--color-gray-0);
        --color-primary: var(--color-red-50);
      }
    }
+ }
```