【ダークモード対応】Astro + Svelte 5環境にTailwindCSSを導入

【ダークモード対応】Astro + Svelte 5環境にTailwindCSSを導入

現在、AstroとSvelte 5、TypeScriptというモダンな構成で個人のプロジェクトを進めています。これまでスタイリングにはSCSSを利用していましたが、より効率的でコンポーネント指向な開発を目指して、人気のCSSフレームワークであるTailwindCSSを導入してみることにしました。

TailwindCSS #Astro#Svelte#ハンズオン

【ダークモード対応】Astro + Svelte 5環境にTailwindCSSを導入

サムネイル

現在、AstroとSvelte 5、TypeScriptというモダンな構成で個人のプロジェクトを進めています。これまでスタイリングにはSCSSを利用していましたが、より効率的でコンポーネント指向な開発を目指して、人気のCSSフレームワークであるTailwindCSSを導入してみることにしました。

更新日: 6/18/2025

実際に作業したリポジトリはこちらで公開しています。

TailwindCSSを導入するメリット

今回、SCSSからTailwindCSSへの移行を検討した背景には、TailwindCSSが持つ以下のような一般的なメリットがあります。

  • ユーティリティファーストによる迅速なUI構築: flex, pt-4, text-centerのような具体的なスタイルを持つクラスをHTMLに直接記述していくため、CSSファイルとHTMLファイルを行き来することなく、マークアップ内で迅速にスタイリングを完結させることができます。
  • デザインシステムの一貫性維持: tailwind.config.jsファイルにカラースキーム、スペースの単位、フォントサイズなどのデザイン制約を定義できます。これにより、プロジェクト全体で一貫したデザインを保ちやすく、偶発的なスタイルの逸脱を防ぎます。
  • レスポンシブデザインの実装の容易さ: md:, lg:といったブレークポイントプレフィックスをクラスに付与するだけで、直感的にレスポンシブ対応のスタイルを記述できます。
  • ビルド時の最適化: プロジェクト内で使用されていないユーティリティクラスは、ビルド時に自動的に削除(Purge)されます。これにより、最終的なCSSファイルのサイズを最小限に抑えることができ、パフォーマンス向上に貢献します。

手順1: Astro公式インテグレーションで簡単セットアップ

Astroには多くのツールを簡単に導入するためのインテグレーション(統合機能)が用意されています。今回はこれを利用してTailwindCSSを導入しました。

ターミナルでプロジェクトのディレクトリに移動し、以下のコマンドを実行するだけです。

cd frontend && pnpm astro add tailwind

コマンドを実行すると、いくつか質問されますが、基本的にはすべて yes で進めて問題ありません。

✔ Continue? … yes
⠋ Installing dependencies...
✔ Installing dependencies...

  Astro will scaffold ./src/styles/global.css.

✔ Continue? … yes

  Astro will make the following changes to your config file:

 ╭ astro.config.mjs ─────────────────────────────╮
 │ // @ts-check                                  │
 │ import { defineConfig } from "astro/config";  │
 │                                               │
 │ import svelte from "@astrojs/svelte";         │
 │                                               │
 │ import tailwindcss from "@tailwindcss/vite";  │
 │                                               │
 │ // https://astro.build/config                 │
 │ export default defineConfig({                 │
 │   integrations: [svelte()],                   │
 │                                               │
 │   vite: {                                     │
 │     plugins: [tailwindcss()],                 │
 │   },                                          │
 │ });                                           │
 ╰───────────────────────────────────────────────╯

✔ Continue? … yes

   success  Added the following integration to your project:
  - tailwind

このコマンド一つで、以下の作業が自動的に行われました。

  • 必要なパッケージ(@tailwindcss/vite@^4.1.10, tailwindcss@^4.1.10)のインストール
  • astro.config.mjs にTailwindCSSをViteプラグインとして追加する設定を追記
  • TailwindCSSのディレクティブをインポートするための src/styles/global.css ファイルの生成

本当にあっという間に基本的なセットアップが完了しました。

自動で更新されたastro.config.mjsは以下のようになっています。

// @ts-check
import { defineConfig } from "astro/config";

import svelte from "@astrojs/svelte";

import tailwindcss from "@tailwindcss/vite";

// https://astro.build/config
export default defineConfig({
  integrations: [svelte()],

  vite: {
    plugins: [tailwindcss()],
  },
});

また、生成されたsrc/styles/global.cssには、TailwindCSSの基本スタイルを読み込むための記述がされています。

@import "tailwindcss";

最後に、このglobal.cssをレイアウトファイルやページにインポートします。今回はsrc/pages/index.astroに追記しました。

---
import Counter from '../components/Counter.svelte';
// この行を追加
import '../styles/global.css';
---

これで、プロジェクト全体でTailwindCSSが使えるようになりました。

手順2: Svelteコンポーネントとの統合を確認

次に、もともとSCSSでスタイリングしていたCounter.svelteコンポーネントをTailwindCSSのユーティリティクラスで書き換えてみました。

変更前 (SCSS)

<style lang="scss"> ブロックにスタイルを記述する、一般的なSvelteのコンポーネントでした。

<div class="counter">
  <h2>Svelte Counter Component</h2>
  <p class="count">Count: {count}</p>
  <div class="buttons">
    <button onclick={increment}>+</button>
    <button onclick={decrement}>-</button>
    <button onclick={reset}>Reset</button>
  </div>
