Skip to main content

ThemeSwitch Component

A toggle button component that enables users to switch between light, dark, and system color themes. The component displays sun and moon icons that animate smoothly during transitions.

Import

import { ThemeSwitch } from '@/components/ui/theme-switch';

Usage

Basic Usage

<ThemeSwitch />

The component doesn't require any props as it uses the theme context internally.

Within Layout Components

import { ThemeSwitch } from '@/components/ui/theme-switch';

export function Header() {
return (
<header className="flex items-center justify-between p-4 border-b">
<Logo />
<nav className="flex items-center gap-4">
{/* Other navigation items */}
<ThemeSwitch />
</nav>
</header>
);
}

With Custom Positioning

<div className="fixed bottom-4 right-4 z-50">
<ThemeSwitch />
</div>

Props

The ThemeSwitch component doesn't accept any props as it's designed to be a self-contained component that works with the ThemeProvider context.

TypeScript

The component doesn't have a specific props interface since it doesn't accept any props.

// The component uses the theme context internally
import { useTheme } from '@/contexts/theme-provider';

// Theme type from the context
type Theme = 'dark' | 'light' | 'system';

Customization

Style Overrides

Since ThemeSwitch is based on the Button component, you can extend it by creating a custom wrapper:

import { ThemeSwitch } from '@/components/ui/theme-switch';

export function CustomThemeSwitch() {
return (
<div className="p-2 bg-accent rounded-full">
<ThemeSwitch />
</div>
);
}

Extending the Component

If you need to add functionality or change the behavior:

import { useTheme } from '@/contexts/theme-provider';
import { Button } from '@/components/ui/button';
import { Sun, Moon, Laptop } from 'lucide-react';

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

// Added cycling through all three theme options
const cycleTheme = () => {
if (theme === 'dark') {
setTheme('system');
} else if (theme === 'light') {
setTheme('dark');
} else {
setTheme('light');
}
};

return (
<Button variant="ghost" size="icon" onClick={cycleTheme}>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:scale-0 dark:-rotate-90" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<Laptop className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all data-[theme=system]:rotate-0 data-[theme=system]:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
);
}

Examples

Integration with Navbar

import { ThemeSwitch } from '@/components/ui/theme-switch';

export function Navbar() {
return (
<nav className="flex items-center justify-between px-4 py-3 border-b">
<div className="flex items-center gap-6">
<Logo />
<NavLinks />
</div>
<div className="flex items-center gap-4">
<UserDropdown />
<ThemeSwitch />
</div>
</nav>
);
}

Integration with Settings Form

import { ThemeSwitch } from '@/components/ui/theme-switch';
import { useTheme } from '@/contexts/theme-provider';

export function UserSettingsForm() {
const { theme } = useTheme();

return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-4">
{/* Other form fields */}
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-medium">Theme Preference</h4>
<p className="text-sm text-muted-foreground">
Current theme: {theme.charAt(0).toUpperCase() + theme.slice(1)}
</p>
</div>
<ThemeSwitch />
</div>
<Button type="submit">Save Settings</Button>
</div>
</form>
);
}

Responsive Behavior

The component is designed to work well across all device sizes without special responsiveness considerations:

  • Mobile: Works well as a compact icon button
  • Tablet & Desktop: Same compact appearance, fits well in navigation bars
  • Positioning: Can be placed in headers, settings panels, or as a floating button

Accessibility

The ThemeSwitch component follows these accessibility best practices:

  • Screen reader support: Uses sr-only class for descriptive text that is visible to screen readers
  • Keyboard navigation: Fully operable via keyboard as it's based on the Button component
  • ARIA attributes: Inherits appropriate button ARIA attributes from the Button component
  • Motion preferences: Animations respect prefers-reduced-motion media query via Tailwind's defaults
  • Color contrast: Works with the theme system to ensure proper contrast in both light and dark modes

Implementation Details

The component:

  • Uses the useTheme hook from the ThemeProvider context to access and update the current theme
  • Toggles between 'light' and 'dark' themes (doesn't include 'system' in the toggle cycle)
  • Leverages CSS transitions for smooth icon animations when switching themes
  • Uses absolute positioning to overlay the sun and moon icons in the same space
  • Utilizes the ghost button variant for a minimal appearance that works in various UI contexts
  • Handles theme persistence through localStorage in the ThemeProvider

Common Pitfalls

  • Missing ThemeProvider: The component must be used within a ThemeProvider context or it will throw an error
  • Theme Initialization Flicker: To prevent theme flickering on page load, ensure the ThemeProvider is properly set up with server-side rendering considerations
  • Icon Sizing: The component uses fixed icon sizes, which may need adjustment if used in contexts with different spacing requirements
  • Dark Mode Class: Ensure your Tailwind configuration includes the darkMode: 'class' setting for the component to work correctly

Testing

// Example test for the ThemeSwitch component
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeSwitch } from '@/components/ui/theme-switch';
import { ThemeProvider } from '@/contexts/theme-provider';

describe('ThemeSwitch', () => {
it('renders correctly', () => {
render(
<ThemeProvider>
<ThemeSwitch />
</ThemeProvider>
);
expect(screen.getByRole('button')).toBeInTheDocument();
expect(screen.getByText('Toggle theme')).toBeInTheDocument();
});

it('toggles theme when clicked', async () => {
const user = userEvent.setup();
render(
<ThemeProvider defaultTheme="light">
<ThemeSwitch />
</ThemeProvider>
);

// Initial state check
const documentElement = document.documentElement;
expect(documentElement).toHaveClass('light');

// Click the button
await user.click(screen.getByRole('button'));

// Check if theme changed
expect(documentElement).toHaveClass('dark');
});
});
  • Button: The base component that ThemeSwitch extends
  • Dropdown Menu: Often used alongside ThemeSwitch in navigation components