T-CREATOR

Nuxt でダークモード&テーマ切替を爆速実装

Nuxt でダークモード&テーマ切替を爆速実装

ユーザーの目の疲労軽減や視認性向上のため、ダークモードは現代のWebアプリケーションには欠かせない機能となっています。

特にNuxtアプリケーションにおいては、SSR環境での適切な実装が求められるため、多くの開発者が課題を抱えているのが現状です。今回は、Nuxtでダークモード&テーマ切替を効率的に実装する方法をご紹介します。

背景

モダンなWebアプリケーションにおけるダークモードの重要性

現在、主要なWebサービスの90%以上がダークモードに対応しており、これはもはやトレンドではなく標準機能となっています。ダークモードの普及により、以下のような効果が期待されています。

効果詳細重要度
目の疲労軽減長時間の作業における視覚疲労の軽減★★★★★
バッテリー節約OLEDディスプレイでの消費電力削減★★★★
集中力向上暗い環境での視認性向上による作業効率化★★★★
ブランディングモダンで洗練されたUI/UXの提供★★★

ユーザビリティ向上とアクセシビリティ対応の観点

ダークモードの実装は単なる見た目の問題ではありません。WCAG(Web Content Accessibility Guidelines)の観点からも、ユーザーが快適にコンテンツを閲覧できる環境を提供することは重要な責務です。

特に視覚に何らかの困難を抱えるユーザーにとって、適切なコントラスト比を保った色彩設計は必須要件となっています。

課題

Nuxt 3でのダークモード実装における複雑さ

Nuxt 3では、従来のNuxt 2とは異なる仕組みでアプリケーションが動作します。特に以下の点で実装の複雑さが増しています。

課題項目詳細対応難易度
Composition APIVue 3のComposition APIに対応した状態管理★★★
Auto Import自動インポート機能との整合性確保★★
TypeScript対応型安全性を保ったテーマ管理★★★

SSR環境でのテーマ状態管理の困難さ

Server-Side Rendering(SSR)環境では、サーバー側でレンダリングされたHTMLとクライアント側での状態が一致しない場合、ハイドレーションエラーが発生する可能性があります。

これは特にテーマ情報がlocalStorageなどのブラウザAPI依存の場合に顕著に現れます。

フラッシュ(画面のちらつき)問題への対処

初回読み込み時に、一瞬ライトテーマが表示されてからダークテーマに切り替わる現象(FOUC: Flash of Unstyled Content)は、ユーザー体験を大きく損なう問題です。

この問題を解決するには、サーバー側でのテーマ状態の適切な管理と、CSSの優先度設計が重要になります。

解決策

@nuxtjs/color-mode モジュールを活用した実装方法

Nuxtコミュニティが提供する @nuxtjs​/​color-mode モジュールは、これらの課題を効率的に解決してくれる強力なツールです。

このモジュールの主な特徴は以下のとおりです:

  • SSR対応済みのテーマ管理機能
  • システム設定との自動同期
  • TypeScript完全対応
  • Tailwind CSSとの完全な互換性

Tailwind CSSとの連携による効率的なスタイリング

Tailwind CSSのダークモード機能と @nuxtjs​/​color-mode を組み合わせることで、クラスベースでの簡潔なスタイリングが可能になります。

これにより、従来のCSS変数を用いた複雑な実装から解放され、保守性の高いコードが書けるようになります。

システム設定との自動同期機能

prefers-color-scheme メディアクエリを活用することで、ユーザーのシステム設定に応じたテーマの自動適用が実現できます。

これにより、初回訪問時からユーザーの環境に最適化されたテーマでコンテンツを提供することが可能です。

具体例

基本的なダークモード実装手順

まずは必要なパッケージをインストールしましょう。Nuxtプロジェクトにダークモード機能を追加する最初のステップです。

bashyarn add @nuxtjs/color-mode
yarn add -D @nuxtjs/tailwindcss

次に、nuxt.config.ts にモジュールを登録します。この設定により、プロジェクト全体でカラーモード機能が利用できるようになります。

