Skip to content

Theme Switching Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add dark/light theme switching with system preference detection, header toggle button, and full control in cabinet settings.

Architecture: Nuxt UI v3 includes @nuxtjs/color-mode and provides ready-made components (ColorModeButton, ColorModeSelect). We configure colorMode in nuxt.config.ts, add the built-in ColorModeButton to both layouts' headers, and add ColorModeSelect to the cabinet settings page. No custom components needed — Nuxt UI handles everything including Russian locale translations.

Tech Stack: Nuxt UI v3 (ColorModeButton, ColorModeSelect, useColorMode), @nuxtjs/color-mode, Tailwind CSS 4 dark mode


Task 1: Configure colorMode in nuxt.config.ts

Files:

  • Modify: nuxt.config.ts:62-72 (add colorMode section before runtimeConfig)

Step 1: Add colorMode configuration

Add colorMode section to nuxt.config.ts after the image section and before routeRules:

ts
colorMode: {
  preference: 'system',
  fallback: 'light',
},

This tells @nuxtjs/color-mode (bundled with Nuxt UI) to default to system preference, falling back to light if system detection is unavailable.

Step 2: Verify dev server starts

Run: npm run dev Expected: Dev server starts without errors. The <html> element should now get a class="light" or class="dark" based on your OS setting.

Step 3: Commit

bash
git add nuxt.config.ts
git commit -m "feat: configure colorMode with system preference default"

Task 2: Add color palette comments to app.config.ts

Files:

  • Modify: app/app.config.ts

Step 1: Add comments with available palette options

Replace the entire file content with:

ts
export default defineAppConfig({
  ui: {
    colors: {
      // Available palettes: red, orange, amber, yellow, lime, green, emerald,
      // teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose
      primary: 'blue',
      neutral: 'slate',
    },
  },
})

Step 2: Verify no errors

Run: npm run dev Expected: Dev server runs without errors, UI unchanged.

Step 3: Commit

bash
git add app/app.config.ts
git commit -m "docs: add available color palette options to app.config"

Task 3: Add ColorModeButton to default layout header

Files:

  • Modify: app/layouts/default.vue:23-33 (desktop nav) and app/layouts/default.vue:36-51 (mobile nav area)

Step 1: Add ColorModeButton to desktop nav

In the desktop <nav> section (line 23), add <ColorModeButton /> as the last element before the closing </nav>:

html
<!-- Desktop nav -->
<nav class="hidden md:flex items-center gap-4">
  <UButton :label="t('common.addListing')" to="/cabinet/products/new" variant="solid" />
  <template v-if="authStore.isAuthenticated">
    <UButton :label="t('common.favorites')" to="/cabinet/favorites" variant="ghost" icon="i-lucide-heart" />
    <UButton variant="ghost" to="/cabinet" icon="i-lucide-user">
      &#123;&#123; authStore.user?.display_name }}
    </UButton>
    <UButton :label="t('common.logout')" variant="ghost" icon="i-lucide-log-out" @click="logout" />
  </template>
  <UButton v-else :label="t('common.login')" to="/auth/login" variant="ghost" icon="i-lucide-log-in" />
  <ColorModeButton />
</nav>

Step 2: Add ColorModeButton to mobile nav area

In the mobile nav area (the <div class="flex md:hidden items-center gap-2">), add <ColorModeButton size="sm" /> before the hamburger menu button:

html
<!-- Mobile nav -->
<div class="flex md:hidden items-center gap-2">
  <UButton
    to="/cabinet/products/new"
    variant="solid"
    size="sm"
    icon="i-lucide-plus"
    :aria-label="t('common.addListing')"
  />
  <ColorModeButton size="sm" />
  <UButton
    variant="ghost"
    size="sm"
    icon="i-lucide-menu"
    :aria-label="t('common.menu')"
    @click="mobileMenuOpen = true"
  />
</div>

Step 3: Verify visually

Run: npm run dev, open http://localhost:3000 Expected:

  • Sun/moon icon visible in desktop header (right side, after auth buttons)
  • Sun/moon icon visible in mobile header (between "+" and hamburger)
  • Clicking the icon toggles between light and dark mode
  • Page colors change: background, text, borders all respect dark mode
  • Preference persists after page reload

Step 4: Commit

bash
git add app/layouts/default.vue
git commit -m "feat: add theme toggle button to default layout header"

Task 4: Add ColorModeButton to cabinet layout header

Files:

  • Modify: app/layouts/cabinet.vue:8-18 (header area)

Step 1: Add ColorModeButton to header

In the header <div> (the flex container on line 8), add <ColorModeButton /> between the logo and the hamburger button. Wrap the right-side elements in a flex container:

html
<header class="border-b border-gray-200 dark:border-gray-800">
  <div class="container mx-auto px-4 py-3 flex items-center justify-between">
    <NuxtLink to="/">
      <SharedAppLogo />
    </NuxtLink>
    <div class="flex items-center gap-2">
      <ColorModeButton />
      <UButton
        icon="i-heroicons-bars-3"
        variant="ghost"
        class="md:hidden"
        @click="mobileMenuOpen = true"
      />
    </div>
  </div>
