Dark Mode
Purpose and Scope
Dark mode in shadcn/ui is documented as a framework-specific setup problem rather than a single universal package. The dark mode landing page introduces the task as adding dark mode to a site and routes readers to guides for Next.js, Vite, Astro, Remix, and TanStack Start. That organization matters because the visual styling model is shared, but the persistence, hydration, server rendering, and route integration details differ by framework. In practice, the common goal is to make the root document carry the theme class that shadcn/ui components and Tailwind dark variants can react to.
Sources: apps/v4/content/docs/dark-mode/index.mdx, apps/v4/content/docs/dark-mode/meta.json
The shared mental model is simple: choose a theme value, resolve it to light or dark when needed, apply a class to the document element, and expose a mode toggle so users can change the preference. The official examples use the same visible toggle pattern with sun and moon icons, a shadcn/ui Button, and Dropdown Menu items for Light, Dark, and System when the framework supports that option. The implementation details vary, but the user-facing contract remains consistent: the site should render correctly on first load and should not unexpectedly flash the wrong theme.
Sources: apps/v4/content/docs/dark-mode/astro.mdx, apps/v4/content/docs/dark-mode/next.mdx, apps/v4/content/docs/dark-mode/remix.mdx, apps/v4/content/docs/dark-mode/tanstack-start.mdx
Relevant Source Files
- apps/v4/content/docs/dark-mode/index.mdx — The dark mode landing page with the title, description, and framework cards for guide selection.
- apps/v4/content/docs/dark-mode/astro.mdx — The Astro guide, including an inline theme script, a React mode toggle, and usage with a client directive.
- apps/v4/content/docs/dark-mode/meta.json — The documentation navigation metadata for the dark mode section.
- apps/v4/content/docs/dark-mode/next.mdx — The Next.js guide, including next-themes installation, provider setup, root layout wrapping, and mode toggle placement.
- apps/v4/content/docs/dark-mode/remix.mdx — The Remix guide, including Tailwind selector adjustment, remix-themes setup, session storage, action route, and toggle code.
- apps/v4/content/docs/dark-mode/tanstack-start.mdx — The TanStack Start guide, including a custom provider, pre-hydration script injection, root route wrapping, and toggle setup.
Framework Guide Selection
Start with the dark mode index when you are deciding which setup path matches your application. The metadata declares the section title as Dark mode and orders the pages as index, next, vite, astro, remix, and tanstack-start. The index presents cards for the major supported framework paths, including Next.js, Vite, and Astro in the supplied content. This is a docs navigation surface, not an implementation file, so it is best used to route the reader to the framework guide that matches the runtime where the document element, hydration, and persistence need to be handled.
Sources: apps/v4/content/docs/dark-mode/index.mdx, apps/v4/content/docs/dark-mode/meta.json
For Next.js applications, the guide delegates theme state and system preference handling to next-themes. The flow begins by installing the dependency, then creating a small client-side ThemeProvider wrapper around the provider from next-themes. The root layout imports that provider, adds suppressHydrationWarning to the html element, and configures the provider with class-based theming, a system default, enableSystem, and disableTransitionOnChange. Those settings align shadcn/ui with class-driven dark styling while reducing hydration noise and transition artifacts when the theme changes.
Sources: apps/v4/content/docs/dark-mode/next.mdx
Astro uses a more explicit browser script because the page can be rendered before React components hydrate. The guide creates an inline script in the Astro page that reads a stored theme when present, otherwise falls back to the prefers-color-scheme media query. It immediately adds or removes the dark class on document.documentElement, then watches class changes with a MutationObserver and writes the chosen value back to localStorage. The React mode toggle is then loaded with client:load so the interactive dropdown can take over after the initial document theme is already correct.
Sources: apps/v4/content/docs/dark-mode/astro.mdx
Remix treats theme as request-aware state. Its guide first expands the Tailwind dark selector to include :root[class~="dark"], allowing the dark class on the html element to activate dark styles. It then installs remix-themes, creates cookie session storage, and exports a theme session resolver. The root loader reads the stored theme, the ThemeProvider receives the specifiedTheme and themeAction, and PreventFlashOnWrongTheme is rendered in the head. A separate action route stores theme changes, which gives Remix a server-integrated persistence path rather than only local browser state.
Sources: apps/v4/content/docs/dark-mode/remix.mdx
TanStack Start uses a custom provider and a pre-hydration script to avoid a flash of unstyled content. The guide defines a Theme union of dark, light, and system, stores the preference under a configurable storage key, and resolves system mode using the prefers-color-scheme media query. ScriptOnce from @tanstack/react-router injects a script before React hydrates, adding the resolved class and setting colorScheme. After mount, effects load the stored value, apply theme changes, and subscribe to media changes while the selected theme is system.
Sources: apps/v4/content/docs/dark-mode/tanstack-start.mdx
System-to-Code Mapping
| Concern | Next.js | Astro | Remix | TanStack Start |
|---|---|---|---|---|
| Initial theme | next-themes provider in root layout | Inline script before page body | Loader reads theme from session | ScriptOnce injects early resolver |
| Persistence | next-themes behavior | localStorage via MutationObserver | Cookie session storage and action route | localStorage storageKey |
| Root class | Provider uses attribute="class" | Script and toggle update documentElement | html className uses selected theme | applyTheme updates documentElement |
| System preference | defaultTheme="system" and enableSystem | Toggle resolves system with matchMedia | remix-themes manages selected value | Explicit media query resolution and listener |
Setup Flow by Framework
A practical implementation usually follows three steps. First, install or write the theme state layer that fits the framework. Next.js installs next-themes, Remix installs remix-themes, Astro writes an inline script, and TanStack Start builds a provider around localStorage and ScriptOnce. Second, wire that layer into the application shell, such as the root layout, root route, or top-level Astro page. Third, add a mode toggle using shadcn/ui Button and Dropdown Menu primitives so users can switch modes without knowing how the state is stored internally.
Sources: apps/v4/content/docs/dark-mode/astro.mdx, apps/v4/content/docs/dark-mode/next.mdx, apps/v4/content/docs/dark-mode/remix.mdx, apps/v4/content/docs/dark-mode/tanstack-start.mdx
npm install next-themes
npm install remix-themesThe toggle examples also show how theming and component composition meet. The button is rendered as an icon-sized control, the trigger is wrapped with asChild, and the Dropdown Menu contains Light, Dark, and System actions where supported. The sun icon is visible in light mode while the moon icon becomes visible through dark-prefixed utility classes, so the control itself demonstrates that the root dark class is working. Keep the screen-reader-only label because the visual icon swap does not communicate the control purpose to assistive technology.
Sources: apps/v4/content/docs/dark-mode/astro.mdx, apps/v4/content/docs/dark-mode/remix.mdx, apps/v4/content/docs/dark-mode/tanstack-start.mdx
Implementation Details and Edge Cases
Hydration is the main edge case across these guides. Next.js and TanStack Start both add suppressHydrationWarning to the html element in their root shell examples because the class applied before or during client hydration can differ from the static markup. Remix uses PreventFlashOnWrongTheme in the head, backed by server-loaded session data. Astro avoids waiting for React by executing an inline script before the page content becomes interactive. These choices are not interchangeable decorations; they are the parts that keep the first paint aligned with the stored or system preference.
Sources: apps/v4/content/docs/dark-mode/astro.mdx, apps/v4/content/docs/dark-mode/next.mdx, apps/v4/content/docs/dark-mode/remix.mdx, apps/v4/content/docs/dark-mode/tanstack-start.mdx
Storage choice should match the application model. Browser-only applications can use localStorage and media queries directly, as shown by Astro and TanStack Start. Server-rendered Remix applications use cookie session storage so the loader can provide the current theme before rendering the document. Next.js applications can rely on next-themes for the common provider behavior, but still need to configure class-based theming to match the way shadcn/ui dark styles are authored. When changing any of these examples, preserve the root class behavior first, then customize the UI around it.
Sources: apps/v4/content/docs/dark-mode/astro.mdx, apps/v4/content/docs/dark-mode/next.mdx, apps/v4/content/docs/dark-mode/remix.mdx, apps/v4/content/docs/dark-mode/tanstack-start.mdx
Compact Reference
| Name or setting | Where it appears | Purpose |
|---|---|---|
| attribute="class" | Next.js ThemeProvider | Applies theme by class instead of another attribute strategy. |
| defaultTheme="system" | Next.js and TanStack Start | Starts from the operating system preference. |
| enableSystem | Next.js ThemeProvider | Enables system preference handling in next-themes. |
| disableTransitionOnChange | Next.js ThemeProvider | Avoids distracting transitions during theme changes. |
| suppressHydrationWarning | Next.js layout and TanStack Start root route | Prevents expected root markup mismatch warnings. |
| client:load | Astro ModeToggle usage | Hydrates the React toggle on the client. |
| themeAction | Remix ThemeProvider | Points to the action route that stores theme changes. |
| ScriptOnce | TanStack Start provider | Runs the theme resolver before hydration. |
| storageKey | TanStack Start provider | Names the localStorage key for the selected theme. |
Next Steps
After choosing the framework guide, verify three things in your application: the html element receives the expected light or dark class on first load, the toggle changes the theme without a full navigation, and a refresh preserves the selected preference or correctly follows system mode. If the styles do not change, inspect the document element before debugging individual components. If the first render flashes, revisit the framework-specific pre-hydration mechanism described above. For deeper styling changes after dark mode works, continue with the Theming and Tailwind v4 pages because they explain the token and CSS variable layers that dark mode activates.