본문으로 건너뛰기

테마 만들기

토큰이 어떻게 이어지고, 내 테마를 어떻게 만드는지 알려 드려요.

토큰은 이렇게 동작해요

모든 컴포넌트는 값을 직접 쓰지 않고 토큰 변수만 읽어요. 토큰은 components/arc/foundation.css 한 파일에 있어요.

무엇대표 토큰
면과 글자--background, --surface, --surface-raised, --surface-muted, --foreground, --text-secondary
강조와 상태--accent, --accent-strong, --accent-foreground, --success, --warning, --danger
선--border, --border-subtle, --border-strong
모서리--radius-control, --radius-panel, --radius-surface, --radius-pill
그림자--shadow-resting, --shadow-raised, --shadow-floating
서체--font-display, --font-body
모션--duration-*, --ease-*

라이트 값은 :root에, 다크 값은 :root[data-theme="dark"]와 :root.dark에 있어요. 그래서 <html data-theme="dark">로 켜도 되고, shadcn처럼 <html class="dark">로 켜도 돼요. 둘 다 없으면 운영체제의 다크 모드 설정을 따라가요.

강조색 하나로 컨트롤 전체가 바뀌어요

몇몇 토큰은 값을 따로 갖지 않고 다른 토큰에서 계산해요. 그래서 --accent와 --accent-foreground만 바꿔도 버튼, 스위치, 체크박스, 차트 첫 계열이 함께 바뀌어요.

토큰계산하는 법
--accent-strong--accent 그대로예요.
--accent-subtle--accent를 투명하게 섞어요(라이트 12%, 다크 18%).
--accent-foreground-muted--accent-foreground의 50%예요.
--control-on, --control-glyph라이트에서는 --accent, --accent-foreground예요. 다크에서는 흰 표시가 잘 보이도록 따로 정한 값을 써요.
--series-1--accent예요.
--highlight-subtle, --glow--highlight에서 계산해요.

토큰 이름은 하나예요

컴포넌트와 블록은 --accent, --surface 같은 토큰 이름을 읽어요. 새 화면과 새 테마도 이 이름을 써요.

base와 시즌 테마

테마설명
base모든 테마의 기준이 되는 중립 토큰 세트예요. 서체, 모서리, 그림자의 기준값도 여기 있어요.
halloween호박 주황 강조색과 보라 하이라이트, 깊은 보랏빛 밤의 다크 모드예요. 탄력 있는 스프링을 쓰고 알림과 성공 아이콘이 유령과 호박이 돼요.
seollal한지 같은 따뜻한 바탕에 다홍 강조색과 금빛 하이라이트를 올린 테마예요. 움직임이 경쾌하고 알림과 성공 아이콘이 복주머니와 방패연이 돼요.
chuseok보름달 금빛 강조색과 감 주황 하이라이트, 밤하늘 남색 다크 모드예요. 움직임이 느긋하고 알림과 성공 아이콘이 송편과 감이 돼요.
christmas진홍 강조색과 소나무 초록 하이라이트를 눈처럼 흰 바탕에 올린 테마예요. 움직임이 부드럽고 알림과 성공 아이콘이 종과 별이 돼요.

시즌 테마는 색과 모션, 아이콘 일부, 소리 일부, 제목 글꼴을 바꿔요.

  • 제목 글꼴: 할로윈은 제주돌담체, 설날은 빙그레체II, 추석은 그리운 옥수니 손글씨, 크리스마스는 넥슨 메이플스토리예요. 제목(display)에만 쓰고 본문은 Pretendard 그대로예요. 테마는 "Halloween Display"처럼 별칭만 가리키고, 실제 글꼴 파일은 app/globals.css의 @font-face가 연결해요. 글꼴을 바꾸려면 그 src만 고치면 돼요. 설치한 프로젝트에서는 같은 이름으로 글꼴을 직접 불러 주세요. 글꼴 파일은 고치지 않고 받은 그대로 쓰고, 라이선스는 각 글꼴의 것을 확인해 주세요(public/fonts/season/SEASON-FONTS.txt).

  • 소리: click, success, error, notify, confirm 다섯 역할만 시즌 소리로 바꾸고 나머지는 기본 소리를 써요. 소리는 scripts/gen-season-sounds.ts가 직접 합성한 것이라 따로 지킬 라이선스가 없어요. pnpm sounds:season으로 다시 만들 수 있고, 설치한 프로젝트에서 쓰려면 public/sounds/<테마 이름>/ 폴더를 함께 옮겨 주세요. 시즌 테마는 일러스트도 따로 그렸어요. 일러스트는 도형 목록이라 item.json의 illustrations에 쓰고, 17장 중 그리지 않은 그림은 base 그림을 그 테마의 색으로 써요. 크리스마스는 전나무와 화환, 추석은 보름달과 언덕, 할로윈은 호박과 유령, 설날은 색동 부채와 보자기 조각이에요.

  • 테마 페이지에서 제목 글꼴, 바꾼 아이콘, "소리 들어 보기" 버튼을 볼 수 있어요.

