# Theming

The tokens every component is styled with, and how dark mode switches them.

The theme sets shadcn's standard color variables, so other shadcn components match. It lives in
your CSS, and changing a token there changes every component that uses it.

## Dark mode

Every token has a dark value, used inside any element with the `dark` class, where Tailwind's
`dark:` variant applies too. The theme also sets `color-scheme: dark` there, so scrollbars and
native form controls follow.

Put the class on `<html>` to switch the whole page. It has to be there before the page first
paints, or a dark page flashes light as it loads, so a script at the top of `<head>` sets it from
the reader's stored choice, or from the system setting until they make one. A provider then
stores each new choice and, while it's System, follows the system setting as it changes.

Below are setups for Next.js and Vite. For other frameworks, such as Astro, Remix and TanStack
Start, follow [shadcn's dark mode guides](https://ui.shadcn.com/docs/dark-mode), which set the
same class.

### Next.js

[next-themes](https://github.com/pacocoursey/next-themes) does both. Install it.

```bash
npm install next-themes
```

Wrap the root layout in its `ThemeProvider`, and add `suppressHydrationWarning` to `<html>`,
since its script changes the class before React hydrates.

```tsx title="app/layout.tsx"
import { ThemeProvider } from "next-themes";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}
```

`disableTransitionOnChange` turns transitions off for the moment the theme switches, so controls
change color at once rather than fading one by one.

### Vite

Add the script to `<head>` in `index.html`, before anything that renders. It falls back to the
system setting when nothing valid is stored, and leaves the page alone if storage is blocked.

```html title="index.html"
<script>
  (function () {
    try {
      var theme = localStorage.getItem("theme");
      if (theme !== "light" && theme !== "dark" && theme !== "system") theme = "system";
      var dark = matchMedia("(prefers-color-scheme: dark)").matches;
      var resolved = theme === "system" ? (dark ? "dark" : "light") : theme;
      var root = document.documentElement;
      root.classList.add(resolved);
      root.style.colorScheme = resolved;
    } catch (e) {}
  })();
</script>
```

Then add a provider, and wrap the app in it.

```tsx title="components/theme-provider.tsx"
import * as React from "react";

type Theme = "light" | "dark" | "system";

const ThemeContext = React.createContext<{
  theme: Theme;
  setTheme: (theme: Theme) => void;
} | null>(null);

function isTheme(value: unknown): value is Theme {
  return value === "light" || value === "dark" || value === "system";
}

function applyTheme(theme: Theme) {
  const dark = matchMedia("(prefers-color-scheme: dark)").matches;
  const resolved = theme === "system" ? (dark ? "dark" : "light") : theme;
  const root = document.documentElement;
  root.classList.remove("light", "dark");
  root.classList.add(resolved);
  root.style.colorScheme = resolved;
}

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setThemeState] = React.useState<Theme>(() => {
    const stored = localStorage.getItem("theme");
    return isTheme(stored) ? stored : "system";
  });

  React.useEffect(() => {
    applyTheme(theme);
    if (theme !== "system") return;
    const media = matchMedia("(prefers-color-scheme: dark)");
    const onChange = () => applyTheme("system");
    media.addEventListener("change", onChange);
    return () => media.removeEventListener("change", onChange);
  }, [theme]);

  const value = React.useMemo(
    () => ({
      theme,
      setTheme: (next: Theme) => {
        localStorage.setItem("theme", next);
        setThemeState(next);
      },
    }),
    [theme],
  );

  return <ThemeContext value={value}>{children}</ThemeContext>;
}

export function useTheme() {
  const context = React.useContext(ThemeContext);
  if (!context) throw new Error("useTheme must be used within a ThemeProvider");
  return context;
}
```

It stores the choice under `theme`, the key next-themes uses too.

### Toggle

A [Dropdown Menu](https://ui.jarv.is/components/dropdown-menu.md) with a radio item for each choice lets readers pick
one, and shows which is set. The trigger's icon follows the page with the `dark:` variant, so it's
right before the provider knows the choice. Import `useTheme` from `next-themes` in Next.js, or
from `@/components/theme-provider` with the Vite provider. Both return the `theme` and `setTheme`
it uses.

```tsx title="components/mode-toggle.tsx"
"use client";

import { MoonIcon, SunIcon } from "lucide-react";

import { useTheme } from "@/components/theme-provider";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

export function ModeToggle() {
  const { theme, setTheme } = useTheme();
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="ghost" size="icon" aria-label="Theme" />}>
        <SunIcon className="dark:hidden" />
        <MoonIcon className="hidden dark:block" />
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
          <DropdownMenuRadioItem value="light">Light</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="dark">Dark</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="system">System</DropdownMenuRadioItem>
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
```

## Tokens

Alongside shadcn's variables, the theme adds these.

| Tokens                                                                        | Use                                                                             |
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `layer-1` … `layer-4`                                                         | Stepped surfaces, from raised panels to tracks.                                 |
| `faint`                                                                       | Tertiary text, quieter than `muted-foreground`.                                 |
| `icon`, `icon-muted`                                                          | Icon colors.                                                                    |
| `border-muted`, `border-strong`                                               | Lighter and heavier hairlines.                                                  |
| `hover`, `pressed`                                                            | Translucent overlays for interactive states.                                    |
| `primary-hover`, `primary-pressed`                                            | The same overlays, for primary buttons.                                         |
| `scrim`                                                                       | The backdrop behind dialogs and sheets.                                         |
| `link`                                                                        | Link text.                                                                      |
| `brand`, `brand-foreground`                                                   | The accent on checked controls, sliders and progress.                           |
| `knob`, `knob-checked`                                                        | Knobs on switches and sliders, and the dot in a checked radio.                  |
| `success`, `warning`, `info`, `destructive`                                   | State colors. Each has `-muted` (a tinted surface) and `-border` companions.    |
| `syntax-property`, `-string`, `-number`, `-constant`, `-function`, `-keyword` | Code: keys, strings, numbers, constants such as `true`, functions and keywords. |
| `shadow-raised`, `-floating`, `-overlay`                                      | Elevation for cards, popovers and dialogs.                                      |
| `shadow-button`, `-button-primary`, `-elements`, `-switch-off`, `-switch-on`  | Elevation for controls.                                                         |
| `palette-*`                                                                   | The color scales behind the tokens.                                             |

## Colors

Every color token in light and dark mode. Values such as `grey-50` are steps of the
[palette](#palette). Hover a swatch to see the color it resolves to.

### Surfaces

| Token | Light | Dark |
| --- | --- | --- |
| `background` | `grey-50` (`#fff`) | `grey-1100` (`#161616`) |
| `card` | `grey-100` (`#fafafa`) | `grey-1000` (`#242424`) |
| `popover` | `grey-100` (`#fafafa`) | `grey-1000` (`#242424`) |
| `muted` | `grey-200` (`#f2f2f2`) | `grey-900` (`#2e2e2e`) |
| `layer-1` | `grey-100` (`#fafafa`) | `grey-1000` (`#242424`) |
| `layer-2` | `grey-200` (`#f2f2f2`) | `grey-900` (`#2e2e2e`) |
| `layer-3` | `grey-300` (`#eee`) | `grey-800` (`#3a3a3a`) |
| `layer-4` | `grey-400` (`#dbdbdb`) | `grey-700` (`#5c5c5c`) |

### Text and icons

| Token | Light | Dark |
| --- | --- | --- |
| `foreground` | `grey-1100` (`#161616`) | `grey-100` (`#fafafa`) |
| `card-foreground` | `grey-1100` (`#161616`) | `grey-100` (`#fafafa`) |
| `popover-foreground` | `grey-1100` (`#161616`) | `grey-100` (`#fafafa`) |
| `muted-foreground` | `grey-700` (`#5c5c5c`) | `grey-500` (`#aeaeae`) |
| `faint` | `grey-600` (`#808080`) | `grey-600` (`#808080`) |
| `link` | `blue-600` (`#3b5cf6`) | `blue-400` (`#a2bcff`) |
| `icon` | `grey-800` (`#3a3a3a`) | `grey-400` (`#dbdbdb`) |
| `icon-muted` | `grey-600` (`#808080`) | `grey-600` (`#808080`) |

### Borders

| Token | Light | Dark |
| --- | --- | --- |
| `border` | `#0000001a` | `#ffffff1a` |
| `border-muted` | `#00000014` | `#ffffff14` |
| `border-strong` | `#00000033` | `#ffffff33` |
| `input` | `#0000001a` | `#ffffff1a` |
| `ring` | `blue-500` (`#7698fd`) | `blue-500` (`#7698fd`) |

### Controls

| Token | Light | Dark |
| --- | --- | --- |
| `primary` | `grey-1000` (`#242424`) | `grey-700` (`#5c5c5c`) |
| `primary-foreground` | `grey-50` (`#fff`) | `grey-50` (`#fff`) |
| `secondary` | `grey-50` (`#fff`) | `#ffffff0f` |
| `secondary-foreground` | `grey-1100` (`#161616`) | `grey-100` (`#fafafa`) |
| `accent` | `#0000000a` | `#ffffff0f` |
| `accent-foreground` | `grey-1100` (`#161616`) | `grey-100` (`#fafafa`) |
| `brand` | `blue-600` (`#3b5cf6`) | `blue-600` (`#3b5cf6`) |
| `brand-foreground` | `grey-50` (`#fff`) | `grey-50` (`#fff`) |
| `knob` | `grey-200` (`#f2f2f2`) | `grey-200` (`#f2f2f2`) |
| `knob-checked` | `grey-300` (`#eee`) | `grey-300` (`#eee`) |

### Overlays

Overlays are translucent. `primary-hover` and `primary-pressed` are shown over `primary`.

| Token | Light | Dark |
| --- | --- | --- |
| `hover` | `#0000000a` | `#ffffff0f` |
| `pressed` | `#00000014` | `#ffffff1a` |
| `primary-hover` | `#ffffff1f` | `#0000003d` |
| `primary-pressed` | `#ffffff3d` | `#00000066` |
| `scrim` | `#00000066` | `#00000099` |
| `skeleton` | `#0000000f` | `#ffffff0f` |
| `skeleton-shine` | `#ffffff80` | `#ffffff0a` |

### States

| Token | Light | Dark |
| --- | --- | --- |
| `success` | `green-800` (`#198b43`) | `green-500` (`#6bd586`) |
| `success-muted` | `green-100` (`#e7f9ea`) | `green-1200` (`#14361d`) |
| `success-border` | `green-300` (`#b8e9c1`) | `green-900` (`#1d783c`) |
| `warning` | `yellow-800` (`#cb9f34`) | `yellow-500` (`#f2cf76`) |
| `warning-muted` | `yellow-100` (`#fefaec`) | `yellow-1200` (`#4b4025`) |
| `warning-border` | `yellow-300` (`#f7e5b5`) | `yellow-900` (`#ac8833`) |
| `info` | `blue-800` (`#2c47c8`) | `blue-500` (`#7698fd`) |
| `info-muted` | `blue-100` (`#ecf1fe`) | `blue-1200` (`#1b2852`) |
| `info-border` | `blue-300` (`#c3d4fd`) | `blue-900` (`#263fa9`) |
| `destructive` | `red-800` (`#b82d35`) | `red-500` (`#f17471`) |
| `destructive-muted` | `red-100` (`#fceceb`) | `red-1200` (`#461516`) |
| `destructive-border` | `red-300` (`#f2bbb7`) | `red-900` (`#97252b`) |

### Syntax

Colors for code, such as the keys and values in [JSON Viewer](https://ui.jarv.is/components/json-viewer.md). They're
GitHub Light's and GitHub Dark's, as in the code blocks on this site.

| Token | Light | Dark |
| --- | --- | --- |
| `syntax-property` | `#005cc5` | `#79b8ff` |
| `syntax-string` | `#032f62` | `#9ecbff` |
| `syntax-number` | `#005cc5` | `#79b8ff` |
| `syntax-constant` | `#005cc5` | `#79b8ff` |
| `syntax-function` | `#6f42c1` | `#b392f0` |
| `syntax-keyword` | `#d73a49` | `#f97583` |

### Charts

| Token | Light | Dark |
| --- | --- | --- |
| `chart-1` | `blue-600` (`#3b5cf6`) | `blue-500` (`#7698fd`) |
| `chart-2` | `green-800` (`#198b43`) | `green-500` (`#6bd586`) |
| `chart-3` | `yellow-800` (`#cb9f34`) | `yellow-500` (`#f2cf76`) |
| `chart-4` | `red-800` (`#b82d35`) | `red-500` (`#f17471`) |
| `chart-5` | `grey-600` (`#808080`) | `grey-500` (`#aeaeae`) |

### Sidebar

| Token | Light | Dark |
| --- | --- | --- |
| `sidebar` | `grey-100` (`#fafafa`) | `grey-1200` (`#080808`) |
| `sidebar-foreground` | `grey-1100` (`#161616`) | `grey-100` (`#fafafa`) |
| `sidebar-primary` | `grey-1000` (`#242424`) | `grey-700` (`#5c5c5c`) |
| `sidebar-primary-foreground` | `grey-50` (`#fff`) | `grey-50` (`#fff`) |
| `sidebar-accent` | `#0000000a` | `#ffffff0f` |
| `sidebar-accent-foreground` | `grey-1100` (`#161616`) | `grey-100` (`#fafafa`) |
| `sidebar-border` | `#0000001a` | `#ffffff1a` |
| `sidebar-ring` | `blue-500` (`#7698fd`) | `blue-500` (`#7698fd`) |

## Palette

These are the same in both modes. Each step is a variable, such as `--palette-blue-600`.

| Step | Grey | Red | Orange | Yellow | Green | Cyan | Blue | Purple | Pink |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 50 | `#fff` |  |  |  |  |  |  |  |  |
| 100 | `#fafafa` | `#fceceb` | `#fdf2ed` | `#fefaec` | `#e7f9ea` | `#e2f7fb` | `#ecf1fe` | `#ebecfe` | `#fdecf3` |
| 200 | `#f2f2f2` | `#f6d5d3` | `#ffe7dc` | `#fcefd0` | `#d0f0d5` | `#c4edf4` | `#d7e2fc` | `#d5d5fc` | `#f7d5e4` |
| 300 | `#eee` | `#f2bbb7` | `#ffd8c6` | `#f7e5b5` | `#b8e9c1` | `#a3e4ef` | `#c3d4fd` | `#b9b8f5` | `#fabcd8` |
| 400 | `#dbdbdb` | `#f29b96` | `#ffc1a4` | `#f3da9b` | `#96e3a6` | `#65d9eb` | `#a2bcff` | `#9e99f7` | `#f799c6` |
| 500 | `#aeaeae` | `#f17471` | `#ffa478` | `#f2cf76` | `#6bd586` | `#00c5df` | `#7698fd` | `#8271f8` | `#f26cb2` |
| 600 | `#808080` | `#f1484f` | `#ff8648` | `#f6c251` | `#49c970` | `#00abcf` | `#3b5cf6` | `#7152f4` | `#f64aab` |
| 700 | `#5c5c5c` | `#d92e3c` | `#ee7330` | `#e7af36` | `#2eaf5a` | `#0096b8` | `#3250df` | `#623be2` | `#e4429e` |
| 800 | `#3a3a3a` | `#b82d35` | `#d16427` | `#cb9f34` | `#198b43` | `#007d9b` | `#2c47c8` | `#5230c2` | `#c83d8b` |
| 900 | `#2e2e2e` | `#97252b` | `#b35624` | `#ac8833` | `#1d783c` | `#006c85` | `#263fa9` | `#442aa1` | `#aa3576` |
| 1000 | `#242424` | `#7a1f23` | `#954c27` | `#8e7231` | `#196130` | `#005a6e` | `#22388f` | `#361f83` | `#8c2d61` |
| 1100 | `#161616` | `#5f1a1c` | `#723d22` | `#68552b` | `#164c26` | `#004756` | `#1c2e70` | `#2b1b6a` | `#6f284f` |
| 1200 | `#080808` | `#461516` | `#5a2c14` | `#4b4025` | `#14361d` | `#00353f` | `#1b2852` | `#221358` | `#5c1d3f` |

## Type scale

`text-base` stays at 16px, so inputs can use `text-base md:text-sm` to keep iOS from zooming in.

| Class      | Size        | Line height |
| ---------- | ----------- | ----------- |
| `text-2xs` | 11px        | 16px        |
| `text-xs`  | 12px        | 16px        |
| `text-sm`  | 13px (body) | 20px        |
| `text-md`  | 15px        | 20px        |
| `text-xl`  | 21px        | 28px        |