typescript// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxtjs/tailwindcss',
    '@nuxtjs/color-mode'
  ],
  colorMode: {
    preference: 'system', // デフォルトはシステム設定
    fallback: 'light', // システム設定が取得できない場合の初期値
    hid: 'nuxt-color-mode-script',
    globalName: '__NUXT_COLOR_MODE__',
    componentName: 'ColorScheme',
    classPrefix: '',
    classSuffix: '',
    storageKey: 'nuxt-color-mode'
  }
})

Tailwind CSSの設定ファイルでダークモードを有効化します。この設定により、dark: プレフィックスを使用したスタイリングが可能になります。

javascript// tailwind.config.js
module.exports = {
  darkMode: 'class', // クラスベースでのダークモード制御
  content: [
    './components/**/*.{js,vue,ts}',
    './layouts/**/*.vue',
    './pages/**/*.vue',
    './plugins/**/*.{js,ts}',
    './app.vue'
  ],
  theme: {
    extend: {
      colors: {
        // カスタムカラーパレット
        primary: {
          light: '#3B82F6',
          dark: '#60A5FA'
        },
        surface: {
          light: '#FFFFFF',
          dark: '#1F2937'
        },
        text: {
          light: '#1F2937',
          dark: '#F9FAFB'
        }
      }
    }
  }
}

テーマ切替ボタンの作成

テーマ切替機能の核となるコンポーネントを作成します。このコンポーネントでは、ライト・ダーク・システム設定の3つのモードを切り替えることができます。

vue<!-- components/ThemeToggle.vue -->
<template>
  <div class="relative">
    <button 
      @click="toggleTheme"
      class="p-2 rounded-lg transition-colors duration-200 
             bg-gray-200 hover:bg-gray-300 
             dark:bg-gray-700 dark:hover:bg-gray-600"
      :aria-label="`現在のテーマ: ${currentTheme}`"
    >
      <Icon :name="themeIcon" class="w-5 h-5" />
    </button>
  </div>
</template>

続いて、テーマ切替のロジックを実装します。Composition APIを使用して、リアクティブなテーマ管理を行います。

vue<script setup lang="ts">
// テーマの型定義
type ColorMode = 'light' | 'dark' | 'system'

interface ThemeConfig {
  mode: ColorMode
  icon: string
  label: string
}

// テーマ設定の配列
const themes: ThemeConfig[] = [
  { mode: 'light', icon: 'ph:sun', label: 'ライトモード' },
  { mode: 'dark', icon: 'ph:moon', label: 'ダークモード' },
  { mode: 'system', icon: 'ph:monitor', label: 'システム設定' }
]

// カラーモードのcomposable
const colorMode = useColorMode()

// 現在のテーマ設定を取得
const currentTheme = computed(() => {
  return themes.find(theme => theme.mode === colorMode.preference) || themes[0]
})

// テーマアイコンの算出プロパティ
const themeIcon = computed(() => currentTheme.value.icon)
</script>

テーマ切替機能の実装を完成させます。配列を循環させて次のテーマに切り替える仕組みです。

vue<script setup lang="ts">
// テーマ切替機能の実装
const toggleTheme = () => {
  const currentIndex = themes.findIndex(
    theme => theme.mode === colorMode.preference
  )
  const nextIndex = (currentIndex + 1) % themes.length
  colorMode.preference = themes[nextIndex].mode
}

// システム設定の変更を監視
const { system } = useColorMode()

// テーマ変更時の処理
watch(
  () => colorMode.preference,
  (newMode) => {
    // カスタムイベントの発火(アニメーション等で使用)
    document.dispatchEvent(
      new CustomEvent('theme-changed', { 
        detail: { mode: newMode, system: system.value } 
      })
    )
  }
)
</script>

カスタムテーマの追加方法

企業ブランドに合わせたカスタムテーマを追加する場合の実装例をご紹介します。まず、カスタムカラーパレットを定義します。

