--- title: Dark Mode description: How the dark variant is defined and switched. type: guide related: - /en/docs/packages/design-system/colors - /en/docs/packages/design-system/provider --- # Dark Mode Dark mode is a `dark` class on the `` element, put there by [next-themes](https://github.com/pacocoursey/next-themes) and read by a Tailwind custom variant. ## The variant Tailwind v4 has no `darkMode` config option, because there is no config file. The variant is declared in CSS instead: ```css title="packages/design-system/styles/globals.css" @custom-variant dark (&:is(.dark *)); ``` That is what makes `dark:bg-card` compile. It reads as "when an ancestor has `.dark`", which is a descendant selector — the element carrying the class is not itself matched, only what is inside it. Since the class goes on ``, that covers the entire document. Most components never use the variant. The token layer already defines each semantic value twice — once in `:root`, once in `.dark` — so `bg-background` changes on its own. Reach for `dark:` only when a component needs a genuinely different treatment rather than a different colour: a border that exists only on dark, an image that needs inverting. ## The switch `ThemeProvider` wraps next-themes and is mounted by `DesignSystemProvider`: ```tsx ``` * `attribute="class"` is what puts `.dark` on ``, matching the variant above. Change one and you must change the other. * `defaultTheme="system"` means each app follows the operating system until the user chooses otherwise. * `disableTransitionOnChange` suppresses CSS transitions during the swap. Without it every transitioned property animates at once and the whole page visibly wipes. The theme is resolved on the client, so the server cannot know it. Each app's `` carries `suppressHydrationWarning` because next-themes writes the class before React hydrates — without it, React reports a mismatch on every page load in development. ## Letting the user choose `ModeToggle` is already in the `app` sidebar and the `web` navbar, and can go anywhere: ```tsx title="page.tsx" import { ModeToggle } from '@kreogen/design-system/components/mode-toggle'; const MyPage = () => ; ``` ## Reading the theme in code ```tsx title="page.tsx" 'use client'; import { useTheme } from 'next-themes'; const MyPage = () => { const { resolvedTheme } = useTheme(); return resolvedTheme === 'dark' ? 'Dark' : 'Light'; }; ``` Use `resolvedTheme`, not `theme`. `theme` can be the literal string `"system"`, which is not something you can branch a colour on. Both are `undefined` on the first client render, before next-themes has read storage. Rendering directly from either causes a flash of the wrong branch, so gate on mount for anything visible. ## Third-party components Sonner is configured to follow the same provider, and the authentication screens inherit it automatically because they are built from these components. Anything you add that has its own theme prop should be pointed at `resolvedTheme` rather than given a fixed value. --- For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md) For an index of all available documentation, see [/llms.txt](/llms.txt)