Eliminate Theme‑Toggle Layout Shifts in Chakra UI with useColorModeScript
Chakra UI’s default theme toggle adds the color‑mode class after the first paint, causing layout shifts. The built‑in useColorModeScript hook injects a pre‑paint script that writes the correct class to <html> before the browser renders, eliminating the flash and keeping CLS at zero.
02 Oct 2026, 00:17 UTC

The flash you didn't ask for
When a Chakra UI app loads, the default color‑mode logic reads the user’s preference after the first React render. The html element gets a chakra-ui-light or chakra-ui-dark class only once the client‑side JavaScript runs. On a page that uses fluid components such as Card, Grid or Flex, that late class change forces a re‑paint and often a visible layout shift — the dreaded “flash of wrong theme”.
The shift is more than cosmetic: it contributes to Cumulative Layout Shift (CLS) scores, hurts perceived performance, and can break animations that depend on stable dimensions.
How useColorModeScript works
Chakra UI v2 ships a tiny hook called useColorModeScript. When placed inside a client‑only component (for example a component that renders only on the browser), it injects a <script> into the document head before the browser paints. The script:
- Runs during server‑side rendering (SSR) or static generation.
- Reads
window.matchMedia('(prefers-color-scheme: dark)')or a persisted value inlocalStorage. - Immediately writes the appropriate class (
chakra-ui-lightorchakra-ui-dark) onto the<html>element.
Because the class is present before the first paint, the browser can apply the correct CSS variables instantly. The later client‑side toggle (via useColorMode) merely flips the same class — no extra layout pass is required.
Putting it together: a minimal example
The following snippet works in a Next.js 13+ app (App Router) or any React 18 project that uses Chakra UI v2+. It shows the minimal wiring needed to avoid the shift.
// app/providers.tsx (client‑only component)
'use client';
import { ChakraProvider, useColorModeScript } from '@chakra-ui/react';
import { ColorModeScript } from '@chakra-ui/react';
// A tiny wrapper that only renders the script.
function ColorModeScriptWrapper() {
useColorModeScript(); // injects the pre‑paint script
return null;
}
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ChakraProvider>
<ColorModeScriptWrapper />
{children}
</ChakraProvider>
);
}
Then use the provider in your root layout:
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Now a simple toggle button anywhere in the tree works without a layout shift:
// components/ThemeToggle.tsx
'use client';
import { useColorMode, Switch } from '@chakra-ui/react';
export function ThemeToggle() {
const { colorMode, toggleColorMode } = useColorMode();
return (
<Switch
checked={colorMode === 'dark'}
onChange={toggleColorMode}
aria-label="Toggle dark mode"
/>
);
}
Caveats and how to verify
| Concern | Impact | Mitigation |
|---|---|---|
| Content Security Policy (CSP) blocks inline scripts | The pre‑paint script won’t run; the page falls back to the system default, causing a brief flash. | Add 'sha256-' or 'unsafe-inline' for the injected script, or serve the script from a separate file and reference it via scriptSrc in Chakra’s config. |
| SSR hydration mismatch | If useColorModeScript is used inside a component that also renders on the server, React may warn about mismatched html class. |
Keep the hook in a 'use client' component (as shown) so it only runs during hydration. |
| Chakra UI < v2 | The hook does not exist; you must manually manage the class or upgrade. | Upgrade to v2+ or implement a tiny inline script that mirrors the hook’s logic. |
Practical verification steps
- Run the app locally:
npm run dev(oryarn dev) in the project root. No elevated permissions required. - Open Chrome DevTools → Performance → “Layout Shift” audit (or Lighthouse “CLS”).
- Reload the page and toggle the theme using the
ThemeTogglecomponent. - Confirm the CLS score stays at 0.00 and no visual jump occurs.
If a shift appears, double‑check that the ColorModeScriptWrapper is rendered before any Chakra components and that CSP isn’t stripping the inline script.
Next steps
Add the useColorModeScript wrapper to every entry point that uses Chakra UI (e.g., each Next.js page or Vite entry). Test with a real‑world layout that includes Grid, Card, or Container components to ensure the fix holds across breakpoints. Once verified, you can safely ship dark‑mode toggles without worrying about CLS regressions.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.