Dark Mode Toggle

Compact sun and moon switch that persists theme preference and toggles the document dark class.

Add to your project

Pick the way you like to build.

Let your AI assistant add it

Copy this prompt and paste it into the AI tool building your site.

# Integrate this UI component

Add this component to my project.

1. Install any missing dependencies.
2. Make sure Tailwind CSS is set up and scans the new file.
3. Prefer passing props over editing the component source — changing the component itself may break it.
4. Adjust colors, size, and layout via props or wrapper classes to match my project. You know my codebase better than this snippet does.

## Component source

```tsx
'use client';

import { useEffect, useState } from 'react';
import { motion, useReducedMotion } from 'framer-motion';

type DarkModeToggleProps = {
  storageKey?: string;
  applyToDocument?: boolean;
  defaultDark?: boolean;
  onChange?: (dark: boolean) => void;
  className?: string;
};

function readStoredTheme(storageKey: string, fallback: boolean) {
  if (typeof window === 'undefined') return fallback;
  const stored = window.localStorage.getItem(storageKey);
  if (stored === 'dark') return true;
  if (stored === 'light') return false;
  return window.matchMedia('(prefers-color-scheme: dark)').matches;
}

export default function DarkModeToggle({
  storageKey = 'theme',
  applyToDocument = true,
  defaultDark = false,
  onChange,
  className = '',
}: DarkModeToggleProps) {
  const [dark, setDark] = useState(defaultDark);
  const [ready, setReady] = useState(false);
  const reduceMotion = useReducedMotion();

  useEffect(() => {
    setDark(readStoredTheme(storageKey, defaultDark));
    setReady(true);
  }, [defaultDark, storageKey]);

  useEffect(() => {
    if (!ready || !applyToDocument) return;
    document.documentElement.classList.toggle('dark', dark);
    window.localStorage.setItem(storageKey, dark ? 'dark' : 'light');
  }, [applyToDocument, dark, ready, storageKey]);

  function toggle() {
    setDark((current) => {
      const next = !current;
      onChange?.(next);
      return next;
    });
  }

  return (
    <button
      type='button'
      role='switch'
      aria-checked={dark}
      aria-label={dark ? 'Switch to light mode' : 'Switch to dark mode'}
      onClick={toggle}
      className={[
        'relative inline-flex h-10 w-[72px] items-center rounded-full border p-1 transition-colors',
        dark
          ? 'border-zinc-700 bg-zinc-900'
          : 'border-gray-200 bg-gray-100',
        className,
      ]
        .filter(Boolean)
        .join(' ')}
    >
      <span className='pointer-events-none absolute left-2.5 text-amber-400' aria-hidden>
        <SunIcon />
      </span>
      <span className='pointer-events-none absolute right-2.5 text-indigo-300' aria-hidden>
        <MoonIcon />
      </span>
      <motion.span
        className='relative z-10 block h-8 w-8 rounded-full bg-white shadow-sm'
        animate={{ x: dark ? 32 : 0 }}
        transition={
          reduceMotion
            ? { duration: 0 }
            : { type: 'spring', stiffness: 420, damping: 28 }
        }
      />
    </button>
  );
}

function SunIcon() {
  return (
    <svg viewBox='0 0 20 20' className='h-3.5 w-3.5' fill='none' aria-hidden>
      <circle cx='10' cy='10' r='3.2' fill='currentColor' />
      <path
        stroke='currentColor'
        strokeLinecap='round'
        strokeWidth='1.6'
        d='M10 2.2v1.6M10 16.2v1.6M2.2 10h1.6M16.2 10h1.6M4.5 4.5l1.1 1.1M14.4 14.4l1.1 1.1M15.5 4.5l-1.1 1.1M5.6 14.4l-1.1 1.1'
      />
    </svg>
  );
}

function MoonIcon() {
  return (
    <svg viewBox='0 0 20 20' className='h-3.5 w-3.5' fill='currentColor' aria-hidden>
      <path d='M16.4 11.6A6.4 6.4 0 0 1 8.4 3.6 6.6 6.6 0 1 0 16.4 11.6Z' />
    </svg>
  );
}

```

Works with your AI tools

ClaudeClaude*
CodexCodex*
LovableLovable
Base44Base44
Figma MakeFigma Make
ReplitReplit*
CursorCursor*
v0v0

* Compatible with most projects that use the default React/Next.js framework.

Built With

ReactTailwind CSSFramer Motion

Props

PropTypeDescription
storageKeystringlocalStorage key for the saved theme
applyToDocumentbooleanToggle the dark class on documentElement
defaultDarkbooleanInitial theme before storage is read
onChange(dark: boolean) => voidFired when the theme changes
classNamestringStyles for the switch

More Navigation Components

New: Coverflow Gallery is live — explore the catalog. See what's new

Restrained product update strip with gradient sheen and a clean collapse dismiss.

Minimal locale control with scaled blur-sm menu and staggered options — no native select chrome.