02 — Color Scale (Radix 12-step)

이 문서가 답하는 질문: 왜 색 scale은 12개여야 하는가? gray-50/100/.../900처럼 9~10단계가 30년 표준이었는데 왜 2022년부터 12-step이 사실상 표준이 됐나? 한 줄 답 (Pyramid Top): 12-step은 “하나의 hue가 UI에서 맡는 모든 역할”역할 단위로 슬롯화한 결과다. 1-2는 배경, 3-5는 컴포넌트, 6-8은 테두리, 9-10은 solid, 11-12는 텍스트. 디자이너가 “이 step 써”가 아니라 “이 역할에 맞는 step 골라” 라고 말할 수 있게 된 게 진짜 혁명이다.


Why — 왜 9-step이 부족했나

9-step (Tailwind 1~3 / Material 등)의 한계

50  100  200  300  400  500  600  700  800  900

이 9개는 그라데이션이지 역할이 아니다. 디자이너에게 “hover bg는 100 써, border는 200 써”라고 암묵적 합의로 운영했다.

문제:

문제9-step에서의 증상
같은 hover에 누구는 100, 누구는 200컴포넌트마다 hover 깊이가 다름
다크모드 다크 배경 위 subtle bg가 없음dark 모드에선 50/100 못 씀
solid brand color가 어디인지 모호500? 600? 700?
WCAG AA 통과하는 텍스트 색이 어디인지 모호700? 800? 900?
Border와 component bg가 같은 200을 공유둘이 시각적으로 분리 안 됨

12-step의 등장 (Radix Colors, 2022)

Modulz/Radix UI 팀(WorkOS가 인수)이 role-based scale을 발표.

1  2  3  4  5  6  7  8  9  10  11  12

각 step에 단일 역할을 부여. 디자이너의 암묵적 합의코드의 명시적 매핑으로 끌어올린 것이 핵심.


How — Radix 12-step의 역할 매핑

공식 매핑표

Step역할사용 예sRGB 명도 영역
1App background페이지 전체 배경가장 밝음 (L≈0.99)
2Subtle background카드, 사이드바, code block 배경L≈0.98
3UI element backgroundButton bg (rest), input bgL≈0.95
4Hovered UI element backgroundButton hoverL≈0.92
5Active / Selected UI element backgroundButton active, selected rowL≈0.88
6Subtle borders, separatorsdivider, 거의 안 보이는 lineL≈0.83
7UI element border, focus ringsinput border (rest)L≈0.77
8Hovered UI element borderinput border (hover)L≈0.68
9Solid backgrounds브랜드 색, primary button bgL≈0.55 (brand 영역)
10Hovered solid backgroundsprimary button hoverL≈0.50
11Low-contrast text보조 텍스트, labelL≈0.43
12High-contrast text본문 헤드라인L≈0.21

4-구역으로 묶기

이 구역 매핑이 정착되면, 디자이너가 figma에서 “이 영역은 component 영역 hover” 라고 부를 수 있다. 코드에서도 bg-{color}-4 또는 bg.hover 토큰이 그 역할을 표현한다.

Step 9의 특별함 — “Solid Anchor”

step 9는 brand identity 색의 자리다. Tailwind의 blue-500에 해당. 다른 step은 9를 기준으로 명도 이동하여 파생할 수 있다.

// step 9를 anchor로 다른 step 파생 (간단한 공식 — 실제 Radix는 더 정교)
function deriveStep(step, anchor /* oklch */) {
  const lTable = {
    1:  0.99, 2:  0.98, 3:  0.95, 4:  0.92,
    5:  0.88, 6:  0.83, 7:  0.77, 8:  0.68,
    9:  anchor.l, 10: anchor.l - 0.05,
    11: 0.43, 12: 0.21,
  };
  const cTable = {
    1: 0.005, 2: 0.01, 3: 0.02, 4: 0.04,
    5: 0.06, 6: 0.08, 7: 0.10, 8: 0.14,
    9: anchor.c, 10: anchor.c,
    11: 0.10, 12: 0.04,
  };
  return { l: lTable[step], c: cTable[step], h: anchor.h };
}

