# Theming

> How tokens connect, and how to make your own theme.

- URL: https://dsui.vercel.app/en/docs/theming

## How tokens work

No component writes values directly. Components read only token variables. The tokens live in one file, `components/arc/foundation.css`.

| What | Main tokens |
| --- | --- |
| Surfaces and text | `--background`, `--surface`, `--surface-raised`, `--surface-muted`, `--foreground`, `--text-secondary` |
| Accent and status | `--accent`, `--accent-strong`, `--accent-foreground`, `--success`, `--warning`, `--danger` |
| Lines | `--border`, `--border-subtle`, `--border-strong` |
| Corners | `--radius-control`, `--radius-panel`, `--radius-surface`, `--radius-pill` |
| Shadows | `--shadow-resting`, `--shadow-raised`, `--shadow-floating` |
| Type | `--font-display`, `--font-body` |
| Motion | `--duration-*`, `--ease-*` |

Light values are in `:root`, and dark values are in `:root[data-theme="dark"]` and `:root.dark`. So you can turn dark mode on with `<html data-theme="dark">`, or with `<html class="dark">` as shadcn does. With neither, it follows the operating system's dark mode setting.

### One accent colour changes every control

Some tokens have no value of their own. They are computed from other tokens. So changing only `--accent` and `--accent-foreground` also changes buttons, switches, checkboxes and the first chart series.

| Token | How it's computed |
| --- | --- |
| `--accent-strong` | The same as `--accent`. |
| `--accent-subtle` | `--accent` mixed with transparency (12% in light, 18% in dark). |
| `--accent-foreground-muted` | 50% of `--accent-foreground`. |
| `--control-on`, `--control-glyph` | `--accent` and `--accent-foreground` in light. Dark uses separate values so white marks stay visible. |
| `--series-1` | `--accent`. |
| `--highlight-subtle`, `--glow` | Computed from `--highlight`. |

### One set of token names

Components and blocks read token names such as `--accent` and `--surface`. New screens and new themes use these names too.

## base and the seasonal themes

