Theme Integration Guide
This guide explains how to integrate the nuxt-users module's theme system with your application's existing theme management.
✨ TL;DR for Nuxt UI Users: If you're using Nuxt UI, it just works! No configuration needed. Jump to the Nuxt UI Integration section for examples.
Overview
The nuxt-users module includes a flexible theme system that:
- Automatically adapts to system color scheme preferences
- Detects and respects consumer app theme changes
- Provides composables for programmatic control
- Works seamlessly with existing theme systems
- Can be disabled if you want complete control
How Theme Detection Works
The module uses a hybrid detection system with multiple layers:
1. System Preference Detection
Listens to OS-level dark mode changes via window.matchMedia('(prefers-color-scheme: dark)'):
// Automatically applied when theme is set to 'system' (default)
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
// Module updates automatically
})2. DOM Class Observation
Uses MutationObserver to detect when your app adds/removes the dark class:
// Your app code
document.documentElement.classList.toggle('dark')
// Module detects this change and syncs its state3. Custom Event Listening
Listens for events dispatched by your app:
// Your app dispatches
window.dispatchEvent(new CustomEvent('theme-change', {
detail: { theme: 'dark' }
}))
// Module receives and applies the theme4. Shared State Composable
Provides a composable that both your app and the module can use:
const { theme, setTheme } = useTheme()
setTheme('dark') // Both app and module stay in syncNuxt UI Integration
The nuxt-users module works seamlessly with Nuxt UI and @nuxtjs/color-mode out of the box.
How It Works
Nuxt UI uses @nuxtjs/color-mode which adds/removes the dark class on document.documentElement. The nuxt-users module detects this automatically via MutationObserver and all components adapt instantly.
Basic Usage (Recommended)
Simply use both modules together - no configuration needed:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui', 'nuxt-users']
// That's it! Both work together automatically
})<!-- app.vue -->
<script setup>
const colorMode = useColorMode() // Nuxt UI's composable
const isDark = computed({
get() {
return colorMode.value === 'dark'
},
set(isDark) {
colorMode.preference = isDark ? 'dark' : 'light'
}
})
</script>
<template>
<div>
<header>
<!-- Nuxt UI's color mode button -->
<ClientOnly v-if="!colorMode?.forced">
<UButton
:icon="isDark ? 'i-lucide-moon' : 'i-lucide-sun'"
color="neutral"
variant="ghost"
@click="isDark = !isDark"
/>
</ClientOnly>
</header>
<main>
<!-- Mix Nuxt UI and nuxt-users components -->
<UCard>
<NUsersLoginForm />
<!-- Both follow the same theme automatically! -->
</UCard>
</main>
</div>
</template>Bidirectional Sync (Optional)
If you want both composables in perfect sync:
<script setup>
const colorMode = useColorMode()
const { theme: nuxtUsersTheme, setTheme } = useTheme()
// Sync nuxt-users with @nuxtjs/color-mode
watch(() => colorMode.preference, (newMode) => {
if (newMode === 'dark' || newMode === 'light') {
setTheme(newMode)
}
if (newMode === 'system') {
setTheme('system')
}
}, { immediate: true })
// Optionally sync back (if nuxt-users controls theme)
watch(nuxtUsersTheme, (newTheme) => {
if (newTheme !== 'system') {
colorMode.preference = newTheme
}
})
</script>
<template>
<div>
<!-- Both systems perfectly synchronized -->
<UCard>
<NUsersLoginForm />
</UCard>
</div>
</template>Testing the Integration
Verify both systems are in sync:
<script setup>
const colorMode = useColorMode()
const { resolvedTheme } = useTheme()
watchEffect(() => {
console.log('Nuxt UI:', colorMode.value)
console.log('nuxt-users:', resolvedTheme.value)
// These will always match!
})
</script>
<template>
<UCard>
<p>Nuxt UI theme: {{ colorMode.value }}</p>
<p>nuxt-users theme: {{ resolvedTheme }}</p>
</UCard>
</template>Using Nuxt UI's Color Mode Components
All of Nuxt UI's theme components work perfectly with nuxt-users:
<template>
<div>
<!-- Use any Nuxt UI color mode component -->
<ColorModeButton />
<!-- or -->
<ColorModeSwitch />
<!-- or -->
<ColorModeSelect />
<!-- nuxt-users components adapt automatically -->
<NUsersLoginForm />
<NUsersUserMenu />
</div>
</template>Why It Works
Both systems follow the same conventions:
- ✅ Both use the
darkclass on<html>element - ✅ Both use CSS custom properties for theming
- ✅ No JavaScript conflicts
- ✅ Single source of truth (
@nuxtjs/color-mode)
Result: Perfect compatibility with zero configuration! 🎉
Integration Patterns
Pattern 1: Let the Module Handle Everything
The simplest approach - no integration needed:
<!-- App.vue -->
<template>
<div>
<NUsersLoginForm />
<!-- Components automatically follow system preferences -->
</div>
</template>When to use: New projects without existing theme systems.
Pattern 2: Use the Module's Composable
Share the theme state between your app and the module:
<!-- App.vue -->
<script setup>
const { theme, resolvedTheme, setTheme, toggleTheme } = useTheme()
const themeIcon = computed(() => {
return resolvedTheme.value === 'dark' ? '🌙' : '☀️'
})
</script>
<template>
<div>
<header>
<button @click="toggleTheme" :title="`Current: ${resolvedTheme}`">
{{ themeIcon }}
</button>
</header>
<main>
<NUsersLoginForm />
</main>
</div>
</template>When to use: You want a simple theme toggle and don't have complex theme requirements.
Pattern 3: Your App Controls, Module Follows
Your app manages themes, module components adapt automatically:
<!-- App.vue -->
<script setup>
// Your existing theme system (e.g., @nuxtjs/color-mode)
const colorMode = useColorMode()
// @nuxtjs/color-mode automatically manages the 'dark' class
// Module's MutationObserver detects this and adapts automatically!
</script>
<template>
<div>
<NUsersLoginForm />
<!-- Automatically follows your theme changes -->
</div>
</template>When to use: Existing app with established theme system (like Nuxt UI with @nuxtjs/color-mode).
Pattern 4: Explicit Communication via Events
Your app notifies the module about theme changes:
<!-- App.vue -->
<script setup>
const currentTheme = ref('light')
const changeTheme = (newTheme: 'light' | 'dark') => {
currentTheme.value = newTheme
// Update DOM
document.documentElement.classList.toggle('dark', newTheme === 'dark')
// Notify module explicitly
window.dispatchEvent(new CustomEvent('theme-change', {
detail: { theme: newTheme }
}))
}
// Or listen to module's events
window.addEventListener('nuxt-users:theme-change', (event) => {
console.log('Module changed theme to:', event.detail.resolved)
// Sync your app state if needed
currentTheme.value = event.detail.resolved
})
</script>
<template>
<div>
<button @click="changeTheme('dark')">Dark</button>
<button @click="changeTheme('light')">Light</button>
<NUsersLoginForm />
</div>
</template>When to use: You want explicit control and clear event-driven communication.
Pattern 5: Disable Module's Theme Plugin
Complete control - module doesn't manage themes at all:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-users'],
nuxtUsers: {
theme: {
enabled: false
}
}
})<!-- App.vue -->
<script setup>
// Your complete theme management
const isDark = useDark()
watchEffect(() => {
document.documentElement.classList.toggle('dark', isDark.value)
})
</script>
<template>
<div>
<NUsersLoginForm />
<!-- Components use CSS only, no JS theme management -->
</div>
</template>When to use: You have a complex theme system and want zero interference.
Advanced Use Cases
Multiple Theme Variants
Combine dark/light mode with color variants when using Nuxt UI:
<script setup>
const themeVariant = ref<'blue' | 'green' | 'purple'>('blue')
const { resolvedTheme } = useTheme()
const themeClasses = computed(() => [
resolvedTheme.value === 'dark' ? 'dark' : '',
`theme-${themeVariant.value}`
])
</script>
<template>
<div :class="themeClasses">
<NUsersLoginForm />
</div>
</template>
<style>
/* Override module colors per variant */
.theme-blue {
--nu-color-primary: #3b82f6;
--nu-color-primary-dark: #2563eb;
}
.theme-green {
--nu-color-primary: #10b981;
--nu-color-primary-dark: #059669;
}
.theme-purple {
--nu-color-primary: #8b5cf6;
--nu-color-primary-dark: #7c3aed;
}
</style>Per-Component Theme Override
<template>
<!-- Main app follows system theme -->
<div>
<header>
<NUsersUserMenu />
</header>
<!-- Force light theme for this section only -->
<aside class="light sidebar">
<NUsersUserList />
</aside>
<main>
<NUsersLoginForm />
</main>
</div>
</template>SSR Considerations
The theme plugin only runs on the client side. For SSR, you can:
<script setup>
const { resolvedTheme } = useTheme()
// On client, will be reactive
// On server, will be 'light' (default)
</script>
<template>
<div :class="{ dark: resolvedTheme === 'dark' }">
<NUsersLoginForm />
</div>
</template>Or use the ClientOnly component:
<template>
<div>
<ClientOnly>
<template #default>
<ThemeToggle />
</template>
<template #fallback>
<!-- Shown during SSR -->
<div>Loading theme...</div>
</template>
</ClientOnly>
<NUsersLoginForm />
</div>
</template>API Reference
useTheme() Composable
interface UseThemeReturn {
// Current theme mode ('light' | 'dark' | 'system')
theme: Ref<ThemeMode>
// Resolved theme ('light' | 'dark')
// If theme is 'system', this shows the actual resolved value
resolvedTheme: Ref<'light' | 'dark'>
// Set theme programmatically
setTheme: (mode: ThemeMode) => void
// Toggle between light and dark
toggleTheme: () => void
}Module Configuration
// nuxt.config.ts
export default defineNuxtConfig({
nuxtUsers: {
theme: {
// Enable/disable automatic theme plugin
enabled: boolean // default: true
}
}
})Events
Listening to module theme changes:
window.addEventListener('nuxt-users:theme-change', (event: CustomEvent) => {
console.log(event.detail.mode) // 'light' | 'dark' | 'system'
console.log(event.detail.resolved) // 'light' | 'dark'
})Notifying module of your theme changes:
window.dispatchEvent(new CustomEvent('theme-change', {
detail: { theme: 'dark' }
}))
// Or
window.dispatchEvent(new CustomEvent('color-scheme-change', {
detail: { colorScheme: 'dark' }
}))Best Practices
Start Simple: Use the default behavior first. Only add complexity if needed.
Choose One Pattern: Pick one integration pattern and stick with it to avoid conflicts.
Test Both Themes: Always test your components in both light and dark modes.
Respect User Preference: If using system preference, provide a way to override it.
Avoid Fighting: Don't have multiple systems trying to control the same
darkclass.Use CSS Variables: Override
--nu-*CSS custom properties for theming, not component styles directly.Document Your Choice: Make it clear in your codebase which pattern you're using.
Troubleshooting
Components not responding to theme changes
Issue: Module components stay in light mode even when your app is dark.
Solution: Ensure the dark class is on document.documentElement (the <html> tag), not <body>.
// ✅ Correct
document.documentElement.classList.add('dark')
// ❌ Wrong
document.body.classList.add('dark')Theme flickering on page load
Issue: Components flash light theme before switching to dark.
Solution: Set theme as early as possible, ideally in a blocking script:
<!-- app.vue -->
<script setup>
// Set theme before hydration
onBeforeMount(() => {
const savedTheme = localStorage.getItem('theme')
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark')
}
})
</script>Or use a plugin with higher priority.
Multiple systems conflicting
Issue: Your theme toggle and the module's system are fighting.
Solution: Disable the module's theme plugin:
// nuxt.config.ts
export default defineNuxtConfig({
nuxtUsers: {
theme: { enabled: false }
}
})Module not detecting your theme changes
Issue: You change the theme, but module doesn't adapt.
Solution: Ensure you're either:
- Adding/removing the
darkclass ondocument.documentElement, OR - Dispatching a
theme-changeevent, OR - Using the
useTheme()composable
Examples Repository
Complete working examples are available in the module's playground:
- Basic theme toggle:
playground/pages/theme-basic.vue - Integration with color-mode:
playground/pages/theme-color-mode.vue - Custom theme system:
playground/pages/theme-custom.vue - Disabled plugin:
playground/pages/theme-disabled.vue
Need Help?
If you're having issues with theme integration:
- Check this guide for your use case
- Look at the playground examples
- Open an issue on GitHub with your setup details
