Theme Toggle for React

Accessible dark mode toggle with View Transitions API support. Icon, icon-label, or dual-tab variants.

Component

Icon

Icon with label

Dual tabs

Installation

 

After running the command

Complete these steps to finish the setup:

1. Import theme transition CSS in globals.css
/* Add to app/globals.css: */
@import "./theme-toggle.css";
2. Wrap app with ThemeProvider
import { ThemeProvider } from "@/providers/theme-provider";

export default function RootLayout({ children }) {
  return (
    <html suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}
3. Import and use
import { ThemeToggle } from "@/components/theme-toggle";

<ThemeToggle />;

Usage

Import the component

Add the ThemeToggle import to your file.

import { ThemeToggle } from "@/components/theme-toggle";

Use with default props

Use the default icon-only toggle in your navbar or header.

<ThemeToggle />;

Customize with props

Use simple for icon + label, or dual for tab-style selection.

<ThemeToggle simple />;

Guidelines

  • Wrap your app with ThemeProvider from providers/theme-provider before using ThemeToggle. Place it inside <body>, not wrapping it.
  • For Tailwind v4, add @custom-variant dark (&:is(.dark *)); to globals.css for class-based dark mode.
  • ThemeProvider must be inside body. Add suppressHydrationWarning to the html tag.
  • For smooth theme transitions, import theme-toggle.css in globals.css.
  • Hold Ctrl or Cmd while clicking to skip the view transition (useful for testing).

Props

All props are optional unless marked required. Use these to customize every aspect of the component.

PropTypeDefaultDescription
simplebooleanfalseShow icon with Light/Dark label next to it.
dualbooleanfalseRender as dual-tab layout with Light and Dark options.
classNamestringundefinedAdditional CSS classes for the toggle.
useViewTransitionbooleantrueUse View Transitions API for theme switch animation when supported.
children(theme: { theme: 'light' | 'dark'; toggleTheme: () => void }) => React.ReactNodeundefinedRender prop receiving { theme, toggleTheme } for custom UI.

Accessibility

  • All variants use native button elements, so they are focusable and operable with Enter and Space out of the box.
  • Every control carries a descriptive aria-label such as Switch to light mode, decorative Sun and Moon icons are marked aria-hidden, and the loading skeleton announces Loading theme toggle while the missing-provider notice uses role alert.
  • The dual variant exposes role tablist on the container and role tab with aria-selected on each option so assistive tech reports the active theme.
  • Dual-tab buttons have explicit focus-visible ring styles, while the icon button inherits focus styles from the shared Button component.
  • The view-transition clip-path animation and the View Transitions API call are not gated by prefers-reduced-motion, so they should be wrapped with a prefers-reduced-motion media query to respect users who request reduced motion.

Performance

  • The theme switch animates only clip-path on the view-transition pseudo-element and declares will-change clip-path, while the dual tabs transition just background-color, border-color, and color, all of which avoid layout reflow.
  • It uses the native View Transitions API behind a startViewTransition feature check and clears viewTransitionName after the transition finished promise resolves, falling back to a plain setTheme when unsupported.
  • Keydown and keyup listeners on document, the matchMedia change listener, and the missing-provider setTimeout are all cleaned up in their effect teardowns, preventing leaks.
  • No requestAnimationFrame or IntersectionObserver is used; small 50ms setTimeout delays sequence the transition, and animations are not gated by reduced-motion or element visibility.

Examples

Icon

Icon-only toggle. Compact for navbars and headers.

Icon

Icon with label

Dual tabs

import { ThemeToggle } from "@/components/theme-toggle";

export function ThemeToggleBasic() {
  return <ThemeToggle />;
}

Icon with label

Icon with Light/Dark label. Useful for mobile or settings.

Icon

Icon with label

Dual tabs

import { ThemeToggle } from "@/components/theme-toggle";

export function ThemeToggleWithLabel() {
  return <ThemeToggle simple />;
}

Dual tabs

Dual-tab layout with Light and Dark options side by side.

Icon

Icon with label

Dual tabs

import { ThemeToggle } from "@/components/theme-toggle";

export function ThemeToggleDual() {
  return <ThemeToggle dual />;
}

Last updated on Jun 19

Made with ❤️ by Pulkit &

© 2026 Pulkit. All rights reserved

Last updated: