원문: https://github.com/facebook/astryx/wiki/Theming-Infrastructure · 번역 기준: 2026-09-03

Astryx 테마 시스템은 하나의 원칙 위에 세워져 있습니다: theme config는 디자인 시스템을 코드로 명문화한 표현이다. defineTheme() 함수는 선언적 config를 받아 두 가지 서로 다른 경로에서 소비 가능한 산출물을 만들어 냅니다.

관련 문서는 다음을 참고하세요:

⚠️ 동기화를 유지하세요. Night Watch Component Auditor는 테마 컨벤션을 자동으로 enforcement합니다. 여기의 규칙을 업데이트할 때는 대응하는 auditor 체크도 업데이트하세요. auditor가 여기에 문서화되지 않은 공백을 발견하면 여기에 추가하세요.

아키텍처 개요

                    defineTheme() input
              ┌───────────────────────────────┐
              │  name: 'ocean'                │
              │  color: { accent: '#0077B6' } │
              │  typography: { scale, body }  │
              │  motion: { fast, medium }     │
              │  tokens: { --color-*, ... }   │
              │  components: { button: {...} }│
              │  fonts: [{ family, url }]     │
              │  icons: { ... }               │
              └──────────┬────────────────────┘
                         │
              ┌──────────▼────────────────────┐
              │   Higher-Order Transforms     │
              │                               │
              │  color ───────► accent, bg,   │
              │                surface, border│
              │                text tokens    │
              │  typography ──► text/heading  │
              │                size tokens    │
              │  motion ──────► duration      │
              │                min/max tokens │
              │                               │
              │  Explicit tokens override     │
              │  generated values (highest    │
              │  precedence)                  │
              └──────────┬────────────────────┘
                         │
              ┌──────────▼────────────────────┐
              │     DefinedTheme           │
              │                               │
              │  name: string                 │
              │  tokens: Record<string,string>│
              │  components: ComponentStyles  │
              │  fonts: ThemeFontSource[]     │
              │  icons: IconRegistry          │
              │  css: string (generated)      │
              └──────────┬────────────────────┘
                         │
           ┌─────────────┴───────────────┐
           │                             │
┌──────────▼───────────┐     ┌───────────▼───────────┐
│  Runtime Path        │     │  Built Path           │
│                      │     │                       │
│  <Theme           │     │  astryx theme build   │
│    theme={ocean}>    │     │    → ocean.css        │
│                      │     │    → ocean.js         │
│  Injects <style> at  │     │    → ocean.d.ts       │
│  runtime with CSS    │     │                       │
│  custom properties   │     │  Static CSS file with │
│  on :scope           │     │  all tokens + @scope  │
│                      │     │  component overrides + │
│  Good for: dev,      │     │  variant type defs    │
│  dynamic themes,     │     │                       │
│  theme switching     │     │  Good for: production,│
└──────────────────────┘     │  CDN delivery, SSR,   │
                             │  zero runtime cost    │
                             └───────────────────────┘

두 경로 모두 동일한 CSS custom property를 만들어 냅니다. 컴포넌트는 어느 경로가 token을 전달했는지 알지도, 신경 쓰지도 않습니다.

Token 레이어

그룹 Prefix 예시
Color --color-* --color-accent, --color-text-primary, --color-border
Spacing --spacing-* --spacing-1부터 --spacing-12까지
Size --size-* --size-element-sm, --size-element-md, --size-element-lg
Radius --radius-* --radius-none, --radius-inner, --radius-element, --radius-container, --radius-page, --radius-full
Shadow --shadow-* --shadow-low, --shadow-med, --shadow-high, --shadow-inset-*
Duration --duration-* --duration-fast, --duration-medium, --duration-slow (각각 -min/-max 포함)
Ease --ease-* --ease-standard
Typography --font-*, --text-* --font-family-body, --font-size-base, --text-body-size, --text-body-leading

모든 color token은 자동 다크 모드를 위해 light-dark()를 사용합니다:

'--color-accent': 'light-dark(#0064E0, #2694FE)'

Neutral Gray Token

"회색" 배경을 담당하는 token은 네 가지입니다. 어떤 것을 골라야 하는지는 원하는 색조가 아니라 그 엘리먼트가 무엇인지에 따라 결정됩니다:

Token 역할 사용처
--color-neutral 자기완결적(self-contained) 엘리먼트를 위한 neutral fill 버튼(secondary), 배지, token, kbd, 아바타 fallback, 페이지네이션 아이템, segmented control, 선택된 내비게이션 아이템, 채팅 버블, 썸네일
--color-background-muted 내부에 콘텐츠를 담는 컨테이너의 배경 섹션, 카드, 코드 블록, 테이블 zebra stripe, 비활성 input fill, progress/slider fill track, 리스트 row
--color-track 본문 휘도(luminance)에 대비되어 읽히는 얇은 채널 affordance Slider rail, Spinner track
--color-overlay-hover 반투명 hover/active 피드백 ghost 버튼의 hover, 리스트/메뉴 아이템 하이라이트, 테이블 row hover. 항상 @media (hover: hover) 또는 keyboard-highlight state를 통해 적용됩니다. --color-overlay-pressed는 그 active-state 쌍둥이입니다

결정 트리:

  1. hover, active, 또는 keyboard-highlighted state인가요? → --color-overlay-hover (active일 때는 --color-overlay-pressed)
  2. 얇은 track/rail(slider, spinner)인가요? → --color-track
  3. 다른 콘텐츠(텍스트, 아이콘, 컨트롤)를 내부에 담는 컨테이너인가요? → --color-background-muted