base는 서체, 모서리, 그림자의 기준값만 담고 색 토큰은 담지 않아요. 토큰의 기본값은 foundation.css에 있어서, 테마가 적지 않은 토큰은 그 값을 그대로 써요.

테마 페이지에서 테마마다 버튼, 입력, 배지, 스위치, 카드를 그 테마로 그린 미리보기를 볼 수 있어요. 미리보기는 사이트의 라이트와 다크 전환을 따라가요. 항목 상세 페이지의 "테마 미리보기"도 같은 방식으로 데모를 바꿔요.

테마 설치하기

테마는 registry:theme 항목이라서 파일 대신 전역 스타일시트에 CSS 규칙을 넣어요.

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

CLI는 부모 테마인 base를 먼저, 그다음 halloween을 전역 스타일시트(app/globals.css 등)에 써 넣어요. 테마마다 자기가 바꾼 값만 들어가요.

선택자들어가는 값
:root라이트 값, 그리고 두 모드가 같은 값(모서리, 서체)
:root[data-theme="dark"], :root.dark다크 값
@media (prefers-color-scheme: dark) 안의 :root:not(…)다크 값(운영체제 설정을 따를 때)

선택자는 foundation.css와 같아요. 같은 우선순위에서는 나중에 오는 규칙이 이기니까 foundation.css를 전역 스타일시트 맨 위에서 불러와요. 그러면 테마 규칙이 그 뒤에 쓰여서 기본값을 덮어요.

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

다크 모드는 <html data-theme="dark">와 <html class="dark"> 어느 쪽으로 켜도 테마의 다크 값이 들어가요.

테마의 서체, 모서리, 그림자(typography, shape, elevation)도 CSS 변수라서 같은 규칙에 함께 들어가요. 아이콘, 사운드, 모션은 CSS로 옮길 수 없어서 테마 JSON의 meta.themeRoles에만 담겨요. 아래 "아이콘, 사운드, 모션은 역할로 불러요"를 보고 ThemeAssetsProvider로 넘겨 주세요.

내 테마 만들기

내 프로젝트에서 색 바꾸기

가장 빠른 방법은 내 전역 스타일시트에서 토큰을 다시 정하는 거예요. foundation.css 다음에 쓰면 덮어써요. 계산되는 토큰(--accent-subtle, --control-on 등)은 따로 적지 않아도 따라와요.

소스 코드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);
}

강조색을 바꾸면 --accent-foreground와의 대비를 꼭 확인해요. 강조색 위의 글자는 4.5:1 이상이어야 읽혀요. 모서리는 --radius-control(버튼, 입력), --radius-panel(카드, 패널), --radius-surface(큰 면) 세 단계만 바꾸고, 알약 모양 --radius-pill은 그대로 둬요.

테마 항목 만들기

dsui의 테마는 registry/theme/<이름>/item.json 파일 하나로 정해요. 저장소에서 아래 명령을 쓰면 시작 파일을 만들어 줘요.

터미널bash
npm run theme:new forest
npm run theme:new forest --extends base

이름과 요약 자리, extends, 그리고 라이트와 다크의 주요 토큰(면, 글자, 선, 강조색, 하이라이트, 컨트롤)이 기본값으로 채워져요. 같은 이름의 테마가 있으면 덮어쓰지 않고 멈춰요. 상속은 한 단계까지라서 --extends에는 부모가 없는 테마만 쓸 수 있어요. 값을 바꾼 뒤 pnpm gen으로 검사하고 pnpm test로 대비를 확인해요.

테마 항목의 모양

부모를 고르고 바꾸고 싶은 것만 써요.

소스 코드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)" }
}
필드쓰는 법
extends물려받을 테마예요. 없으면 base를 물려받아요.
tokenslight와 dark 토큰이에요. 아래 토큰을 받아요. 적지 않은 토큰은 foundation.css 기본값을 써요. 값에는 ;, 중괄호, 역슬래시, 주석, url()을 쓸 수 없어요.
icons역할 아이콘 34개 중 바꿀 것만 써요. 24px 격자의 SVG 경로(paths)에 strokeWidth, fill, viewBox를 더할 수 있어요.
sounds역할 소리 11개 중 바꿀 것만 써요. 번들 소리 이름, /로 시작하는 경로, https:// 주소, data:audio/ URI를 받아요.
motionduration(초), ease(cubic-bezier 네 수), spring을 일부만 써도 돼요.
typographydisplay(제목)와 body(본문)의 family, tracking, leading이에요.
shapecontrol, panel, surface의 모서리 크기예요.
elevationresting, raised, floating 그림자예요. 문자열 하나면 라이트와 다크에 같이 쓰고, { "light": …, "dark": … }면 따로 써요.

테마가 쓸 수 있는 토큰은 이 목록이 전부예요(lib/theme/arc-tokens.ts). 다른 이름은 검사에서 걸러져요.