| Theme | Description |
| --- | --- |
| [base](/en/themes#base) | The neutral token set every theme builds on. It also holds the reference values for type, corners and shadows. |
| [halloween](/en/themes#halloween) | A pumpkin-orange accent, a violet highlight and a deep violet night for dark mode. Springs are bouncy, and the notification and success icons become a ghost and a pumpkin. |
| [seollal](/en/themes#seollal) | A vermilion accent and a gold highlight on warm hanji-paper surfaces. Motion is brisk, and the notification and success icons become a lucky pouch and a shield kite. |
| [chuseok](/en/themes#chuseok) | A harvest-moon gold accent, a persimmon-orange highlight and a night-sky navy for dark mode. Motion is unhurried, and the notification and success icons become a songpyeon rice cake and a persimmon. |
| [christmas](/en/themes#christmas) | A crimson accent and a pine-green highlight on snow-white surfaces. Motion is smooth, and the notification and success icons become a bell and a star. |

The seasonal themes change colour, motion, some icons, some sounds and the heading font.

- **Heading font:** Halloween uses Jejudoldam, Seollal uses Binggrae II, Chuseok uses Griun Oksooni handwriting and Christmas uses Nexon Maplestory. They apply to headings (`display`) only; body text stays Pretendard. A theme points at an alias such as `"Halloween Display"`, and `@font-face` in `app/globals.css` connects the alias to the real file, so swapping a font means changing only that `src`. In an installed project, load a font under the same name yourself. Use the font files exactly as received and check each font's own license (`public/fonts/season/SEASON-FONTS.txt`).
- **Sounds:** only the five roles `click`, `success`, `error`, `notify` and `confirm` change; every other role keeps its default sound. The sounds are synthesized by `scripts/gen-season-sounds.ts`, so there is no license to carry. `pnpm sounds:season` rebuilds them, and to use them in an installed project, copy `public/sounds/<theme name>/` along with the theme.
- The themes page shows each heading font, the icons a theme changes and a "Hear the sounds" button.

`base` holds only the reference values for type, corners and shadows. It holds no colour tokens. The token defaults are in `foundation.css`, so any token a theme leaves out keeps that value.

On the [themes page](/en/themes) you can see each theme drawn on a button, input, badge, switch and card. The previews follow the site's light and dark switch. The "Theme preview" on item pages changes the demo the same way.

## Installing a theme

A theme is a `registry:theme` item, so instead of files it adds CSS rules to your global stylesheet.

```bash
npx shadcn@latest add https://dsui.vercel.app/r/halloween.json
```

The CLI writes the parent theme `base` first, then `halloween`, into your global stylesheet (such as `app/globals.css`). Each theme adds only the values it changes.

| Selector | Values |
| --- | --- |
| `:root` | Light values, and values that are the same in both modes (corners, type) |
| `:root[data-theme="dark"]`, `:root.dark` | Dark values |
| `:root:not(…)` inside `@media (prefers-color-scheme: dark)` | Dark values (when following the operating system) |

The selectors are the same as in `foundation.css`. At equal specificity the later rule wins, so **import `foundation.css` at the very top of your global stylesheet.** Then the theme rules come after it and override the defaults.

```css
@import "../components/arc/foundation.css";
@import "tailwindcss";
/* … CLI가 테마 규칙을 이 아래에 써 넣어요 … */
```

Dark mode gets the theme's dark values whether you turn it on with `<html data-theme="dark">` or `<html class="dark">`.

A theme's type, corners and shadows (`typography`, `shape`, `elevation`) are CSS variables too, so they go into the same rules. Icons, sounds and motion can't be moved into CSS, so they stay only in the theme JSON's `meta.themeRoles`. See "Icons, sounds and motion are called by role" below and pass them to `ThemeAssetsProvider`.

## Making your own theme

### Change colours in your project

The quickest way is to set the tokens again in your own global stylesheet. Written after `foundation.css`, they override it. Computed tokens (`--accent-subtle`, `--control-on` and others) follow without being listed.

```css
:root {
  --accent: oklch(58% .17 165);
  --accent-foreground: oklch(100% 0 0);
  --radius-control: .75rem;
}

:root[data-theme="dark"],
:root.dark {
  --accent: oklch(72% .15 165);
  --accent-foreground: oklch(20% .04 165);
}
```

When you change the accent, always check its contrast with `--accent-foreground`. Text on the accent needs at least 4.5:1 to be readable. For corners, change only the three steps `--radius-control` (buttons, inputs), `--radius-panel` (cards, panels) and `--radius-surface` (large surfaces), and leave the pill shape `--radius-pill` as it is.

### Make a theme item

A dsui theme is defined by one file, `registry/theme/<name>/item.json`. In the repository, these commands create a starter file.

```bash
pnpm theme:new forest
pnpm theme:new forest --extends base
```

The name and summary placeholders, `extends`, and the main light and dark tokens (surfaces, text, lines, accent, highlight, controls) are filled in with the foundation defaults. If a theme with the same name exists, it stops instead of overwriting it. Inheritance goes one level deep, so `--extends` accepts only a theme without a parent. After changing values, validate with `pnpm gen` and check contrast with `pnpm test`.

### The shape of a theme item

Pick a parent and list only what you want to change.

```json
{
  "slug": "my-theme",
  "type": "theme",
  "title": { "ko": "내 테마", "en": "My theme" },
  "summary": { "ko": "초록 강조색과 작은 모서리의 테마" },
  "extends": "base",
  "tokens": {
    "light": { "--accent": "oklch(58% .14 160)", "--accent-foreground": "oklch(100% 0 0)" },
    "dark": { "--accent": "oklch(78% .13 160)", "--accent-foreground": "oklch(20% .04 160)" }
  },
  "sounds": { "click": "tick-001" },
  "motion": { "duration": { "fast": 0.2 } },
  "typography": { "family": { "display": "'Pretendard Variable', sans-serif" } },
  "shape": { "control": "8px", "panel": "14px" },
  "elevation": { "floating": "0 24px 60px oklch(0% 0 0 / .12)" }
}
```

| Field | How to use it |
| --- | --- |
| `extends` | The theme to inherit from. Without it, the theme inherits `base`. |
| `tokens` | The `light` and `dark` tokens. Accepts the tokens below. Tokens you leave out use the `foundation.css` defaults. Values can't contain `;`, braces, backslashes, comments or `url()`. |
| `icons` | List only the role icons you change, out of 34. SVG paths (`paths`) on a 24px grid, optionally with `strokeWidth`, `fill` and `viewBox`. |
| `sounds` | List only the role sounds you change, out of 11. Accepts a bundled sound name, a path starting with `/`, an `https://` URL or a `data:audio/` URI. |
| `motion` | `duration` (seconds), `ease` (four cubic-bezier numbers) and `spring`. You can set only some of them. |
| `typography` | `family`, `tracking` and `leading` for `display` (headings) and `body` (body text). |
| `shape` | Corner sizes for `control`, `panel` and `surface`. |
| `elevation` | The `resting`, `raised` and `floating` shadows. A single string applies to light and dark alike. `{ "light": …, "dark": … }` sets them separately. |

This list is every token a theme can use (`lib/theme/arc-tokens.ts`). Validation filters out any other name.

| Group | Tokens |
| --- | --- |
| Surfaces | `--background`, `--surface`, `--surface-raised`, `--surface-muted`, `--surface-sunken`, `--inverse`, `--inverse-foreground` |
| Text | `--foreground`, `--text-secondary`, `--text-muted` |
| Lines | `--border`, `--border-subtle`, `--border-strong` |
| Accent | `--accent`, `--accent-strong`, `--accent-subtle`, `--accent-foreground`, `--accent-foreground-muted` |
| Highlight | `--highlight`, `--highlight-strong`, `--highlight-subtle`, `--highlight-foreground`, `--glow` |
| Controls | `--control-on`, `--control-glyph`, `--control-track`, `--control-track-hover`, `--control-thumb`, `--control-thumb-on` |
| Status | `--success`, `--warning`, `--danger` |
| Charts | `--series-1` to `--series-4` |
| Corners | `--radius-control`, `--radius-panel`, `--radius-surface`, `--radius-pill` (the same in both modes, so set them in `light` only) |
| Shadows | `--shadow-resting`, `--shadow-raised`, `--shadow-floating` |

Tests check text and background contrast for every theme. Text on the accent (`--accent-foreground`), body text (`--foreground`) and secondary text (`--text-secondary`) on surfaces, and text on the highlight need 4.5:1. The mark on an active control (`--control-glyph`) needs at least 3:1. To match `--control-on` to the accent in dark mode, you can point at other tokens, such as `"--control-on": "var(--accent)"` and `"--control-glyph": "var(--accent-foreground)"`.

Inheritance happens one role at a time. Change a single icon and the rest come from the parent. Type, corner and shadow values go into the stylesheet as they are, so they don't accept `;`, braces, backslashes, comments or `url()`.

Adding a theme to the dsui registry happens inside the repository. If your team needs its own theme, ask the devs7udio studio.

## Icons, sounds and motion are called by role

Components don't pick icon files or sound files directly. They call a role name, and the current theme decides what to use.

```tsx
import { UiIcon } from "@/components/arc/theme";
import { playUiSound } from "@/components/arc/sound";

<UiIcon iconRole="close" size={16} aria-hidden="true" />;
playUiSound("click");
```

| Kind | Roles |
| --- | --- |
| Icons (34) | `close`, `check`, `chevron-*`, `arrow-*`, `search`, `plus`, `minus`, `more`, `delete`, `calendar`, `clock`, `info`, `success`, `warning`, `error`, `alert`, `copy`, `link`, `play`, `pause`, `reset`, `retry`, `loading`, `notification`, `mail`, `file`, `theme-light`, `theme-dark` |
| Sounds (11) | `click`, `tick`, `select`, `toggle-on`, `toggle-off`, `open`, `close`, `success`, `error`, `notify`, `confirm` |
| Motion | Durations `instant`, `fast`, `exit`, `standard`, `considered` / curves `enter`, `exit`, `standard`, `inOut` / springs `responsive`, `gentle`, `snappy`, `smooth`, `morph` |

A theme's icons, sounds, motion, type, corners and shadows are applied with `ThemeAssetsProvider`. One per page is enough.

```tsx
import { ThemeAssetsProvider } from "@/components/arc/theme";

const assets = {
  sounds: { click: "tick-001" },
  shape: { control: "8px", panel: "14px" },
};

<ThemeAssetsProvider assets={assets}>{children}</ThemeAssetsProvider>;
```

An installed project doesn't merge with the parent theme. Pass an object that already includes the inherited values. The installed theme JSON's `meta.themeRoles` holds the values that theme uses. The CLI already added type, corners and shadows as CSS, so you can pass only icons, sounds and motion to the provider.

## Known limits

- **The CLI can't add icons, sounds or motion.** shadcn has no slot for them, so they stay only in `meta.themeRoles`. They apply only when you pass them to `ThemeAssetsProvider` yourself.
- **Layers that float above the page don't follow the preview theme.** Dialogs, menus and toasts are drawn in `body`, outside the preview area's theme (`data-dsui-theme`, the demo's `data-theme`). In an installed project they go under `:root`, so this is not a problem.
- **Names overlap with the shadcn init variables.** The `:root` that `shadcn init` creates also has `--background`, `--foreground`, `--accent`, `--accent-foreground` and `--border`. That block comes after `foundation.css`, so without a theme the light accent becomes shadcn's grey. Installing a theme overrides them with the theme's values. Without a theme, delete those five lines.
- **Motion values held by modules change late.** If you switch themes while a page is open, some transitions that captured their values on first load change on the next load.
- **Springs have no CSS variables.** Durations and curves change through CSS variables right away, but springs exist only in scripts.
- **Sample icons in blocks stay the same.** A theme changes only the 34 shared interface icons.
- **Themes don't ship font files.** You can use only font names the page already loads with `@font-face`.
- **Turning sound on and the volume are the user's choice.** Themes don't change them.
- **A token editor and theme export are in the works.**

The seasonal themes also draw their own illustrations. An illustration is a list of shapes, written under `illustrations` in `item.json`; any of the 17 a theme does not draw is the base picture in the theme's colours. Christmas has a tree and a wreath, Chuseok a harvest moon and hills, Halloween pumpkins and ghosts, Seollal a striped fan and patchwork squares.