javascript// tailwind.config.js - カスタムテーマ拡張
module.exports = {
  darkMode: 'class',
  content: [
    // ... 既存の設定
  ],
  theme: {
    extend: {
      colors: {
        // ブランドテーマ
        brand: {
          50: '#EFF6FF',
          100: '#DBEAFE', 
          500: '#3B82F6',
          900: '#1E3A8A',
          950: '#1E2A5A'
        },
        // セマンティックカラー
        success: {
          light: '#10B981',
          dark: '#34D399'
        },
        warning: {
          light: '#F59E0B',
          dark: '#FBBF24'
        },
        danger: {
          light: '#EF4444',
          dark: '#F87171'
        }
      },
      // カスタムテーマ用のCSS変数
      backgroundColor: {
        'theme-primary': 'var(--color-primary)',
        'theme-secondary': 'var(--color-secondary)',
        'theme-surface': 'var(--color-surface)'
      },
      textColor: {
        'theme-primary': 'var(--color-text-primary)',
        'theme-secondary': 'var(--color-text-secondary)'
      }
    }
  }
}

CSS変数を使用したテーマシステムの構築を行います。この方法により、より柔軟なテーマ管理が可能になります。

css/* assets/css/themes.css */
:root {
  /* ライトテーマ */
  --color-primary: #3B82F6;
  --color-secondary: #8B5CF6;
  --color-surface: #FFFFFF;
  --color-text-primary: #1F2937;
  --color-text-secondary: #6B7280;
  
  /* グラデーション */
  --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  --gradient-surface: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}

