대비비 (WCAG AA)
본문 텍스트 4.5:1, 큰 텍스트·UI 요소 3:1 이상.
*-foreground 짝을 전부 확인한다.
디자인 시스템 적용은 대부분 토큰 매핑이다
이 중 코드로 옮겨야 하는 것은 1과 2다. 그리고 1을 제대로 하면 2는 거의 따라온다 — 15장에서 봤듯 shadcn 컴포넌트에는 원시 색이 하나도 없기 때문이다.
| 시나리오 | 상황 | 난이도 |
|---|---|---|
| A. 브랜드 색만 입힌다 | 디자인 시스템은 없고 브랜드 컬러만 있다 | 30분 |
| B. 유명 디자인 시스템 채택 | Material 3, Radix Colors 등을 따른다 | 반나절 |
| C. 사내 디자인 시스템 매핑 | Figma에 정의된 사내 토큰이 있다 | 1~2주 |
셋 다 같은 파일 하나(globals.css)를 고치는 일이다. 차이는 매핑해야 할 토큰 개수뿐이다.
:root { --primary: oklch(0.55 0.22 264); /* 브랜드 컬러 */ --primary-foreground: oklch(0.99 0 0); /* 그 위의 글자 */ --ring: oklch(0.55 0.22 264); /* 포커스 링도 맞춘다 */ --radius: 0.5rem;}
.dark { --primary: oklch(0.70 0.18 264); /* 다크에선 밝게 */ --primary-foreground: oklch(0.20 0.02 264); --ring: oklch(0.70 0.18 264);}--primary · --primary-foreground · --ring 세 개면 앱의 인상이 바뀐다--radius까지 만지면 성격이 확 달라진다Material 3의 색 역할과 shadcn 토큰을 매핑한다.
| Material 3 | shadcn/ui | 비고 |
|---|---|---|
primary |
--primary |
그대로 |
onPrimary |
--primary-foreground |
on*이 곧 *-foreground |
surface |
--background |
|
onSurface |
--foreground |
|
surfaceContainer |
--card |
고도 단계가 여럿이라 하나를 고른다 |
secondaryContainer |
--secondary |
|
onSecondaryContainer |
--secondary-foreground |
|
outline |
--border |
|
outlineVariant |
--input |
|
error |
--destructive |
Material의 on* 접두사와 shadcn의 *-foreground 접미사는 같은 개념이다 —
“이 표면 위에 올라가는 것”. 이 대응만 알면 절반은 끝난다.
:root { --background: oklch(0.98 0.01 320); /* surface */ --foreground: oklch(0.22 0.01 300); /* onSurface */ --primary: oklch(0.47 0.10 295); /* #6750A4 */ --primary-foreground: oklch(1 0 0); /* onPrimary */ --secondary: oklch(0.90 0.04 305); /* secondaryContainer */ --border: oklch(0.82 0.02 300); /* outline */ --radius: 1.25rem; /* Material은 라운드가 크다 */}Material 3에는 shadcn 기본 컴포넌트에 없는 구조적 특징이 있다.
--radius로는 안 된다. rounded-full이 필요하다이럴 때 16장의 “variant 추가”가 정당한 수정이 된다.
ui/button.tsx의 베이스 클래스를 rounded-full로 바꾸는 것이다.
flowchart LR
D["디자인 시스템 채택"] --> T["① 토큰 매핑<br/>색 · radius · 폰트"]
T --> Q{"이걸로 80%가<br/>해결되는가"}
Q -->|"예 — 대부분"| OK["끝 ✅"]
Q -->|"구조가 다르다"| C["② ui/ 수정<br/>높이 · 패딩 · 알약 모양<br/>16장 규칙 준수"]
C --> OK
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class T key
class C warn
class OK ok
class D,Q mute
색 팔레트만 제공하는 시스템이다. 12단계 스케일에 의미가 정해져 있다.
| 단계 | 용도 |
|---|---|
| 1–2 | 앱 배경, 미묘한 배경 |
| 3–5 | 컴포넌트 배경 (기본 / 호버 / 눌림) |
| 6–8 | 테두리 (미묘 / 기본 / 강조) |
| 9–10 | 채도 높은 솔리드 배경 (기본 / 호버) |
| 11 | 저대비 텍스트 |
| 12 | 고대비 텍스트 |
:root { --background: var(--gray-1); --card: var(--gray-2); --muted: var(--gray-3); --border: var(--gray-6); --primary: var(--indigo-9); /* 9단계 = 솔리드 배경 */ --muted-foreground: var(--gray-11); --foreground: var(--gray-12);}gray-9는 양쪽에서 같은 역할grayA)이 함께 제공되어 겹칠 때 자연스럽다9번이 브랜드 색으로 쓰라고 만든 단계다.
가장 흔하고 가장 까다로운 시나리오다. 절차를 단계로 나눈다.
인벤토리 — 사내 DS의 토큰 목록을 뽑는다
Figma Variables를 CSV로 내보내면 빠르다.
매핑 표 작성 — 사내 토큰 → shadcn 토큰
1:1이 안 되는 것들을 표시한다. 아래에 예시가 있다.
갭 분석 — 사내에만 있는 것 / shadcn에만 있는 것을 나눈다
토큰 파일 작성 — globals.css에 light / dark / @theme inline 세 블록
컴포넌트 차이 반영 — 구조가 다른 것만 ui/에서 수정 (16장 규칙 준수)
검증 — 대비비, 다크 모드, 포커스 링을 전수 확인
| 사내 토큰 | shadcn 토큰 | 상태 | 메모 |
|---|---|---|---|
color.brand.primary |
--primary |
✅ 1:1 | |
color.brand.onPrimary |
--primary-foreground |
✅ 1:1 | |
color.surface.default |
--background |
✅ 1:1 | |
color.surface.raised |
--card |
✅ 1:1 | |
color.surface.sunken |
— | ⚠️ 없음 | 토큰 추가 필요 |
color.text.tertiary |
— | ⚠️ 없음 | muted-foreground로 통합? |
| — | --accent |
⚠️ 미정 | 호버 배경. surface.hover로? |
color.status.warning |
— | ⚠️ 없음 | 12장 방식으로 추가 |
radius.card = 12px |
--radius |
⚠️ 충돌 | 버튼은 6px인데 카드는 12px |
surface.sunken, status.warning 같은 것들.
토큰을 추가한다. 12장의 세 곳 수정(light / dark / @theme inline)을 그대로 한다.
--accent, --popover 같은 것들.
적당한 값을 할당한다. 비워두면 컴포넌트가 깨진다. 디자이너에게 “호버 배경색이 정의돼 있나요?“를 물어 결정한다.
radius가 컴포넌트마다 다른 경우.
파생 스케일을 쓰거나 컴포넌트별로 오버라이드한다.
:root { --radius: 0.375rem; } /* 버튼 기준 */@theme inline { --radius-lg: 0.75rem; /* 카드는 별도로 */}@import "tailwindcss";@custom-variant dark (&:where(.dark, .dark *));
/* ① 값 — 사내 DS에서 옮겨온 것 */:root { --background: oklch(1 0 0); --foreground: oklch(0.21 0.01 250); --primary: oklch(0.52 0.16 254); --primary-foreground: oklch(0.99 0 0); --warning: oklch(0.84 0.16 84); /* 사내 전용 추가 */ --warning-foreground: oklch(0.28 0.07 46); --radius: 0.375rem;}
.dark { /* ② 같은 이름, 다크 값 */ }
/* ③ 유틸리티 생성 */@theme inline { --color-background: var(--background); --color-primary: var(--primary); --color-warning: var(--warning); --radius-lg: calc(var(--radius) * 2);}먼저 토큰만으로 얼마나 가는지 확인한다. 대부분 80%는 토큰으로 해결된다.
남는 20%만 ui/ 수정 대상이고, 수정할 때는 16장의 규칙을 지킨다 —
표시 남기기, 커밋 분리.
| 요구 | 수정 위치 |
|---|---|
| 버튼 높이가 다르다 | button.tsx의 size variant |
| 그림자 대신 테두리 | card.tsx 베이스 클래스 |
| 포커스 링 두께·색 | button.tsx 등의 focus-visible: |
| 입력 필드 라벨 위치 | label.tsx + 폼 패턴 |
대비비 (WCAG AA)
본문 텍스트 4.5:1, 큰 텍스트·UI 요소 3:1 이상.
*-foreground 짝을 전부 확인한다.
다크 모드 전수 확인
.dark에서 값을 빠뜨린 토큰이 있으면
라이트 값이 그대로 남아 대비가 깨진다.
포커스 링
--ring이 배경과 충분히 대비되는가?
primary와 같은 색으로 뒀다면
primary 버튼 위에서 안 보인다.
다섯 화면 스냅샷
로그인·목록·폼·빈 상태·에러 — 라이트/다크로 캡처해 나란히 본다.
나란히 놓고 보면 다크에서만 깨지는 것이 바로 눈에 띈다. 배지 대비, 테이블 구분선, 흐린 텍스트가 단골이다.
shadcn apply <code>로 테마 프리셋을 적용한다pnpm dlx shadcn@latest apply a2r6bw --only theme브랜드가 여러 개거나 프로젝트가 여러 개면 테마 자체를 배포한다. (16장)
{ "name": "acme-theme", "type": "registry:theme", "title": "ACME Design System", "cssVars": { "light": { "primary": "oklch(0.52 0.16 254)", "primary-foreground": "oklch(0.99 0 0)", "warning": "oklch(0.84 0.16 84)" }, "dark": { "primary": "oklch(0.70 0.14 254)", "warning": "oklch(0.41 0.11 46)" } }}pnpm dlx shadcn@latest add @acme/acme-theme새 프로젝트가 명령 한 줄로 사내 디자인 시스템을 입는다.
[data-brand="acme"] { --primary: oklch(0.52 0.16 254);}[data-brand="globex"] { --primary: oklch(0.62 0.19 35);}// 테넌트에 따라 최상위에 붙인다<html data-brand={tenant.brand}>
// 또는 DB에서 받은 값을 인라인 스타일로 주입<html style={themeVars}> {/* { '--primary': tenant.color } */}bg-primary만 쓴다9장에서 “Tailwind가 안 맞는 경우”로 꼽았던 **“디자인 토큰이 런타임에 서버에서 내려온다”**가 이렇게 해결된다.
!important로 덮어쓰기 — 토큰을 안 고치고 스타일로 이기려는 시도cssVariables: false로 시작 — 14장에서 경고한 그것. 되돌리기가 매우 어렵다on*과 shadcn의 *-foreground는 같은 개념registry:theme으로 테마를 배포한다