What — 실제 OKLCH 값으로 보는 12-step

Radix blue (light mode)

:root {
  --blue-1:  oklch(0.994 0.002 247.85);  /* app bg */
  --blue-2:  oklch(0.982 0.008 247.85);  /* subtle bg */
  --blue-3:  oklch(0.961 0.024 247.85);  /* component bg */
  --blue-4:  oklch(0.938 0.045 247.85);  /* component hover */
  --blue-5:  oklch(0.907 0.068 247.85);  /* component active */
  --blue-6:  oklch(0.869 0.090 247.85);  /* subtle border */
  --blue-7:  oklch(0.819 0.118 247.85);  /* border */
  --blue-8:  oklch(0.741 0.155 247.85);  /* hover border */
  --blue-9:  oklch(0.553 0.234 247.85);  /* SOLID brand */
  --blue-10: oklch(0.516 0.232 247.85);  /* solid hover */
  --blue-11: oklch(0.482 0.180 247.85);  /* low contrast text */
  --blue-12: oklch(0.249 0.085 247.85);  /* high contrast text */
}

H(247.85)가 전 step 동일. L이 monotonically 감소. C는 step 9에서 최대. 이게 OKLCH 기반 12-step의 시그니처.

실제 사용 — 컴포넌트별 매핑

/* Button (Solid variant) */
.btn-primary {
  background: var(--blue-9);
  color: white;
}
.btn-primary:hover  { background: var(--blue-10); }
.btn-primary:active { background: var(--blue-10); filter: brightness(0.95); }
 
/* Button (Soft variant) */
.btn-soft {
  background: var(--blue-3);
  color: var(--blue-11);
  border: 1px solid var(--blue-7);
}
.btn-soft:hover  { background: var(--blue-4); border-color: var(--blue-8); }
.btn-soft:active { background: var(--blue-5); }
 
/* Input */
.input {
  background: var(--blue-1);   /* or gray-1 */
  border: 1px solid var(--blue-7);
  color: var(--blue-12);
}
.input:hover { border-color: var(--blue-8); }
.input:focus { border-color: var(--blue-9); outline: 2px solid var(--blue-8); }
 
/* Body text */
body  { color: var(--gray-12); }
small { color: var(--gray-11); }

각 step이 어디서 쓰일지 모호함이 없다. 디자인 시스템이 “scale”이 아니라 “system”인 이유.

Alpha scale (overlay 용)

Radix는 순색 12-step 외에 알파 12-step도 제공한다. 어두운 배경 위 overlay 용도.

/* 같은 색의 알파 버전 */
--blue-a1:  oklch(0.553 0.234 247.85 / 0.02);
--blue-a9:  oklch(0.553 0.234 247.85 / 0.85);
--blue-a12: oklch(0.553 0.234 247.85 / 0.97);

배경이 동적(이미지, gradient)일 때 alpha 12-step이 순색 12-step보다 안전하다.

Tailwind v4와의 매핑

Tailwind는 여전히 50~950 9-step. Radix 12-step과 1:1 매핑하면:

Radix stepTailwind step역할
150bg
2100subtle bg
3100component bg
4200hover
5300active
6200subtle border
7300border
8400hover border
9500solid ★
10600solid hover
11600~700low text
12900high text

Tailwind만 쓸 때도 역할 매핑은 12-step의 사고방식으로 한다. 매핑이 역할 토큰을 사이에 끼우는 패턴과 잘 맞는다. → 06장


What-if — 잘못 쓰면

1) Step 의미를 컴포넌트마다 다르게

/* Button — hover로 step 4 */
.btn:hover { background: var(--blue-4); }
 
