Skip to content

Theming

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

How tokens work

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

WhatMain 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.

TokenHow it's computed
--accent-strongThe same as --accent.
--accent-subtle--accent mixed with transparency (12% in light, 18% in dark).
--accent-foreground-muted50% 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, --glowComputed 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

ThemeDescription
baseThe neutral token set every theme builds on. It also holds the reference values for type, corners and shadows.
halloweenA 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.
seollalA 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.
chuseokA 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.
christmasA 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 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.

Terminalbash
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.

SelectorValues
:rootLight values, and values that are the same in both modes (corners, type)
:root[data-theme="dark"], :root.darkDark 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.

Source codecss
@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.

Source codecss
: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.

Terminalbash
npm run theme:new forest
npm run 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.

Source codejson
{
  "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)" }
}
FieldHow to use it
extendsThe theme to inherit from. Without it, the theme inherits base.
tokensThe 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().
iconsList only the role icons you change, out of 34. SVG paths (paths) on a 24px grid, optionally with strokeWidth, fill and viewBox.
soundsList 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.
motionduration (seconds), ease (four cubic-bezier numbers) and spring. You can set only some of them.
typographyfamily, tracking and leading for display (headings) and body (body text).
shapeCorner sizes for control, panel and surface.
elevationThe 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.

GroupTokens
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.

Source codetsx
import { UiIcon } from "@/components/arc/theme";
import { playUiSound } from "@/components/arc/sound";

<UiIcon iconRole="close" size={16} aria-hidden="true" />;
playUiSound("click");
KindRoles
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
MotionDurations 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.

Source codetsx
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.