How to Implement Dark/Light Theme Switching with the ModeToggle Component in refine-shadcn

Wrap your application with the ThemeProvider from @ferdiunal/refine-shadcn and place the ModeToggle component in your header to enable instant theme switching with persisted user preferences.

The refine-shadcn library provides a production-ready theme switching solution built on top of next-themes and Tailwind CSS. By combining the ThemeProvider context wrapper with the pre-built ModeToggle UI component, you can implement a complete dark/light mode system that respects OS-level preferences and automatically persists user choices across sessions.

How the ModeToggle Architecture Works

The theme system in refine-shadcn relies on three integrated pieces that coordinate through React context and CSS classes.

ThemeProvider (packages/theme/src/providers/theme-provider.tsx) is a thin wrapper around next-themes that supplies the theme context containing setTheme, theme, and systemTheme values. It handles injecting the dark class onto the <html> element and persists the selected mode to localStorage.

ModeToggle (packages/theme/src/components/modeToggle.tsx) is a shadcn-based dropdown component that renders sun and moon icons with Tailwind transition animations. It consumes the useTheme() hook to call setTheme('light' | 'dark' | 'system') when users select an option from the menu.

Tailwind CSS processes the dark class via the darkMode: "class" configuration, enabling all dark: utility variants to respond instantly when the class changes.

At runtime, the flow works as follows: ThemeProvider initializes the theme from storage or system preference, ModeToggle presents the UI controls, and selecting an option triggers next-themes to update the DOM and storage state.

Step-by-Step Implementation Guide

Install the Package and Import Global Styles

First, add the library to your project and import the required CSS that includes Tailwind directives and shadcn base styles.

pnpm add @ferdiunal/refine-shadcn

In your entry file (e.g., src/main.tsx), import the global stylesheet before rendering your app:

import "@ferdiunal/refine-shadcn/dist/globals.css";

Configure the ThemeProvider Wrapper

Import ThemeProvider from the package and wrap your application root. The provider must be rendered outside any layout components that use the toggle.

// src/App.tsx
import { ThemeProvider } from "@ferdiunal/refine-shadcn";

function App() {
  return (
    <ThemeProvider
      attribute="class"
      defaultTheme="system"
      enableSystem
    >
      {/* Your Refine and Router configuration */}
    </ThemeProvider>
  );
}

The attribute="class" prop instructs next-themes to toggle the dark class on the <html> element, which activates Tailwind's dark mode utilities. Setting defaultTheme="system" with enableSystem ensures the app respects the user's OS preference on first load.

Add the ModeToggle to Your Layout

You can place the toggle anywhere in your UI. The DefaultLayout provided by the library already includes it in the header at lines 198-207 of packages/theme/src/layouts/default.tsx:

// Excerpt from packages/theme/src/layouts/default.tsx
<header className="flex items-center justify-between px-4 py-2">
  {/* Logo and navigation */}
  <ModeToggle />
</header>

For custom layouts, import and render the component directly:

import { ModeToggle } from "@ferdiunal/refine-shadcn";

function CustomHeader() {
  return (
    <header className="flex items-center justify-end p-4 gap-4">
      <h1>Dashboard</h1>
      <ModeToggle />
    </header>
  );
}

When clicked, the toggle displays a dropdown menu with Light, Dark, and System options. Selecting an option immediately updates the UI theme and saves the preference.

Customizing the Theme Toggle Behavior

The ModeToggle component is a lightweight wrapper around the shadcn DropdownMenu. If you need additional options—such as "High Contrast" mode or custom icons—you can copy the source from packages/theme/src/components/modeToggle.tsx into your project, modify the DropdownMenuItem entries, and use your custom version instead.

For programmatic control outside of the toggle button, access the same useTheme hook that ModeToggle uses:

import { useTheme } from "@ferdiunal/refine-shadcn";

function ThemeAwareComponent() {
  const { theme, setTheme } = useTheme();
  
  return (
    <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
      Current: {theme}
    </button>
  );
}

Troubleshooting Common Issues

  • No visual change after selecting a theme: Ensure ThemeProvider is the outermost component in your tree and that you imported globals.css. Without the CSS import, Tailwind cannot process the dark: variants.
  • Icons appear static without animation: Verify that your tailwind.config.js includes darkMode: "class". The refine-shadcn templates pre-configure this, but custom setups may override it.
  • Theme resets on page refresh: This occurs when localStorage is disabled (e.g., strict incognito modes). The library falls back to the system theme when persistence is unavailable.
  • Dropdown menu does not open: Check that your build system resolves the @/ui/dropdown-menu alias correctly. The library's tsconfig.json sets this path, but custom Vite or Webpack configs must match.

Summary

  • Wrap your app with ThemeProvider from @ferdiunal/refine-shadcn using attribute="class" and enableSystem to activate the theme context.
  • Import global styles via @ferdiunal/refine-shadcn/dist/globals.css to enable Tailwind dark mode utilities.
  • Place ModeToggle in your header or settings panel; the component is pre-built and ready to use.
  • Persistence is automatic—next-themes handles localStorage and system preference detection without additional configuration.

Frequently Asked Questions

Where is the ModeToggle component located in the refine-shadcn source code?

The component is defined in packages/theme/src/components/modeToggle.tsx. It imports useTheme from the provider and uses the shadcn DropdownMenu, SunIcon, and MoonIcon components to render the switching interface.

Can I use the ThemeProvider without rendering the ModeToggle?

Yes. ThemeProvider establishes the context that any component can consume via the useTheme hook. You can build custom theme controls or programmatically call setTheme() without including the ModeToggle dropdown UI.

Why does my application flash the light theme before switching to dark on reload?

This occurs when the theme script has not loaded before the first paint. Ensure ThemeProvider is rendered at the root level of your application (above any Suspense boundaries or layout effects) so the next-themes script can inject the correct class during the initial render cycle.

How do I set a specific theme as the default instead of system preference?

Change the defaultTheme prop on ThemeProvider from "system" to "light" or "dark". Remove the enableSystem prop if you want to disable system detection entirely and force users to manually select their preferred mode.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →