Implement Dark/Light Mode Toggling with Chakra UI ColorMode System
A practical guide to implementing dark/light mode toggling in React with Chakra UI's ColorMode system, covering provider setup, toggle creation, persistence strategies for SSR, and hydration verification.
01 Jul 2026, 06:46 UTC

Desired Outcome
A React application that switches between light and dark themes instantly, persists the user's choice across sessions, and renders the correct theme on the server without hydration mismatches.
Prerequisites
- React 18+ project (Create React App, Vite, Next.js 13+, or Remix)
- Chakra UI v2.8+ or v3.0+ installed (
@chakra-ui/react,@emotion/react,@emotion/styled,framer-motion) - For SSR frameworks: Next.js App Router or Pages Router, or Remix v2+
- Basic familiarity with React hooks and context
Provider Setup
Chakra UI v2 (Pages Router, CRA, Vite)
Wrap your application root with ColorModeScript and ChakraProvider. The script must execute before the provider to prevent a flash of incorrect theme during hydration.
// src/main.jsx or pages/_app.jsx (Next.js Pages Router)
import { ChakraProvider, ColorModeScript, extendTheme } from '@chakra-ui/react'
const theme = extendTheme({
config: {
initialColorMode: 'light', // default if no preference stored
useSystemColorMode: true, // respect OS preference on first visit
},
})
export default function App({ Component, pageProps }) {
return (
)
}Chakra UI v3 with Next.js App Router
v3 moves the SSR script to a separate package. Install @chakra-ui/next-js and use its provider wrapper.
// app/providers.jsx
'use client'
import { ChakraProvider, ColorModeScript } from '@chakra-ui/next-js'
import { extendTheme } from '@chakra-ui/react'
const theme = extendTheme({
config: {
initialColorMode: 'light',
useSystemColorMode: true,
},
})
export function Providers({ children }) {
return (
{children}
)
}Then wrap your layout:
// app/layout.jsx
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
{children}
)
}The suppressHydrationWarning on <html> silences React's mismatch warning for the data-theme attribute that Chakra manages.
Toggle Button Component
Create a reusable button that reads the current mode and switches it. Use useColorMode for the value and toggle function, and useColorModeValue for dynamic icon selection.
// components/ColorModeToggle.jsx
import { IconButton, useColorMode, useColorModeValue } from '@chakra-ui/react'
import { FaSun, FaMoon } from 'react-icons/fa'
export function ColorModeToggle() {
const { colorMode, toggleColorMode } = useColorMode()
// Returns Sun icon in dark mode, Moon in light mode
const Icon = useColorModeValue(FaMoon, FaSun)
return (
}
variant="ghost"
size="sm"
/>
)
}Place the toggle in your header or navigation:
// components/Header.jsx
import { Box, Flex } from '@chakra-ui/react'
import { ColorModeToggle } from './ColorModeToggle'
export function Header() {
return (
My App
)
}Conditional Styling Patterns
Using the _dark Pseudo-Prop
Apply dark-mode overrides directly in JSX. This works for any style prop and avoids runtime logic.
<Box
bg="gray.50"
_dark={{
bg: "gray.900",
color: "gray.100",
}}
p={6}
borderRadius="lg"
>
Content adapts automatically
</Box>Using useColorModeValue in Logic
When you need a value in JavaScript (e.g., for a chart library or conditional class), use the hook.
import { useColorModeValue } from '@chakra-ui/react'
function ChartContainer() {
const gridColor = useColorModeValue('gray.200', 'gray.700')
const textColor = useColorModeValue('gray.800', 'gray.200')
return (
)
}Persistence: LocalStorage (Default) vs Cookies (SSR)
Chakra's built-in localStorage manager works for client-only apps. For SSR, it causes hydration mismatch because the server cannot read localStorage. Implement a cookie-based manager.
Custom Cookie ColorModeManager (Next.js App Router)
// lib/color-mode-manager.ts
import { ColorModeManager } from '@chakra-ui/react'
import { parseCookies, setCookie, destroyCookie } from 'nookies'
export const cookieColorModeManager: ColorModeManager = {
get: (initialMode) => {
if (typeof window === 'undefined') {
// Server side: read from request cookies
const cookies = parseCookies()
return (cookies['chakra-ui-color-mode'] as 'light' | 'dark') || initialMode
}
// Client side: read from document.cookie
const cookies = parseCookies()
return (cookies['chakra-ui-color-mode'] as 'light' | 'dark') || initialMode
},
set: (mode) => {
if (typeof window === 'undefined') return
setCookie(null, 'chakra-ui-color-mode', mode, {
maxAge: 365 * 24 * 60 * 60, // 1 year
path: '/',
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
})
},
clear: () => {
if (typeof window === 'undefined') return
destroyCookie(null, 'chakra-ui-color-mode', { path: '/' })
},
}Pass the manager to your theme config:
// theme.ts
import { extendTheme } from '@chakra-ui/react'
import { cookieColorModeManager } from './lib/color-mode-manager'
export const theme = extendTheme({
config: {
initialColorMode: 'light',
useSystemColorMode: true,
colorModeManager: cookieColorModeManager,
},
})Next.js Pages Router: _document.js for Early Script Injection
In Pages Router, ColorModeScript must run in <Head> of a custom _document.js to set data-theme before hydration.
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document'
import { ColorModeScript } from '@chakra-ui/react'
export default class MyDocument extends Document {
render() {
return (
)
}
}Expected Checks
- Toggle persists on refresh: Click the toggle, reload the page — the selected mode remains without flash.
- Inspect
<html>element: Open DevTools Elements tab; the root element should havedata-theme="light"ordata-theme="dark"matching the active mode. - Verify storage: DevTools → Application → Local Storage (or Cookies) →
chakra-ui-color-modestores"light"or"dark". - System preference test: With
useSystemColorMode: true, set OS to dark mode, open in incognito — initial render should be dark. - SSR raw HTML check: Run
curl -s http://localhost:3000 | grep data-theme— output should containdata-theme="light"ordata-theme="dark".
Recovery Options
Toggle Appears Stuck
- Clear storage for the domain: DevTools → Application → Clear site data (or run
localStorage.clear()in console). - Verify
ColorModeScriptruns beforeChakraProviderin the component tree — no other provider wraps it. - Check for duplicate
ChakraProviderinstances (common in Next.js when both_app.jsxandlayout.jsxadd one).
Hydration Mismatch in Console
- Ensure
ColorModeScript(v2) or@chakra-ui/next-js(v3) is imported and rendered. - Confirm
suppressHydrationWarningis on the<html>element. - Verify the custom
colorModeManagerreads cookies on the server (Pages Router:getInitialPropsin_document.js; App Router: cookie managergethandlestypeof window === 'undefined').
Private Browsing / Blocked Cookies
The cookie manager falls back to initialColorMode when cookies are unavailable. For a more robust fallback, extend the manager to attempt localStorage first, then cookies, then default:
get: (initialMode) => {
if (typeof window !== 'undefined') {
try {
const ls = localStorage.getItem('chakra-ui-color-mode')
if (ls) return JSON.parse(ls) as 'light' | 'dark'
} catch { /* ignore */ }
}
// ... cookie logic
return initialMode
}Limitations & Gotchas
- Inline style objects referencing theme colors break in SSR:
sx={{ bg: 'gray.50' }}serializes as empty object on server. Use_darkprops oruseColorModeValueinstead. - v3 import paths differ:
ColorModeScriptcomes from@chakra-ui/next-js, not@chakra-ui/react. Check yourpackage.jsonversion. useSystemColorModeonly applies on first visit: After a user toggles, their explicit choice is stored and overrides system preference.- Framer Motion peer dependency: Required for transitions; ensure it's installed or toggle animations will warn.
Quick Verification Checklist
| Check | Command / Action | Pass Criteria |
|---|---|---|
| Theme persists | Toggle → Refresh | No flash, mode retained |
| HTML attribute | Inspect <html> | data-theme="light|dark" present |
| Storage key | DevTools Application tab | chakra-ui-color-mode exists |
| System preference | OS dark + incognito | Initial render dark |
| SSR HTML | curl -s URL | grep data-theme | Attribute in raw response |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.