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
ThemeProvideris the outermost component in your tree and that you importedglobals.css. Without the CSS import, Tailwind cannot process thedark:variants. - Icons appear static without animation: Verify that your
tailwind.config.jsincludesdarkMode: "class". Therefine-shadcntemplates pre-configure this, but custom setups may override it. - Theme resets on page refresh: This occurs when
localStorageis 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-menualias correctly. The library'stsconfig.jsonsets this path, but custom Vite or Webpack configs must match.
Summary
- Wrap your app with
ThemeProviderfrom@ferdiunal/refine-shadcnusingattribute="class"andenableSystemto activate the theme context. - Import global styles via
@ferdiunal/refine-shadcn/dist/globals.cssto enable Tailwind dark mode utilities. - Place
ModeTogglein your header or settings panel; the component is pre-built and ready to use. - Persistence is automatic—
next-themeshandleslocalStorageand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →