# Dark Mode

Starwind UI components support light and dark themes out of the box. Dark styles use Tailwind
CSS's `dark:` variant and become active when the `dark` class is present on the `<html>` element.

Starwind's shared theme controller manages that class, persists the user's preference, follows
system color-scheme changes, and keeps theme controls synchronized.

## Add Theme Initialization

### Astro

Render `ThemeInitScript` in your main layout's `<head>`. It applies the saved or system theme
before the page becomes visible, preventing a flash of the wrong theme.

```astro title="src/layouts/Layout.astro"
<!doctype html>
<html lang="en">
  <head>
    <!-- ... other head elements -->
    <ThemeInitScript />
  </head>
  <body>
    <slot />
  </body>
</html>
```

By default, `ThemeInitScript`:

- reads and writes the `colorTheme` key in `localStorage`;
- accepts `light`, `dark`, or `system` as the saved preference;
- uses `system` when no preference has been saved; and
- toggles the `dark` class on `<html>` based on the resolved theme.

It also reapplies the theme after Astro view transitions, so no additional
`astro:after-swap` listener is needed.

> **Info:** `system` remains the saved preference while the resolved theme follows
`window.matchMedia("(prefers-color-scheme: dark)")`. Once the shared theme controller is active,
it updates the page when the operating system preference changes.

### Vite React