묶음토큰
면--background, --surface, --surface-raised, --surface-muted, --surface-sunken, --inverse, --inverse-foreground
글자--foreground, --text-secondary, --text-muted
선--border, --border-subtle, --border-strong
강조--accent, --accent-strong, --accent-subtle, --accent-foreground, --accent-foreground-muted
하이라이트--highlight, --highlight-strong, --highlight-subtle, --highlight-foreground, --glow
컨트롤--control-on, --control-glyph, --control-track, --control-track-hover, --control-thumb, --control-thumb-on
상태--success, --warning, --danger
차트--series-1 ~ --series-4
모서리--radius-control, --radius-panel, --radius-surface, --radius-pill (두 모드가 같은 값이라 light에만 써요)
그림자--shadow-resting, --shadow-raised, --shadow-floating

글자와 배경의 대비는 테스트가 테마마다 확인해요. 강조색 위 글자(--accent-foreground), 면 위 본문(--foreground)과 보조 글자(--text-secondary), 하이라이트 위 글자는 4.5:1, 켜진 컨트롤 위 표시(--control-glyph)는 3:1 이상이어야 해요. 다크에서 --control-on을 강조색으로 맞추고 싶으면 "--control-on": "var(--accent)", "--control-glyph": "var(--accent-foreground)"처럼 다른 토큰을 가리켜도 돼요.

상속은 역할 하나씩 따로 일어나요. 아이콘 하나만 바꾸면 나머지 아이콘은 부모 것을 그대로 써요. 서체, 모서리, 그림자 값은 스타일시트에 그대로 들어가기 때문에 ;, 중괄호, 역슬래시, 주석, url()은 받지 않아요.

dsui 레지스트리에 테마를 올리는 일은 저장소 안에서 해요. 우리 팀만의 테마가 필요하면 devs7udio 스튜디오에 알려 주세요.

아이콘, 사운드, 모션은 역할로 불러요

컴포넌트는 아이콘 파일이나 소리 파일을 직접 고르지 않아요. 역할 이름을 부르면 지금 테마가 무엇을 쓸지 정해요.

소스 코드tsx
import { UiIcon } from "@/components/arc/theme";
import { playUiSound } from "@/components/arc/sound";

<UiIcon iconRole="close" size={16} aria-hidden="true" />;
playUiSound("click");
종류역할
아이콘 (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
사운드 (11개)click, tick, select, toggle-on, toggle-off, open, close, success, error, notify, confirm
모션길이 instant, fast, exit, standard, considered / 곡선 enter, exit, standard, inOut / 스프링 responsive, gentle, snappy, smooth, morph

테마의 아이콘, 사운드, 모션, 서체, 모서리, 그림자는 ThemeAssetsProvider로 적용해요. 페이지에 하나만 두면 돼요.

소스 코드tsx
import { ThemeAssetsProvider } from "@/components/arc/theme";

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

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

설치한 프로젝트에서는 부모 테마와 합치는 과정이 없어요. 물려받을 값까지 합친 객체를 넘겨 주세요. 설치한 테마 JSON의 meta.themeRoles에 그 테마가 쓴 값이 들어 있어요. 서체, 모서리, 그림자는 CLI가 이미 CSS로 넣었으니, 제공자에는 아이콘, 사운드, 모션만 넘겨도 돼요.

아직 못 하는 것

  • CLI는 아이콘, 사운드, 모션을 넣지 못해요. shadcn에 맞는 칸이 없어서 meta.themeRoles에만 담겨요. ThemeAssetsProvider로 직접 넘겨야 적용돼요.
  • 화면 위에 뜨는 층은 미리보기 테마를 따르지 않아요. 대화상자, 메뉴, 토스트는 body에 그려져서 미리보기 영역(data-dsui-theme, 데모의 data-theme)의 테마 밖에 있어요. 설치한 프로젝트에서는 :root에 들어가니 문제없어요.
  • shadcn init의 변수와 이름이 겹쳐요. shadcn init이 만든 :root에도 --background, --foreground, --accent, --accent-foreground, --border가 있어요. 이 블록이 foundation.css보다 뒤에 있어서, 테마 없이 쓰면 라이트의 강조색이 shadcn의 회색이 돼요. 테마를 설치하면 테마 값으로 덮이고, 테마 없이 쓸 때는 그 다섯 줄을 지워 주세요.
  • 모듈에 붙잡힌 모션 값은 늦게 바뀌어요. 페이지가 열려 있는 동안 테마를 바꾸면, 처음 불러올 때 값을 붙잡아 둔 일부 전환은 다음 로드에서 바뀌어요.
  • 스프링은 CSS 변수가 없어요. 길이와 곡선은 CSS 변수로 바로 바뀌지만 스프링은 스크립트에만 있어요.
  • 블록의 예시 아이콘은 그대로예요. 테마는 인터페이스 공통 아이콘 34개만 바꿔요.
  • 서체 파일은 테마가 싣지 않아요. 페이지가 @font-face로 미리 불러 둔 서체 이름만 쓸 수 있어요.
  • 사운드 켜기와 음량은 사용자의 선택이에요. 테마가 바꾸지 않아요.
  • 토큰 편집기와 테마 내보내기는 준비 중이에요.