Clidey UXDocs

Dark Mode

Every component in Clidey Design supports dark mode out of the box. No configuration is required beyond wrapping your app with ThemeProvider.

Automatic system detection#

When defaultTheme is set to "system", Clidey Design reads the user's OS preference using the prefers-color-scheme CSS media query and applies the matching theme automatically:

TSX

<ThemeProvider defaultTheme="system">
  <App />
</ThemeProvider>

The theme updates in real time if the user changes their OS setting while the app is open.

Choosing the initial mode#

Set the initial preference for a new visitor. A saved choice takes precedence over defaultTheme:

TSX

// Initially light
<ThemeProvider defaultTheme="light">
  <App />
</ThemeProvider>

// Initially dark
<ThemeProvider defaultTheme="dark">
  <App />
</ThemeProvider>

Persisting user choice#

The selected theme is saved to localStorage under the key defined by storageKey (default: "@clidey/ux/theme"). On subsequent visits the saved preference is restored automatically.

To use a custom key (useful if you have multiple apps on the same domain):

TSX

<ThemeProvider storageKey="dashboard-theme" defaultTheme="system">
  <App />
</ThemeProvider>

Building a custom theme toggle#

Use useTheme to build your own theme switcher:

TSX

import { useTheme, Button } from '@clidey/ux';
import { IconSun as Sun, IconMoon as Moon, IconMonitor as Monitor } from '@clidey/ux';

function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <div className="flex gap-1 rounded-lg border p-1">
      <Button
        variant={theme === 'light' ? 'default' : 'ghost'}
        size="icon"
        onClick={() => setTheme('light')}
        aria-label="Light mode"
      >
        <Sun className="size-4" />
      </Button>
      <Button
        variant={theme === 'system' ? 'default' : 'ghost'}
        size="icon"
        onClick={() => setTheme('system')}
        aria-label="System mode"
      >
        <Monitor className="size-4" />
      </Button>
      <Button
        variant={theme === 'dark' ? 'default' : 'ghost'}
        size="icon"
        onClick={() => setTheme('dark')}
        aria-label="Dark mode"
      >
        <Moon className="size-4" />
      </Button>
    </div>
  );
}

Dark mode in custom components#

When building your own components, use Tailwind's dark: modifier to respond to the active theme:

TSX

function CustomCard() {
  return (
    <div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4">
      This card adapts to the active theme.
    </div>
  );
}

The dark class is applied to <html> by ThemeProvider, so any dark: utility in your own components works seamlessly.

Next.js Server Components#

When using Next.js with server components, use the "use client" directive for any component that calls useTheme or renders ThemeProvider:

TSX

// app/providers.tsx
'use client';

import { ThemeProvider } from '@clidey/ux';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider defaultTheme="system" storageKey="app-theme">
      {children}
    </ThemeProvider>
  );
}

TSX

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

Note

Add suppressHydrationWarning to <html> to prevent React hydration warnings when the theme class is applied server-side.

Was this page helpful?