.dark {
  /* ダークテーマ */
  --color-primary: #60A5FA;
  --color-secondary: #A78BFA;
  --color-surface: #1F2937;
  --color-text-primary: #F9FAFB;
  --color-text-secondary: #D1D5DB;
  
  /* ダーク用グラデーション */
  --gradient-primary: linear-gradient(135deg, #4c1d95 0%, #581c87 100%);
  --gradient-surface: linear-gradient(135deg, #374151 0%, #1f2937 100%);
}

/* 企業ブランド用カスタムテーマ */
.theme-corporate {
  --color-primary: #DC2626;
  --color-secondary: #7C3AED;
  --color-surface: #F8FAFC;
  --color-text-primary: #0F172A;
  --color-text-secondary: #475569;
}

アニメーション効果の実装

テーマ切替時のスムーズなトランジション効果を実装します。まず、基本的なトランジションを設定します。

vue<!-- components/AnimatedLayout.vue -->
<template>
  <div 
    class="theme-transition min-h-screen"
    :class="themeClasses"
  >
    <slot />
  </div>
</template>

続いて、アニメーションのロジックとスタイルを実装します。

vue<script setup lang="ts">
const colorMode = useColorMode()

// テーマクラスの動的生成
const themeClasses = computed(() => ({
  'theme-light': colorMode.value === 'light',
  'theme-dark': colorMode.value === 'dark',
  'theme-system': colorMode.preference === 'system'
}))

// テーマ変更時のアニメーション制御
const isTransitioning = ref(false)

watch(
  () => colorMode.value,
  () => {
    isTransitioning.value = true
    
    // アニメーション終了後にフラグをリセット
    setTimeout(() => {
      isTransitioning.value = false
    }, 300)
  }
)
</script>

CSS でアニメーション効果を定義します。この実装により、テーマ切替時に自然なフェード効果が適用されます。

css<style scoped>
.theme-transition {
  transition: 
    background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
    color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
    border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
    box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

/* フェードイン効果 */
.theme-light {
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}

.theme-dark {
  background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
}

/* ページ遷移時のアニメーション */
.page-enter-active,
.page-leave-active {
  transition: opacity 0.3s ease-in-out;
}

.page-enter-from,
.page-leave-to {
  opacity: 0;
}

/* コンポーネント単位のテーマアニメーション */
.component-theme-transition {
  transition: all 0.2s ease-in-out;
  transform-origin: center;
}

.component-theme-transition:hover {
  transform: scale(1.02);
}
</style>

実際のページでテーマ機能を使用する例を示します。各要素がテーマに応じて適切にスタイリングされることを確認できます。

vue<!-- pages/index.vue -->
<template>
  <AnimatedLayout>
    <div class="container mx-auto px-4 py-8">
      <!-- ヘッダー部分 -->
      <header 
        class="flex justify-between items-center mb-8 p-4 rounded-lg
               bg-white dark:bg-gray-800 
               shadow-sm dark:shadow-gray-700/50
               border border-gray-200 dark:border-gray-700"
      >
        <h1 class="text-2xl font-bold text-theme-primary">
          Nuxt Dark Mode Demo
        </h1>
        <ThemeToggle />
      </header>

      <!-- メインコンテンツ -->
      <main class="space-y-6">
        <section 
          class="p-6 rounded-lg bg-theme-surface shadow-lg
                 border border-gray-200 dark:border-gray-700"
        >
          <h2 class="text-xl font-semibold mb-4 text-theme-primary">
            テーマ切替デモ
          </h2>
          <p class="text-theme-secondary mb-4">
            右上のボタンをクリックしてテーマを切り替えてみてください。
            スムーズなアニメーションとともにテーマが変更されます。
          </p>
          
          <!-- ボタンサンプル -->
          <div class="flex gap-4">
            <button 
              class="px-4 py-2 rounded-lg transition-all duration-200
                     bg-brand-500 hover:bg-brand-600 
                     text-white font-medium
                     shadow-md hover:shadow-lg"
            >
              Primary Button
            </button>
            <button 
              class="px-4 py-2 rounded-lg transition-all duration-200
                     bg-gray-200 hover:bg-gray-300 
                     dark:bg-gray-700 dark:hover:bg-gray-600
                     text-theme-primary"
            >
              Secondary Button
            </button>
          </div>
        </section>
      </main>
    </div>
  </AnimatedLayout>
</template>

最後に、実装したテーマシステムの動作確認方法をご紹介します。

vue<script setup lang="ts">
// SEO対応とメタデータ設定
useHead({
  title: 'Nuxt Dark Mode Demo',
  meta: [
    {
      name: 'description',
      content: 'Nuxt 3でダークモード・テーマ切替を実装したデモページ'
    },
    {
      name: 'theme-color',
      content: computed(() => 
        colorMode.value === 'dark' ? '#1F2937' : '#FFFFFF'
      )
    }
  ]
})

// パフォーマンス監視
onMounted(() => {
  // テーマ変更イベントのリスナー
  document.addEventListener('theme-changed', (event) => {
    const { mode } = event.detail
    console.log(`テーマが変更されました: ${mode}`)
    
    // Google Analytics等での追跡
    if (process.client && window.gtag) {
      window.gtag('event', 'theme_change', {
        theme_mode: mode
      })
    }
  })
})
</script>

まとめ

今回は、Nuxt 3で効率的にダークモード&テーマ切替機能を実装する方法をご紹介しました。

@nuxtjs​/​color-mode モジュールとTailwind CSSを組み合わせることで、複雑なSSR環境でも安定したテーマ管理が実現できることがお分かりいただけたでしょう。

特に重要なポイントをまとめると以下のとおりです:

実装ポイント効果注意事項
モジュール活用開発効率の大幅向上設定の理解が重要
CSS変数の使用柔軟なテーマカスタマイズパフォーマンスへの配慮
アニメーション実装優れたUX提供過度な演出は避ける
アクセシビリティ配慮幅広いユーザーへの対応WCAG準拠の確認

この実装により、モダンで使いやすいWebアプリケーションの構築が可能になります。ユーザーの満足度向上と開発効率の両立を実現し、競合他社との差別化につなげていただければと思います。

また、実装後は必ずさまざまなデバイスでの動作確認を行い、パフォーマンスの監視も忘れずに実施してください。継続的な改善により、より良いユーザー体験を提供していくことが大切です。

関連リンク