Dark Mode
FlexiUI provides built-in support for dark mode with automatic theme switching.
Setup
Dark mode works out of the box with the FlexiUIProvider:
import { FlexiUIProvider } from '@flexi-ui/react'
function App() {
return (
<FlexiUIProvider>
{/* Your app automatically supports dark mode */}
</FlexiUIProvider>
)
}Using next-themes
For Next.js applications, integrate with next-themes:
Installation
pnpm add next-themesSetup
Wrap your app with ThemeProvider:
'use client'
import { ThemeProvider } from 'next-themes'
import { FlexiUIProvider } from '@flexi-ui/react'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system">
<FlexiUIProvider>
{children}
</FlexiUIProvider>
</ThemeProvider>
)
}Theme Switcher
Create a theme switcher component:
'use client'
import { useTheme } from 'next-themes'
import { Button } from '@flexi-ui/button'
import { Moon, Sun } from 'lucide-react'
export function ThemeSwitcher() {
const { theme, setTheme } = useTheme()
return (
<Button
isIconOnly
variant="light"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
{theme === 'dark' ? <Sun /> : <Moon />}
</Button>
)
}Dark Mode Colors
Customize colors for dark mode:
const theme = {
colors: {
background: {
light: '#ffffff',
dark: '#000000'
},
foreground: {
light: '#000000',
dark: '#ffffff'
},
content1: {
light: '#ffffff',
dark: '#18181b'
}
}
}Using Tailwind Dark Mode
Apply dark mode styles with Tailwind:
<div className="bg-white dark:bg-black text-black dark:text-white">
Content that adapts to theme
</div>System Preference
Respect user's system preference:
<ThemeProvider attribute="class" defaultTheme="system">
<FlexiUIProvider>
{children}
</FlexiUIProvider>
</ThemeProvider>Force Dark Mode
Force dark mode for specific components:
<div className="dark">
<Button>Always dark</Button>
</div>Storage
Theme preference is automatically saved to localStorage:
<ThemeProvider
attribute="class"
defaultTheme="system"
storageKey="flexi-ui-theme"
>
{children}
</ThemeProvider>SSR Considerations
Prevent flash of wrong theme on initial load:
import { ThemeProvider } from 'next-themes'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<FlexiUIProvider>
{children}
</FlexiUIProvider>
</ThemeProvider>
)
}Dropdown Theme Selector
Create a dropdown for multiple themes:
'use client'
import { useTheme } from 'next-themes'
import { Select, SelectItem } from '@flexi-ui/select'
export function ThemeSelector() {
const { theme, setTheme } = useTheme()
return (
<Select
label="Theme"
selectedKeys={[theme || 'system']}
onSelectionChange={(keys) => setTheme(Array.from(keys)[0] as string)}
>
<SelectItem key="light">Light</SelectItem>
<SelectItem key="dark">Dark</SelectItem>
<SelectItem key="system">System</SelectItem>
</Select>
)
}Advanced Theme Toggle
Animated Theme Toggle
Create a smooth toggle with animations:
'use client'
import { useTheme } from 'next-themes'
import { Button } from '@flexi-ui/react'
import { Moon, Sun } from 'lucide-react'
import { useEffect, useState } from 'react'
export function AnimatedThemeToggle() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return <div className="w-10 h-10" /> // Placeholder
}
return (
<Button
isIconOnly
variant="light"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="relative overflow-hidden"
>
<div
className={`
absolute inset-0 flex items-center justify-center transition-transform duration-300
${theme === 'dark' ? 'translate-y-0' : '-translate-y-full'}
`}
>
<Moon className="w-5 h-5" />
</div>
<div
className={`
absolute inset-0 flex items-center justify-center transition-transform duration-300
${theme === 'dark' ? 'translate-y-full' : 'translate-y-0'}
`}
>
<Sun className="w-5 h-5" />
</div>
</Button>
)
}Segmented Theme Control
Create a segmented control for theme selection:
'use client'
import { useTheme } from 'next-themes'
import { Button } from '@flexi-ui/react'
import { Monitor, Moon, Sun } from 'lucide-react'
export function SegmentedThemeControl() {
const { theme, setTheme } = useTheme()
const themes = [
{ value: 'light', icon: Sun, label: 'Light' },
{ value: 'dark', icon: Moon, label: 'Dark' },
{ value: 'system', icon: Monitor, label: 'System' },
]
return (
<div className="inline-flex rounded-lg border border-content3 p-1">
{themes.map(({ value, icon: Icon, label }) => (
<Button
key={value}
size="sm"
variant={theme === value ? 'solid' : 'light'}
color={theme === value ? 'primary' : 'default'}
startContent={<Icon className="w-4 h-4" />}
onClick={() => setTheme(value)}
className="min-w-24"
>
{label}
</Button>
))}
</div>
)
}Dropdown Menu Theme Selector
Integrate theme switching into a dropdown menu:
'use client'
import { useTheme } from 'next-themes'
import {
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem,
Button,
} from '@flexi-ui/react'
import { Palette, Check } from 'lucide-react'
export function ThemeDropdown() {
const { theme, setTheme } = useTheme()
return (
<Dropdown>
<DropdownTrigger>
<Button
variant="light"
startContent={<Palette className="w-4 h-4" />}
>
Theme
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="Theme selection"
selectedKeys={[theme || 'system']}
onAction={(key) => setTheme(key as string)}
>
<DropdownItem key="light" endContent={theme === 'light' && <Check />}>
Light
</DropdownItem>
<DropdownItem key="dark" endContent={theme === 'dark' && <Check />}>
Dark
</DropdownItem>
<DropdownItem key="system" endContent={theme === 'system' && <Check />}>
System
</DropdownItem>
</DropdownMenu>
</Dropdown>
)
}Framework-Specific Integration
Next.js App Router
Complete setup for Next.js 13+ with App Router:
// app/providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'
import { FlexiUIProvider } from '@flexi-ui/react'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
storageKey="flexi-ui-theme"
>
<FlexiUIProvider>{children}</FlexiUIProvider>
</ThemeProvider>
)
}// app/layout.tsx
import { Providers } from './providers'
import './globals.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}/* app/globals.css */
@import 'tailwindcss';
:root {
--background: 255 255 255;
--foreground: 0 0 0;
}
.dark {
--background: 0 0 0;
--foreground: 255 255 255;
}Next.js Pages Router
Setup for Next.js Pages Router:
// pages/_app.tsx
import { ThemeProvider } from 'next-themes'
import { FlexiUIProvider } from '@flexi-ui/react'
import type { AppProps } from 'next/app'
import '@/styles/globals.css'
export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<FlexiUIProvider>
<Component {...pageProps} />
</FlexiUIProvider>
</ThemeProvider>
)
}// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html suppressHydrationWarning>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}Vite + React
Setup for Vite projects:
// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ThemeProvider } from './components/ThemeProvider'
import { FlexiUIProvider } from '@flexi-ui/react'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ThemeProvider>
<FlexiUIProvider>
<App />
</FlexiUIProvider>
</ThemeProvider>
</React.StrictMode>
)// src/components/ThemeProvider.tsx
import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'light' | 'dark' | 'system'
interface ThemeContextType {
theme: Theme
setTheme: (theme: Theme) => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(() => {
const stored = localStorage.getItem('theme') as Theme
return stored || 'system'
})
useEffect(() => {
const root = document.documentElement
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light'
root.classList.remove('light', 'dark')
root.classList.add(systemTheme)
} else {
root.classList.remove('light', 'dark')
root.classList.add(theme)
}
localStorage.setItem('theme', theme)
}, [theme])
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}Remix
Setup for Remix applications:
// app/root.tsx
import { ThemeProvider } from './components/ThemeProvider'
import { FlexiUIProvider } from '@flexi-ui/react'
import type { LinksFunction } from '@remix-run/node'
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react'
import styles from './styles/global.css'
export const links: LinksFunction = () => [
{ rel: 'stylesheet', href: styles },
]
export default function App() {
return (
<html lang="en" suppressHydrationWarning>
<head>
<Meta />
<Links />
</head>
<body>
<ThemeProvider>
<FlexiUIProvider>
<Outlet />
</FlexiUIProvider>
</ThemeProvider>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
)
}Custom Dark Mode Implementation
Without next-themes
Implement dark mode without external libraries:
// hooks/useDarkMode.ts
import { useState, useEffect } from 'react'
type Theme = 'light' | 'dark'
export function useDarkMode() {
const [theme, setTheme] = useState<Theme>(() => {
if (typeof window === 'undefined') return 'light'
const stored = localStorage.getItem('theme') as Theme
if (stored) return stored
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
})
useEffect(() => {
const root = document.documentElement
if (theme === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
localStorage.setItem('theme', theme)
}, [theme])
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = (e: MediaQueryListEvent) => {
const storedTheme = localStorage.getItem('theme')
if (!storedTheme) {
setTheme(e.matches ? 'dark' : 'light')
}
}
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}, [])
const toggleTheme = () => {
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'))
}
return { theme, setTheme, toggleTheme }
}Media Query Detection
Detect and respond to system theme changes:
'use client'
import { useEffect, useState } from 'react'
export function useSystemTheme() {
const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>('light')
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
setSystemTheme(mediaQuery.matches ? 'dark' : 'light')
const handleChange = (e: MediaQueryListEvent) => {
setSystemTheme(e.matches ? 'dark' : 'light')
}
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}, [])
return systemTheme
}
// Usage
export function SystemThemeDisplay() {
const systemTheme = useSystemTheme()
return (
<div>
<p>System prefers: {systemTheme}</p>
</div>
)
}Smooth Transitions
CSS Transitions
Add smooth transitions between themes:
/* globals.css */
@import 'tailwindcss';
* {
transition-property: background-color, border-color, color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 200ms;
}
/* Disable transitions for theme changes */
html.no-transition * {
transition: none !important;
}// hooks/useThemeTransition.ts
import { useTheme } from 'next-themes'
export function useThemeTransition() {
const { setTheme } = useTheme()
const setThemeWithTransition = (newTheme: string) => {
// Disable transitions temporarily
document.documentElement.classList.add('no-transition')
setTheme(newTheme)
// Re-enable transitions after theme change
setTimeout(() => {
document.documentElement.classList.remove('no-transition')
}, 0)
}
return { setTheme: setThemeWithTransition }
}View Transitions API
Use the View Transitions API for smooth theme changes:
'use client'
import { useTheme } from 'next-themes'
export function useThemeWithTransition() {
const { theme, setTheme } = useTheme()
const setThemeWithTransition = (newTheme: string) => {
// Check if View Transitions API is supported
if (!document.startViewTransition) {
setTheme(newTheme)
return
}
document.startViewTransition(() => {
setTheme(newTheme)
})
}
return { theme, setTheme: setThemeWithTransition }
}/* Add to globals.css */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
}Accessibility
ARIA Labels and Roles
Make theme toggles accessible:
'use client'
import { useTheme } from 'next-themes'
import { Button } from '@flexi-ui/react'
import { Moon, Sun } from 'lucide-react'
export function AccessibleThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button
isIconOnly
variant="light"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
aria-pressed={theme === 'dark'}
role="switch"
>
{theme === 'dark' ? (
<Sun aria-hidden="true" />
) : (
<Moon aria-hidden="true" />
)}
</Button>
)
}Keyboard Navigation
Ensure theme controls are keyboard accessible:
'use client'
import { useTheme } from 'next-themes'
import { Button } from '@flexi-ui/react'
import { Monitor, Moon, Sun } from 'lucide-react'
import { useRef } from 'react'
export function KeyboardAccessibleThemeControl() {
const { theme, setTheme } = useTheme()
const groupRef = useRef<HTMLDivElement>(null)
const themes = [
{ value: 'light', icon: Sun, label: 'Light' },
{ value: 'dark', icon: Moon, label: 'Dark' },
{ value: 'system', icon: Monitor, label: 'System' },
]
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
if (e.key === 'ArrowLeft' && index > 0) {
e.preventDefault()
const prevButton = groupRef.current?.children[index - 1] as HTMLButtonElement
prevButton?.focus()
} else if (e.key === 'ArrowRight' && index < themes.length - 1) {
e.preventDefault()
const nextButton = groupRef.current?.children[index + 1] as HTMLButtonElement
nextButton?.focus()
}
}
return (
<div
ref={groupRef}
role="radiogroup"
aria-label="Theme selection"
className="inline-flex gap-1 rounded-lg border border-content3 p-1"
>
{themes.map(({ value, icon: Icon, label }, index) => (
<Button
key={value}
size="sm"
variant={theme === value ? 'solid' : 'light'}
color={theme === value ? 'primary' : 'default'}
startContent={<Icon className="w-4 h-4" />}
onClick={() => setTheme(value)}
onKeyDown={(e) => handleKeyDown(e, index)}
role="radio"
aria-checked={theme === value}
tabIndex={theme === value ? 0 : -1}
>
{label}
</Button>
))}
</div>
)
}Testing Dark Mode
Component Testing
Test components in both light and dark modes:
// __tests__/ThemeToggle.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { ThemeProvider } from 'next-themes'
import { FlexiUIProvider } from '@flexi-ui/react'
import { ThemeSwitcher } from '@/components/ThemeSwitcher'
describe('ThemeSwitcher', () => {
it('toggles between light and dark themes', () => {
render(
<ThemeProvider attribute="class">
<FlexiUIProvider>
<ThemeSwitcher />
</FlexiUIProvider>
</ThemeProvider>
)
const button = screen.getByRole('button')
// Initial state (light)
expect(document.documentElement.classList.contains('dark')).toBe(false)
// Toggle to dark
fireEvent.click(button)
expect(document.documentElement.classList.contains('dark')).toBe(true)
// Toggle back to light
fireEvent.click(button)
expect(document.documentElement.classList.contains('dark')).toBe(false)
})
})Visual Testing
Test visual appearance in both themes:
// __tests__/visual/dark-mode.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Dark Mode Visual Tests', () => {
test('renders correctly in light mode', async ({ page }) => {
await page.goto('/')
await page.emulateMedia({ colorScheme: 'light' })
await expect(page).toHaveScreenshot('light-mode.png')
})
test('renders correctly in dark mode', async ({ page }) => {
await page.goto('/')
await page.emulateMedia({ colorScheme: 'dark' })
await expect(page).toHaveScreenshot('dark-mode.png')
})
test('toggles theme smoothly', async ({ page }) => {
await page.goto('/')
const toggle = page.locator('[aria-label*="theme"]')
await toggle.click()
await page.waitForTimeout(300) // Wait for transition
await expect(page).toHaveScreenshot('after-toggle.png')
})
})Performance Optimization
Lazy Loading Theme Provider
Optimize initial load by lazy loading theme provider:
// components/LazyThemeProvider.tsx
'use client'
import dynamic from 'next/dynamic'
import { ReactNode } from 'react'
const ThemeProvider = dynamic(
() => import('next-themes').then((mod) => mod.ThemeProvider),
{ ssr: false }
)
export function LazyThemeProvider({ children }: { children: ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system">
{children}
</ThemeProvider>
)
}Memoized Theme Context
Optimize re-renders with memoization:
'use client'
import { createContext, useContext, useMemo, ReactNode } from 'react'
import { useTheme as useNextTheme } from 'next-themes'
interface ThemeContextValue {
theme: string | undefined
setTheme: (theme: string) => void
isDark: boolean
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)
export function ThemeProvider({ children }: { children: ReactNode }) {
const { theme, setTheme } = useNextTheme()
const value = useMemo(
() => ({
theme,
setTheme,
isDark: theme === 'dark',
}),
[theme, setTheme]
)
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}Best Practices
1. Prevent Flash of Unstyled Content (FOUC)
Always use suppressHydrationWarning:
// app/layout.tsx
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>{children}</body>
</html>
)
}2. Use System Preference as Default
Respect user's system preferences:
<ThemeProvider
attribute="class"
defaultTheme="system" // Use system preference
enableSystem
>
{children}
</ThemeProvider>3. Disable Transitions During Theme Change
Prevent jarring transitions:
<ThemeProvider
attribute="class"
defaultTheme="system"
disableTransitionOnChange // Disable transitions
>
{children}
</ThemeProvider>4. Store User Preference
Remember user's choice:
<ThemeProvider
attribute="class"
defaultTheme="system"
storageKey="app-theme" // Custom storage key
>
{children}
</ThemeProvider>5. Test Both Themes
Always test components in both themes:
describe('MyComponent', () => {
it('renders correctly in light mode', () => {
render(<MyComponent />, { wrapper: LightThemeWrapper })
// assertions
})
it('renders correctly in dark mode', () => {
render(<MyComponent />, { wrapper: DarkThemeWrapper })
// assertions
})
})6. Use Semantic Colors
Prefer semantic colors over specific colors:
// ✅ Good - semantic colors
<div className="bg-background text-foreground">
Content
</div>
// ❌ Avoid - specific colors
<div className="bg-white dark:bg-black text-black dark:text-white">
Content
</div>7. Consider Color Contrast
Ensure sufficient contrast in both themes:
// Verify contrast ratios
const lightContrast = getContrast('#000000', '#ffffff') // 21:1
const darkContrast = getContrast('#ffffff', '#000000') // 21:18. Handle Images and Media
Use appropriate images for each theme:
'use client'
import { useTheme } from 'next-themes'
import Image from 'next/image'
export function ThemedImage() {
const { theme } = useTheme()
return (
<Image
src={theme === 'dark' ? '/logo-dark.png' : '/logo-light.png'}
alt="Logo"
width={200}
height={50}
/>
)
}Troubleshooting
Hydration Mismatch
Problem: Warning about hydration mismatch.
Solution:
// ✅ Add suppressHydrationWarning
<html lang="en" suppressHydrationWarning>
<body>{children}</body>
</html>
// ✅ Ensure ThemeProvider is client component
'use client'
import { ThemeProvider } from 'next-themes'Flash of Wrong Theme
Problem: Brief flash of wrong theme on load.
Solutions:
// ✅ Disable transitions during change
<ThemeProvider disableTransitionOnChange>
{children}
</ThemeProvider>
// ✅ Add blocking script (advanced)
// app/layout.tsx
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
try {
const theme = localStorage.getItem('theme') || 'system'
if (theme === 'dark' ||
(theme === 'system' &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
}
} catch {}
`,
}}
/>
</head>
<body>{children}</body>
</html>
)
}Theme Not Persisting
Problem: Theme resets on page reload.
Solutions:
// ✅ Check localStorage access
if (typeof window !== 'undefined') {
localStorage.setItem('theme', theme)
}
// ✅ Use correct storage key
<ThemeProvider storageKey="my-app-theme">
{children}
</ThemeProvider>
// ✅ Verify localStorage permissions
try {
localStorage.setItem('test', 'test')
localStorage.removeItem('test')
} catch (e) {
console.error('localStorage not available')
}Icons Not Showing
Problem: Theme toggle icons don't appear.
Solutions:
// ✅ Check for client-side rendering
'use client'
import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
export function ThemeToggle() {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return <div className="w-10 h-10" /> // Skeleton
}
return (
<Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? <Sun /> : <Moon />}
</Button>
)
}Tailwind Dark Classes Not Working
Problem: dark: classes don't apply.
Solutions:
// ✅ Configure Tailwind dark mode
// tailwind.config.ts
export default {
darkMode: 'class', // or 'media'
// ...
}
// ✅ Ensure class is applied to html
<ThemeProvider attribute="class">
{children}
</ThemeProvider>
// ✅ Check class is added
useEffect(() => {
console.log(document.documentElement.classList.contains('dark'))
}, [theme])Complete Examples
Minimal Setup
// app/providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'
import { FlexiUIProvider } from '@flexi-ui/react'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system">
<FlexiUIProvider>{children}</FlexiUIProvider>
</ThemeProvider>
)
}
// app/layout.tsx
import { Providers } from './providers'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
// components/ThemeToggle.tsx
'use client'
import { useTheme } from 'next-themes'
import { Button } from '@flexi-ui/react'
import { Moon, Sun } from 'lucide-react'
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button
isIconOnly
variant="light"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
{theme === 'dark' ? <Sun /> : <Moon />}
</Button>
)
}Full-Featured Setup
// app/providers.tsx
'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { FlexiUIProvider } from '@flexi-ui/react'
import { ReactNode } from 'react'
export function Providers({ children }: { children: ReactNode }) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
storageKey="flexi-ui-theme"
themes={['light', 'dark', 'system']}
>
<FlexiUIProvider>{children}</FlexiUIProvider>
</NextThemesProvider>
)
}
// components/ThemeControls.tsx
'use client'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
import {
Button,
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem,
} from '@flexi-ui/react'
import { Monitor, Moon, Sun, Check } from 'lucide-react'
export function ThemeControls() {
const { theme, setTheme, systemTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return null
const currentTheme = theme === 'system' ? systemTheme : theme
const themes = [
{ value: 'light', icon: Sun, label: 'Light Mode' },
{ value: 'dark', icon: Moon, label: 'Dark Mode' },
{ value: 'system', icon: Monitor, label: 'System' },
]
return (
<Dropdown>
<DropdownTrigger>
<Button
variant="light"
isIconOnly
aria-label="Toggle theme"
>
{currentTheme === 'dark' ? <Moon /> : <Sun />}
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="Theme selection"
selectedKeys={[theme || 'system']}
onAction={(key) => setTheme(key as string)}
>
{themes.map(({ value, icon: Icon, label }) => (
<DropdownItem
key={value}
startContent={<Icon className="w-4 h-4" />}
endContent={theme === value && <Check className="w-4 h-4" />}
>
{label}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
)
}Next Steps
Learn more about theming:
- Customize Theme - Customize theme colors and styles
- Create Theme - Build custom themes from scratch
- Colors - Master color systems
- Override Styles - Advanced styling techniques
On this page
- Setup
- Using next-themes
- Installation
- Setup
- Theme Switcher
- Dark Mode Colors
- Using Tailwind Dark Mode
- System Preference
- Force Dark Mode
- Storage
- SSR Considerations
- Dropdown Theme Selector
- Advanced Theme Toggle
- Animated Theme Toggle
- Segmented Theme Control
- Dropdown Menu Theme Selector
- Framework-Specific Integration
- Next.js App Router
- Next.js Pages Router
- Vite + React
- Remix
- Custom Dark Mode Implementation
- Without next-themes
- Media Query Detection
- Smooth Transitions
- CSS Transitions
- View Transitions API
- Accessibility
- ARIA Labels and Roles
- Keyboard Navigation
- Testing Dark Mode
- Component Testing
- Visual Testing
- Performance Optimization
- Lazy Loading Theme Provider
- Memoized Theme Context
- Best Practices
- 1. Prevent Flash of Unstyled Content (FOUC)
- 2. Use System Preference as Default
- 3. Disable Transitions During Theme Change
- 4. Store User Preference
- 5. Test Both Themes
- 6. Use Semantic Colors
- 7. Consider Color Contrast
- 8. Handle Images and Media
- Troubleshooting
- Hydration Mismatch
- Flash of Wrong Theme
- Theme Not Persisting
- Icons Not Showing
- Tailwind Dark Classes Not Working
- Complete Examples
- Minimal Setup
- Full-Featured Setup
- Next Steps