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

ConcernNext.jsAstroRemixTanStack Start
Initial themenext-themes provider in root layoutInline script before page bodyLoader reads theme from sessionScriptOnce injects early resolver
Persistencenext-themes behaviorlocalStorage via MutationObserverCookie session storage and action routelocalStorage storageKey
Root classProvider uses attribute="class"Script and toggle update documentElementhtml className uses selected themeapplyTheme updates documentElement
System preferencedefaultTheme="system" and enableSystemToggle resolves system with matchMediaremix-themes manages selected valueExplicit 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-themes

The 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 settingWhere it appearsPurpose
attribute="class"Next.js ThemeProviderApplies theme by class instead of another attribute strategy.
defaultTheme="system"Next.js and TanStack StartStarts from the operating system preference.
enableSystemNext.js ThemeProviderEnables system preference handling in next-themes.
disableTransitionOnChangeNext.js ThemeProviderAvoids distracting transitions during theme changes.
suppressHydrationWarningNext.js layout and TanStack Start root routePrevents expected root markup mismatch warnings.
client:loadAstro ModeToggle usageHydrates the React toggle on the client.
themeActionRemix ThemeProviderPoints to the action route that stores theme changes.
ScriptOnceTanStack Start providerRuns the theme resolver before hydration.
storageKeyTanStack Start providerNames 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.