/* Card — hover로 step 3 */
.card:hover { background: var(--blue-3); }
 
/* Row — hover로 step 5 */
.row:hover { background: var(--blue-5); }

같은 hover 의미인데 3개의 깊이. 역할 토큰을 만들어 매핑을 한 곳에 두라:

:root {
  --bg-hover: var(--blue-4);  /* 모든 hover는 여기서 */
}
.btn:hover, .card:hover, .row:hover { background: var(--bg-hover); }

2) Step 9를 “조금 어둡게” 쓰기

step 9는 그 색의 가장 선명한 자리다. 여기서 조금 어둡게 하려고 step 10/11을 쓰면 채도가 떨어진 어두운 색이 된다. brand identity가 흐려진다.

✗ btn-primary-darker = step-11  (채도 떨어짐, 흙색)
✓ btn-primary-hover  = step-10  (명도만 살짝 떨어짐, 같은 brand)

3) Light 모드의 step을 그대로 다크모드에서 쓰기

step 1(app bg)이 light에선 흰색이지만 다크에선 짙은 회색이어야 한다. step 번호는 같아도 명도는 반전이다.

:root            { --gray-1: oklch(0.99 0 0); }  /* 흰색 가까움 */
[data-theme=dark]{ --gray-1: oklch(0.14 0 0); }  /* 검정 가까움 */

04장에서 자세히.

4) “12개는 너무 많아” → 6개로 압축

1, 3, 5, 7, 9, 12

가능하지만, 3-4-5의 hover/active 분리가 사라지면 시각 위계가 무너진다. 인지 차이가 작은 step끼리 묶을 거라면 1-2 (bg), *6-7 (border)*가 후보. 절대 3-4-5나 9-10은 묶지 말 것.


Insight — 왜 Radix가 9-step이 아닌 12-step을 골랐나

“디자이너의 합의를 코드 슬롯으로”

Radix Colors의 디자이너 Vlad Moroz(이전 Modulz CEO)는 2021년 인터뷰에서 “우리는 색의 그라데이션을 디자인하지 않았다. UI의 역할 슬롯을 디자인했다” 고 말했다.

Material Design의 50/100/…/900은 수학적 등간격에 가깝다 — 어디에 쓸지는 디자이너 재량. Radix 1~12는 역할의 슬롯이다 — step 자체가 사용처를 강제한다.

이 차이는 디자인 시스템의 문화를 바꾼다. 디자이너가 gray-200이라고 부르지 않고 gray-7 또는 역할 토큰 border.subtle로 부르기 시작한다. figma 변수 이름코드 토큰 이름역할로 통일된다. 이게 디자이너-개발자 합의의 다음 단계다.

또 하나의 반전 — Radix 12-step은 알파 12-step다크 12-step같은 step 번호로 짝짓는다. 같은 step 9는 light에서 brand color, dark에서도 brand color, alpha에서도 같은 brand color의 알파. step 번호가 의미의 영구 좌표가 된다.

2024년 Tailwind v4가 공식적으로 Radix와 매핑되지 않음을 유지한 건 의도적이다. Tailwind는 유틸리티고 Radix는 역할 토큰이라는 다른 추상화 레벨이라는 입장. 디자인 시스템은 그 사이에 자기 역할 토큰을 끼우는 게 답이다.


요약

  • 12-step은 역할 슬롯. 9-step은 그라데이션.
  • 1-2 bg / 3-5 component / 6-8 border / 9-10 solid / 11-12 text.
  • step 9가 brand anchor — 다른 step은 여기서 명도 이동으로 파생.
  • H는 step 전체 동일, L은 monotonic 감소, C는 9에서 최대 — OKLCH ramp의 시그니처.
  • light/dark에서 step 번호는 같고 명도는 반전 — 이게 다음 챕터의 핵심.

다음: 03-contrast-and-accessibility — 11-12 텍스트와 1-2 배경의 contrast pair가 정말 안전한지 검증.