Build beautiful apps
Start

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-themes

Setup

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>
  )
}

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>
  )
}

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:1

8. 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>
  )
}
// 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: