4. 색 리소스
색 토큰은 하나씩이 아니라 짝으로 만든다. 이유가 명확하다.
이 장에서 처음 나오는 말5개
foreground- "배경 위에 올라가는 것" — 주로 글자색. shadcn/ui는 배경색 토큰마다 짝이 되는
-foreground를 둔다. 대비Contrast Ratio- 배경과 글자의 밝기 차이. 낮으면 안 읽힌다. 접근성 기준(WCAG)은 본문 4.5:1 이상을 요구한다.
oklchOklab Lightness Chroma Hue- 색을 지각 밝기·채도·색상 세 숫자로 적는 CSS 색 표기법. 밝기 단계를 hex·HSL보다 예측 가능하게 조절하기 쉽다.
WCAGWeb Content Accessibility Guidelines- 웹 접근성 국제 표준. 색 대비 기준의 출처.
baseColor- shadcn init 때 고르는 중립색(회색) 계열. 화면의 90%를 차지한다.
짝으로 만든다 — background / foreground
섹션 제목: “짝으로 만든다 — background / foreground”shadcn/ui의 색 토큰에는 규칙이 하나 있다. 배경색을 정하면 그 위의 글자색도 같이 정한다.
--primary: oklch(0.55 0.22 264); /* 바탕 */--primary-foreground: oklch(0.99 0 0); /* 그 위의 글자 */<button className="bg-primary text-primary-foreground">저장</button>이 둘은 항상 같이 쓴다. 실물로 보면 —
bg-primarytext-primary-foregroundbg-secondarytext-secondary-foregroundbg-mutedtext-muted-foregroundbg-destructivetext-white왜 짝이어야 하는가
섹션 제목: “왜 짝이어야 하는가”짝이 아니면 이런 일이 생긴다.
<button className="bg-primary text-white">저장</button>라이트 모드에서 --primary가 진한 남색이라 흰 글자가 잘 보인다. 그런데 —
- 다크 모드에서
--primary를 밝은 색으로 뒤집으면 → 흰 배경에 흰 글자 - 브랜드 색을 노란색으로 바꾸면 → 노란 배경에 흰 글자
- 글자색이 배경 변경을 따라오지 않는다
<button className="bg-primary text-primary-foreground">저장</button>:root { --primary: oklch(0.25 0.05 264); --primary-foreground: oklch(0.99 0 0); }.dark { --primary: oklch(0.92 0.02 264); --primary-foreground: oklch(0.21 0.03 264); }- 배경이 어두워지면 글자가 밝아지고, 반대도 마찬가지
- 대비가 테마 정의 시점에 보장된다. 사용하는 쪽은 신경 쓸 게 없다
알아야 할 색 토큰
섹션 제목: “알아야 할 색 토큰”20개 남짓이지만 역할별로 묶으면 여섯 덩어리다.
| 토큰 짝 | 언제 쓰나 | 빈도 |
|---|---|---|
background / foreground | 페이지 바탕과 기본 글자 | ★★★ |
card / card-foreground | 카드처럼 떠 있는 표면 | ★★★ |
primary / primary-foreground | 주요 액션 버튼, 강조 | ★★★ |
muted / muted-foreground | 흐린 배경, 설명 문구 | ★★★ |
border · input · ring | 테두리 / 입력창 테두리 / 포커스 링 | ★★★ |
destructive | 삭제·오류 | ★★☆ |
secondary / secondary-foreground | 보조 액션 버튼 | ★★☆ |
accent / accent-foreground | 마우스 올렸을 때·선택됐을 때 | ★★☆ |
popover / popover-foreground | 드롭다운·툴팁 등 떠 있는 레이어 | ★☆☆ |
chart-1 ~ chart-5 | 차트 팔레트 | ★☆☆ |
sidebar-* | 사이드바 전용 한 벌 | ★☆☆ |
위 다섯 줄만 확실히 알면 실무의 90%가 커버된다. 아래 넷은 필요해질 때 찾아 쓰면 된다.
자주 헷갈리는 셋
섹션 제목: “자주 헷갈리는 셋”backgroundvscard— 라이트 모드에서는 둘 다 흰색이라 차이가 안 보인다. 하지만 다크 모드에서 카드가 살짝 떠 보이게 하려면 값을 다르게 준다. 처음부터 구분해서 쓴다mutedvssecondary—muted는 “덜 중요한 것”(설명 문구, 비활성 영역),secondary는 “주요 액션은 아니지만 액션인 것”(취소 버튼). 의도가 다르다accentvsprimary—accent는 상호작용 상태용이다. 마우스 올림·키보드 선택 시 배경. 브랜드 색이 아니다
oklch — 왜 이 표기법인가
섹션 제목: “oklch — 왜 이 표기법인가”init이 만든 파일을 열면 익숙한 #18181b 대신 이런 게 보인다.
--primary: oklch(0.205 0 0);/* ↑ ↑ ↑ 밝기 선명도 색상(각도) */| 밝기 | 선명도 | 색상 | |
|---|---|---|---|
| 범위 | 0(검정) ~ 1(흰색) | 0(회색) ~ 0.4 정도 | 0 ~ 360도 |
| 예 | 0.205 = 꽤 어두움 | 0 = 완전 무채색 | 264 = 파랑 계열 |
왜 굳이? HSL이나 hex는 밝기 숫자가 사람 눈과 안 맞는다.
같은 밝기 값 50%인데 노랑은 눈부시고 파랑은 어둡게 보인다.
oklch는 지각 밝기를 별도 축으로 다뤄, 밝기 단계를 비교적 일관되게 조절하기 쉽다.
다만 아주 선명한 색은 화면의 색역을 벗어날 수 있으므로 결과 색과 대비를 실제로 확인해야 한다.
/* 같은 파랑, 밝기만 다른 5단계 — oklch면 이게 자연스럽다 */--blue-300: oklch(0.80 0.12 264);--blue-500: oklch(0.62 0.20 264);--blue-700: oklch(0.45 0.18 264);baseColor — 중립색 고르기
섹션 제목: “baseColor — 중립색 고르기”화면 대부분은 브랜드 색이 아니라 회색이다. 텍스트, 테두리, 배경, 비활성 상태 전부.
init 때 고르는 baseColor가 그 중립색 계열을 정한다.
현재 CLI의 선택지는 neutral · stone · zinc · mauve · olive · mist · taupe다.
위 셋은 차이를 이해하기 위한 대표 예고, 나머지는 shadcn/create에서 실제 화면으로 비교하는 편이 빠르다.
고르는 기준은 간단하다 — 따뜻하면 stone, 완전 무채색이면 neutral, 애매하면 zinc부터 본다.
초기화 뒤 components.json의 값만 바꿔서는 기존 토큰이 교체되지 않는다는 점을 기억하자.
연결하지 않으면 클래스가 안 생긴다
섹션 제목: “연결하지 않으면 클래스가 안 생긴다”가장 자주 겪는 함정이다. CSS 변수를 정의하는 것만으로는 bg-primary가 만들어지지 않는다.
- 왼쪽 — 무슨 값인가. 테마마다, 라이트/다크마다 다르다
- 가운데 — 어떤 유틸리티 클래스를 만들 것인가. 한 번 쓰고 끝
- 오른쪽 — 어떻게 쓰는가. 이름만 알면 된다
--color- 접두사가 붙은 이름이 곧 Tailwind 클래스가 된다.
--color-primary를 선언하면 bg-primary, text-primary, border-primary가 전부 생긴다.
색 토큰 추가하기 — warning을 예로
섹션 제목: “색 토큰 추가하기 — warning을 예로”기본 토큰에는 “경고”용 노란색이 없다. 직접 추가해 보자. 세 곳을 건드린다.
-
라이트 값을 정의한다
:root {--warning: oklch(0.84 0.16 84);--warning-foreground: oklch(0.28 0.07 46);} -
다크 값을 정의한다 — 밝기를 뒤집는다
.dark {--warning: oklch(0.41 0.11 46);--warning-foreground: oklch(0.99 0.02 95);} -
@theme inline에 연결한다 — 이게 있어야 클래스가 생긴다@theme inline {--color-warning: var(--warning);--color-warning-foreground: var(--warning-foreground);} -
쓴다
<div className="bg-warning text-warning-foreground">저장하지 않은 변경사항이 있습니다</div>
색 안티패턴 다섯
섹션 제목: “색 안티패턴 다섯”- 컴포넌트에 원시 색 직접 —
bg-zinc-900,bg-[#18181b]. 테마 밖으로 나간다 -foreground짝을 안 씀 —text-white하드코딩. 다크 모드에서 깨진다@theme inline연결 누락 — 변수는 있는데 클래스가 없다- 의미 없는 이름 —
--color-1,--blue-2. 반년 뒤 아무도 못 쓴다 - 다크 값을 밝기만 반전 —
#fff↔#000단순 반전은 눈이 아프다. 다크 배경은 순수 검정이 아니라oklch(0.145 0 0)정도가 편하다
4장 요약
섹션 제목: “4장 요약”- 색 토큰은
background/foreground짝으로 만든다. 대비가 정의 시점에 보장된다 - 컴포넌트에서
text-white같은 고정 글자색은 거의 항상 버그의 씨앗 - 실무의 90%는 다섯 짝 —
background·card·primary·muted·border/input/ring muted(덜 중요한 것) /secondary(보조 액션) /accent(호버·선택)는 의도가 다르다oklch는 밝기를 눈에 맞게 다뤄서 다크 값 만들기가 쉽다. hex를 써도 되긴 한다baseColor는 화면 대부분의 중립색 인상을 정한다. 초기화 뒤에는 필드만 바꿔도 기존 코드가 따라 바뀌지 않는다- 값 정의 +
@theme inline연결이 한 세트다.inline을 빠뜨리면 다크 모드가 조용히 죽는다
참고 자료
섹션 제목: “참고 자료”- shadcn/ui 테마 문서 — 기본 토큰, base color 목록, 새 토큰 연결 방식
- WCAG 2.2 대비 기준 — 일반 텍스트 4.5:1 기준