</header>

Step 2: Verify visually

Run: npm run dev, navigate to /cabinet Expected: Sun/moon icon visible in cabinet header, toggles theme correctly.

Step 3: Commit

bash
git add app/layouts/cabinet.vue
git commit -m "feat: add theme toggle button to cabinet layout header"

Task 5: Add theme section to cabinet settings page

Files:

  • Modify: app/pages/cabinet/settings.vue
  • Reference: i18n/locales/ru.json (for new i18n keys)

Step 1: Add i18n keys for settings page

Add a settings section to i18n/locales/ru.json:

json
"settings": {
  "appearance": "Оформление",
  "theme": "Тема",
  "themeDescription": "Следует настройкам вашего устройства"
}

Step 2: Add ColorModeSelect to settings page

Replace the content of app/pages/cabinet/settings.vue:

html
<script setup lang="ts">
definePageMeta({ layout: 'cabinet', middleware: 'auth' })

const { t } = useI18n()
</script>

<template>
  <div class="space-y-8">
    <h1 class="text-2xl font-bold">&#123;&#123; t('common.settings') }}</h1>

    <section>
      <h2 class="text-lg font-semibold mb-4">&#123;&#123; t('settings.appearance') }}</h2>

      <div class="flex items-center gap-4">
        <label class="text-sm font-medium">&#123;&#123; t('settings.theme') }}</label>
        <ColorModeSelect />
      </div>
    </section>
  </div>
</template>

Step 3: Verify visually

Run: npm run dev, navigate to /cabinet/settings Expected:

  • "Оформление" section heading visible
  • Dropdown with three options: "Системная", "Светлая", "Тёмная"
  • Selecting an option immediately switches the theme
  • The header toggle button reflects the current mode

Step 4: Commit

bash
git add i18n/locales/ru.json app/pages/cabinet/settings.vue
git commit -m "feat: add theme selection to cabinet settings page"

Task 6: Visual audit of existing pages in dark mode

Files:

  • Potentially modify: any file with hardcoded colors missing dark: variant

Step 1: Check all pages in dark mode

With dev server running, switch to dark mode via the header toggle. Visit each page and check for visual issues:

  1. / — homepage
  2. /catalog — catalog with filters
  3. /product/{id} — product detail (use any valid ID)
  4. /auth/login — login form
  5. /auth/register — registration form
  6. /auth/forgot-password — forgot password
  7. /cabinet — my listings
  8. /cabinet/products/new — new listing form
  9. /cabinet/settings — settings (just added)

Step 2: Fix any issues found

Look for:

  • bg-white without dark:bg-gray-900 (or dark:bg-[var(--ui-bg)])
  • text-gray-* without dark:text-gray-* variant
  • border-gray-* without dark:border-gray-* variant
  • Any hardcoded colors that look wrong in dark mode

Fix each issue in the relevant file. Nuxt UI components (UButton, UInput, UCard, etc.) should look correct automatically.

Step 3: Commit fixes (if any)

bash
git add -A
git commit -m "fix: resolve dark mode visual issues across pages"

Task 7: Update CLAUDE.md progress

Files:

  • Modify: CLAUDE.md (Progress section)

Step 1: Mark theme switching as complete

In the Progress section of CLAUDE.md, change:

markdown
- [ ] Theme switching (dark/light mode)

to:

markdown
- [x] Theme switching (dark/light mode)

Step 2: Commit

bash
git add CLAUDE.md
git commit -m "docs: mark theme switching as complete"

Task 8: Write test for ThemeToggle behavior

Files:

  • Create: app/shared/ui/ThemeToggle.test.ts

Step 1: Write the test

Since we're using Nuxt UI's built-in ColorModeButton, the test verifies it renders and integrates correctly:

ts
import { describe, it, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { ColorModeButton } from '#components'

describe('ColorModeButton', () => {
  it('renders without errors', async () => {
    const wrapper = await mountSuspended(ColorModeButton)
    expect(wrapper.find('button').exists()).toBe(true)
  })

  it('has accessible aria-label', async () => {
    const wrapper = await mountSuspended(ColorModeButton)
    const button = wrapper.find('button')
    const label = button.attributes('aria-label')
    expect(label).toBeTruthy()
  })
})

Step 2: Run the test to verify it passes

Run: npm run test:run -- app/shared/ui/ThemeToggle.test.ts Expected: 2 tests pass.

Step 3: Commit

bash
git add app/shared/ui/ThemeToggle.test.ts
git commit -m "test: add ColorModeButton integration test"

Task 9: Final verification

Step 1: Run linter

Run: npm run lint Expected: No errors.

Step 2: Run all tests

Run: npm run test:run Expected: All tests pass.

Step 3: Run typecheck

Run: npm run typecheck Expected: No type errors.

Step 4: Verify dark mode end-to-end

  1. Open http://localhost:3000
  2. Click theme toggle → dark mode activates
  3. Refresh page → dark mode persists
  4. Navigate to /cabinet/settings → select "Системная" → follows OS
  5. Select "Светлая" → forces light mode
  6. Click header toggle → switches to dark

Step 5: Commit any remaining fixes and push

bash
git push