Vite React uses `getThemeInitScript` from `@starwind-ui/react/theme`. The Starwind CLI adds a small
Vite `transformIndexHtml` plugin that places the script at the start of the document head. This
applies the theme before React renders. See the
[Vite React placement and config](/docs/frameworks/vite-react/#manual-setup-details).

### Next.js

The App Router renders `ThemeInitScript` in `app/layout.*` or `src/app/layout.*`. The Pages Router
renders it in `pages/_document.*` or `src/pages/_document.*`; Starwind creates `_document` when it
is absent. Both use `ThemeInitScript` from `@starwind-ui/react/theme` and set
`suppressHydrationWarning` on the root HTML element. See the
[Next.js App Router](/docs/frameworks/nextjs/#app-router) and
[Next.js Pages Router](/docs/frameworks/nextjs/#pages-router) sections.

### TanStack Start

TanStack Start renders `ThemeInitScript` in `src/routes/__root.*`, immediately before
`HeadContent`. Keep one prepaint script in the root document. Initialization recognizes an
existing Starwind initializer and avoids adding a duplicate. See the
[TanStack Start theme setup](/docs/frameworks/tanstack-start/#manual-setup-details).

### React Router

React Router framework mode renders `ThemeInitScript` in the `<head>` owned by `app/root.*`. The
root `<html>` element receives `suppressHydrationWarning`. See the
[React Router root setup](/docs/frameworks/react-router/#manual-setup-details).

### Vue (Beta)

Vue 3.5 public beta exposes `getThemeInitScript` and `initThemeController` from
`@starwind-ui/vue/theme`. The first returns a script for the document head. The controller connects
controls and follows system theme changes after mount. Keep their options consistent.

The Vue host setup in `starwind init` wires CSS. Add the theme initializer below when the
application needs saved or system themes.

#### Vite Vue

Add a plugin beside the existing Vue and Tailwind plugins in `vite.config.ts`:

```ts title="vite.config.ts"
import { getThemeInitScript } from "@starwind-ui/vue/theme";
import type { Plugin } from "vite";

function starwindThemeInitPlugin(): Plugin {
  return {
    name: "starwind-theme-init",
    transformIndexHtml() {
      return [{
        tag: "script",
        attrs: { "data-starwind-theme-init": "" },
        children: getThemeInitScript(),
        injectTo: "head-prepend",
      }];
    },
  };
}

// Include starwindThemeInitPlugin() in the existing plugins array.
```

#### Nuxt 3 and 4

Merge a script into the document head in `nuxt.config.ts`. Keep the CSS and Tailwind settings
from the [Nuxt setup guide](/docs/frameworks/nuxt/#framework-wiring).

```ts title="nuxt.config.ts"
import { getThemeInitScript } from "@starwind-ui/vue/theme";

export default defineNuxtConfig({
  app: {
    head: {
      script: [{ innerHTML: getThemeInitScript() }],
    },
  },
});
```

This uses Nuxt's [head configuration](https://nuxt.com/docs/4.x/getting-started/seo-meta).

#### Astro Vue

Use the Astro `ThemeInitScript` in the shared layout, as shown in the
[Astro section](#astro). It applies the theme for the whole page, including Vue islands.

#### Laravel with Inertia Vue

The Laravel starter already owns an appearance preference and a head script. Keep that owner when
using its appearance controls. If adopting Starwind's theme controller, connect all theme controls
to the same storage key and replace the previous head initializer with Starwind's script.

For Laravel or Quasar, the script can be generated as a public asset:

```js title="scripts/write-starwind-theme.mjs"
import { mkdir, writeFile } from "node:fs/promises";
import { getThemeInitScript } from "@starwind-ui/vue/theme";

await mkdir("public", { recursive: true });
await writeFile("public/starwind-theme.js", getThemeInitScript());
```

Run `node scripts/write-starwind-theme.mjs` before development and production builds. Regenerate it
after changing theme options or updating the adapter. In Laravel, load it before the application
scripts in the `<head>` of `resources/views/app.blade.php`:

```html
<script src="{{ asset('starwind-theme.js') }}"></script>
```

#### Quasar Vite SPA and SSR

Generate the public asset above and load it in the `<head>` of Quasar's root `index.html`, before
the application entry. This example assumes the app is deployed at the domain root:

```html
<script src="/starwind-theme.js"></script>
```

Quasar copies [public assets](https://quasar.dev/quasar-cli-vite/handling-assets/) into the build.
For deployment under a subdirectory, prefix the script URL with the app's configured public path.
If the app also uses Quasar Dark, connect its controls to the same preference so the two sets of
components change together.

#### Vue Theme Controls

The installed Starwind Theme Toggle connects the theme controller on mount. For a custom control,
initialize it in the application root and release it when that owner unmounts:

```vue title="src/components/ThemeControls.vue"
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue";
import { initThemeController } from "@starwind-ui/vue/theme";

let controller: ReturnType<typeof initThemeController> | undefined;
onMounted(() => { controller = initThemeController(); });
onUnmounted(() => { controller?.destroy(); });
</script>

<template>
  <button type="button" @click="controller?.setTheme('light')">Light</button>
  <button type="button" @click="controller?.setTheme('dark')">Dark</button>
  <button type="button" @click="controller?.setTheme('system')">System</button>
</template>
```

Keep this owner mounted for the application lifetime. In Astro, the page's Astro theme helper owns
the shared controller; use the installed Vue Theme Toggle inside islands.

## Customize the defaults

Pass the same settings to the initializer and any controller you initialize manually. The defaults
work for most Starwind projects.

```astro
<ThemeInitScript storageKey="colorTheme" defaultTheme="system" className="dark" />
```

| Prop | Default | Description |
| --- | --- | --- |
| `storageKey` | `"colorTheme"` | The `localStorage` key used for the saved preference. |
| `defaultTheme` | `"system"` | The preference used when no valid saved value exists. |
| `className` | `"dark"` | The class applied to `<html>` when the resolved theme is dark. |

## Add a theme control

Use Starwind UI's [Theme Toggle](/docs/components/theme-toggle/) component for a ready-to-use
light/dark control. It initializes the shared controller, updates the saved preference and `<html>`
class, and synchronizes other theme controls on the page.

```astro
---
import { ThemeToggle } from "@/components/starwind/theme-toggle";
---

<ThemeToggle ariaLabel="Toggle theme" />
```

The toggle switches between `light` and `dark`. Use the lower-level theme controller when your UI
needs an explicit three-way light, dark, and system picker or needs to change the theme
imperatively.

```ts
import { initThemeController } from "@starwind-ui/astro/theme";

const theme = initThemeController();

theme.setTheme("system");
theme.setTheme("dark");
```

> **Tip:** You can find advanced production-ready theme switchers on [Starwind Pro](https://pro.starwind.dev/components/theme-switcher/).