</div>

<style lang="scss">
  .counter { /* ... */ }
  /* 他のスタイル */
</style>

変更後 (TailwindCSS)

<style>ブロックをばっさり削除し、HTMLのclass属性に直接ユーティリティクラスを記述していきます。

<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 shadow-lg">
  <h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-4">Svelte Counter Component</h2>
  <p class="text-3xl font-bold text-gray-900 dark:text-white mb-6">Count: {count}</p>
  <div class="flex gap-3 justify-center">
    </div>
</div>

dark: プレフィックスを使うことで、ダークモード時のスタイルも同時に定義できるのが非常に便利です。これによりCSSファイルとSvelteファイルを往復する必要がなくなりました。

Svelte 5 Runesを使った新規コンポーネントでのテスト

さらに、Svelte 5の新しい構文であるRunes ($props) を使った新規コンポーネントでも問題なく動作するか確認しました。

<script lang="ts">
  const { 
    title, 
    description, 
    imageUrl = '', 
    buttonText = 'Learn More' 
  } = $props<{
    title: string;
    description: string;
    imageUrl?: string;
    buttonText?: string;
  }>();
</script>

<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
  </div>

このCardコンポーネントをResponsiveGridコンポーネントで#eachを使って表示してみましたが、こちらも全く問題ありませんでした。

<script lang="ts">
  type Item = {
    id: number;
    title: string;
    description: string;
    imageUrl?: string;
  };

  const { items } = $props<{
    items: Item[];
  }>();

  import Card from './Card.svelte';
</script>

<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-4">
  {#each items as item (item.id)}
    <Card 
      title={item.title}
      description={item.description}
      imageUrl={item.imageUrl}
      buttonText="詳細を見る"
    />
  {/each}
</div>

手順3: システムテーマの自動切り替え機能を実装

ダークモードに対応させるだけならdark:プレフィックスで十分ですが、今回はもう一歩進んで、OSのテーマ設定(ライト/ダーク)が変更されたときに、Webページもリアルタイムで追従するようにしてみました。

まず、テーマを判定して<html>タグにdarkクラスを付け外しする簡単なスクリプトを作成します。

(function() {
  // システムテーマの検出
  function getSystemTheme() {
    return window.matchMedia('(prefers-color-scheme: dark)').matches;
  }

  function applyTheme(isDark) {
    if (isDark) {
      document.documentElement.classList.add('dark');
    } else {
      document.documentElement.classList.remove('dark');
    }
  }

  function initTheme() {
    const isDark = getSystemTheme();
    applyTheme(isDark);
  }

  // システムテーマの変更を監視して、リアルタイムに反映
  function watchSystemTheme() {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    
    mediaQuery.addEventListener('change', (e) => {
      applyTheme(e.matches);
    });
  }

  initTheme();

  // DOMが読み込まれた後に監視をはじめる
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', watchSystemTheme);
  } else {
    watchSystemTheme();
  }
})();

そして、このスクリプトをAstroのページやレイアウトの<head>内で読み込みます。

<head>
  <meta charset="utf-8" />
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
  <meta name="viewport" content="width=device-width" />
  <meta name="generator" content={Astro.generator} />
  <title>Astro + Svelte Demo</title>
  <script src="../scripts/theme.js"></script>
</head>

この実装により、OSの設定でテーマを切り替えると、リロードすることなく即座にWebページの表示が切り替わるようになりました。

手順4: 最終確認と総評

開発サーバー(pnpm dev)での動作確認はもちろん、プロダクションビルド(pnpm build)も試してみました。
ビルド後のCSSファイルは 12.9KB と非常に軽量でした。TailwindCSSはビルド時にプロジェクト内で使用されていないユーティリティクラスを自動的に削除(Purge)してくれるため、最終的なファイルサイズを小さく抑えることができます。

Svelte 5のclass:ディレクティブによる動的なクラス適用も、従来通り使用可能です。

<div class:bg-red-500={isError} class:bg-green-500={isSuccess}>
  </div>

総評

TailwindCSSの基本的な使い方は、やはりユーティリティクラスをHTML要素に直接適用していく方法が最もシンプルかつ効率的です。しかし、ボタンのように繰り返し利用するUIコンポーネントで、毎回多くのユーティリティクラスを記述するのは冗長に感じられることもあります。

そのような場合は、Svelteコンポーネントの<style>ブロック内で@applyディレクティブを使い、意味的なクラス名(例: .btn-primary)にユーティリティクラス群をまとめるアプローチが有効です。

<style>
  .btn-primary {
    @apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
  }
</style>

このように、基本はユーティリティクラスを直接使い、再利用性の高い共通パーツのみ@applyでまとめる、というバランスを取ることが、プロジェクトが大規模になっても破綻しないための現実的な使い方だと考えられます。

今回はdark:プレフィックスを使ってダークモード対応を行いましたが、より複雑なテーマ管理(例えば、ライト/ダーク以外のテーマも追加する場合など)を視野に入れると、この方法ではマークアップが煩雑になる可能性があります。

今後の更新では、global.cssを拡張し、CSS変数を活用してテーマ別のスタイルを一元管理する方法を探求していこうと思います。これにより、コンポーネント側はdark:プレフィックスを意識する必要がなくなり、よりクリーンな記述を保てるのではないかと考えています。

参考資料

検索

検索条件に一致する記事が見つかりませんでした