- route table: extension route for products with an existing visual language, feedback row, derived state tokens stay local - completion: blocking 0 plus reported unverified; hard-gate surface is the touched scope; delivery report starts with what and how to open - design.md created at step 0 with a skeleton, fingerprint line and change log; product-surface rubric reading; direction delegation states the chosen card and reason - layout: hero families, nav forms, footer forms; art-direction page convergence 6 axes; genre sections; image treatments; placeholder conventions; multi-screen order; Tailwind theme mapping - platform: scroll-driven examples, anchor positioning, canvas OT, three/R3F pins, shadcn style prefixes, Safari 26 notes, subgrid - design-gate: four result states, exit codes, cwd module resolution, --url/--out-dir/--strict, oklch contrast, dead selector word match - Korean font budget split from the Latin baseline
29 KiB
color — 색 체계 상세
3단계에서 tokens.md §2 의 역할 토큰(--surface·--surface-raised·--ink·--ink-muted·--line·--accent·--accent-ink)을 실제 값으로 채울 때 필요한 절만 연다. 역할 토큰이 정본이고 이 문서는 방법이다 — 여기서 새 역할을 만들거나 tokens.md의 규칙을 바꾸지 않는다. 이 문서는 그 역할에 어떤 값을 어떻게 채우는지, 다크모드·그라디언트·대비를 어떻게 다루는지를 다룬다.
이 문서를 읽는 법
| 상황 | 읽을 곳 |
|---|---|
| 역할 토큰에 채울 실제 색이 없다(새 시스템) | §1~§3 |
| 여러 단계(램프)가 필요하다 | §2 |
| 그라디언트를 쓴다 | §4 |
| 다크모드를 만들거나 다듬는다 | §5 |
| 대비가 실패했거나 대비 근거가 필요하다 | §6 |
| 상태색·강조색의 의미와 로케일을 정한다 | §7 |
| 표준 7개 역할 밖의 색(비활성·scrim 등)이 필요하다 | §8 |
| 고채도 브랜드 색이 화면에서 탁하게 보인다 | §9 |
| 리디자인이나 정리·일관화 작업에서 기존 팔레트를 정리한다 | §10 |
| 값을 어떤 표기법(hex·oklch 등)으로 쓸지 모르겠다 | §11 |
| 마무리 전 확인 | §12 |
§0. 언제
색을 다루는 모든 순간이 아니라, 역할 토큰에 값을 채우는 3단계와 다크모드·그라디언트·리디자인처럼 별도 결정이 필요한 순간에만 연다. 브리프·기존 코드에 이미 정해진 색이 있으면 그것을 재사용하고 이 문서는 건너뛴다.
판정 위계는 이 문서 전체에서 세 층으로 나뉜다. 하드 게이트는 WCAG 2 대비처럼 협상 대상이 아닌 규범이다. 프로젝트 계약은 브리프·다크모드 지원 여부·특정 프레임워크 채택처럼 이 프로젝트가 선택하면 지키는 약속이다. 관찰 후보는 스타일 휴리스틱이며 시작값일 뿐 통과 기준이 아니다.
§1. 역할 우선과 선택적 원시값 계층
언제: 다중 hue 브랜드(강조색이 여러 개 필요)이거나 정밀한 다크모드 튜닝이 필요할 때만 연다.
tokens.md §2의 7개 역할 토큰은 대부분의 프로젝트에 충분한 시맨틱 계층이다. 시맨틱 토큰은 직무를 이름 짓고(--accent, --ink-muted) 컴포넌트가 참조하는 유일한 계층이다.
그 아래에 원시값(primitive) 계층을 둘지는 선택이다. 원시값은 값을 이름 짓고(--blue-500, --neutral-200) 컴포넌트에 절대 직접 쓰이지 않으며, 시맨틱 토큰이 그것을 가리킨다. 이 2계층 구조가 다크모드·화이트라벨 테마·증가-대비 변형을 가능하게 하는 이음매다 — 경계가 없으면 다크 모드를 만들 때마다 "이 파랑이 강조를 뜻했는지 그냥 파랑을 원했는지"를 모든 사용처에서 가려내야 한다 [SKILL-BETTER-COLORS].
:root {
/* 원시값(1계층) — 값을 이름 짓는다. 컴포넌트에 직접 쓰지 않는다 */
--blue-500: #3b82f6;
--neutral-700: #374151;
/* 시맨틱(2계층) — 직무를 이름 짓는다. tokens.md §2의 역할과 일치시킨다 */
--accent: var(--blue-500);
--ink-muted: var(--neutral-700);
}
단순 프로젝트에 원시값 계층을 강제하지 않는다. 다중 hue나 정밀 다크모드 튜닝이 필요 없으면 tokens.md의 7개 역할 토큰에 값을 직접 채우는 것으로 끝낸다 — 계층을 늘리는 것 자체가 목적이 아니다.
§2. 램프 생성
언제: 하나의 hue에서 여러 단계(호버·활성·옅은 배경·솔리드 필 등)가 동시에 필요할 때만 연다. 역할 토큰 하나에 값 하나면 이 절은 필요 없다.
잘 만든 램프는 네 가지 속성을 갖는다 [SKILL-BETTER-COLORS].
- 지각 명도 간격이 고르다. 포맷이 부르는 명도가 아니라 사람이 실제로 느끼는 명도로 계단진다 — HSL의 명도는 비지각적이라 고르게 벌린 HSL 값이 한쪽으로 뭉친다.
- hue가 끝까지 일정하다. 방황하는 hue는 두 색이 섞인 것처럼 읽힌다.
- 생생함(채도·명도의 지각적 강도)은 중간에서 정점을 찍고 양끝에서 떨어진다. 양끝까지 완전 채도를 유지하면 빛나는 밝은 끝과 잉크를 쏟은 듯한 어두운 끝이 된다.
- 밝은 끝일수록 스텝이 촘촘하다. 밝은 배경은 더 세밀한 구분이 필요하다 — 전 범위를 고르게 배치하면 옅은 끝의 두 표면(예: 페이지 배경과 카드 배경)이 구별되지 않는다.
관찰 후보 — 브랜드 색을 고정하는 두 방식. 계약상 정확히 지켜야 하는 브랜드 색은 핀(pin) — 그 값을 정확히 유지하고 램프가 그 주변에서 바깥으로 지어지며, 그 스텝만 약간 고르지 않게 배치된다. 그 외에는 스냅(snap) — 램프에 맞춰 모든 스텝을 고르게 배치한다. 스냅이 대개 더 고르게 보이고, 스와치를 나란히 대보지 않으면 차이가 잘 드러나지 않는다 [SKILL-BETTER-COLORS].
절대 손이나 눈으로 계산하지 않는다. 색 라이브러리(culori, colorjs.io, chroma.js 등)로 지각 공간에서 보간하고 프로젝트 표기법으로 출력한다 [SKILL-BETTER-COLORS].
import { clampChroma, formatHex } from 'culori'
// OKLCH에서 hue를 고정하고, 밝은 끝을 촘촘하게 둔 명도 배열을 직접 준다(원칙 4)
const L = [0.98, 0.955, 0.92, 0.86, 0.78, 0.68, 0.58, 0.49, 0.41, 0.33, 0.25]
const hue = 259, peakChroma = 0.19
const steps = L.map((l, i) => {
const t = i / (L.length - 1)
const c = peakChroma * Math.sin(Math.PI * t) + 0.01 // 채도는 중간에서 정점(원칙 3)
return formatHex(clampChroma({ mode: 'oklch', l, c, h: hue }, 'oklch')) // sRGB 밖이면 채도만 깎는다
})
명도 배열과 채도 곡선은 시작값이다. 브랜드 색을 핀으로 고정하면 그 스텝의 l·c를 브랜드 값으로 바꾸고 이웃을 다시 고른다.
Tailwind 50-950 11스텝, Radix 1-12 12스텝 매핑은 그 체계를 이미 쓰는 프로젝트에서만 참조한다. 이 매핑을 새 프로젝트의 기본값으로 강제하지 않는다. 두 컨벤션은 번호가 아니라 종류가 다르다 — Radix는 스텝을 역할로 정의해 다크 스케일이 같은 번호를 재사용하는 별도 램프라 --accent-9가 라이트·다크 양쪽에서 "솔리드 필"을 뜻하지만, Tailwind는 스텝을 명도로 정의해(50=밝음, 950=어두움) 다크모드에서 매핑이 반전된다(페이지 배경이 950이 됨) [SKILL-BETTER-COLORS]. 양끝 모두 순수 검정·흰색에는 못 미치게 둔다 — 거기 닿는 순간 색이 페이지 배경이 사는 지점에서 정체성을 잃는다.
§3. OKLCH 파생
oklch(L C H) — 명도(L) 01, 채도(C) 0약 0.4, hue(H) 0~360, 알파는 /로 분리한다. 지각적으로 균일한 명도·안정적 hue·예측 가능한 램프가 장점이다. Baseline 2023-05 Newly(모든 주요 엔진 지원), 2025-11경 Widely다 [WEB-BASELINE].
hue와 chroma를 고정하고 lightness만 움직이면 라이트·다크 쌍을 체계적으로 파생할 수 있다.
:root {
/* hue(259)·chroma(0.19)는 고정, lightness만 움직인다 — 어두운 배경 위에서는 강조색을 밝게 올린다 */
--accent-light: oklch(0.55 0.19 259);
--accent-dark: oklch(0.75 0.19 259);
}
진짜 새 색 시스템이면 oklch()가 최선의 기본값이다. 그 외에는 색 라이브러리로 프로젝트 고유 표기법에 맞춰 같은 연산을 한다 — 표기법 선택 자체는 §11을 따른다.
color-mix()는 상태용 색 파생(호버·비활성 등)에 유용하다. Baseline 2023-05 Newly, 2025-11경 Widely다 [WEB-BASELINE]. 다만 생성값이 토큰 계층 밖에 놓여 디자인 툴에서 검사할 수 없다는 한계가 있으므로, 반복해서 쓰이는 값이면 토큰으로 승격할지 판단한다. 상대 색 문법(oklch(from var(--x) calc(l - 0.1) c h))도 같은 용도로 강력하다. 다만 Baseline Newly(2024-09-16)로 color-mix()보다 16개월 늦어 아직 Widely가 아니다 — 지원 하한이 Safari 17 이하까지 내려가는 프로젝트는 고정 값 폴백을 먼저 선언한다[WEB-BASELINE]. 3중 이상 연쇄 파생은 읽을 수 없어진다 — 관찰 후보로 두고 체인이 길어지면 고정 값으로 되돌린다 [SKILL-BETTER-COLORS].
contrast-color(<배경>)는 배경에 대해 검정·흰색 중 대비가 큰 쪽을 고른다(Baseline Newly 2026-04-10). 사용자 지정 색 위의 글자색처럼 배경을 미리 알 수 없는 자리의 폴백으로만 쓴다 — 결과가 둘뿐이라 브랜드 잉크 토큰을 대체하지 못하고, 중간 명도 배경에서는 고른 쪽도 4.5:1에 못 미칠 수 있으므로 §6의 대비 측정을 건너뛰는 근거가 되지 않는다. [WEB-BASELINE]
§4. 그라디언트 보간
언제: 실제로 그라디언트를 쓸 때만 연다.
기본값(아무것도 지정하지 않았을 때)인 sRGB 보간은 직선(rectangular) 공간이다. hue 휠에서 반대편에 있는 두 색 사이를 직선으로 잇기 때문에 그 직선이 중립축 근처를 지나며 중간 지점이 탁하고 어두운 회색으로 꺼진다(회색 데드존). oklab도 직선 보간이지만 지각적으로 균일한 밝기를 유지해 sRGB보다 낫다. oklch는 극좌표(polar) 공간이라 hue 각도를 돌아가며 보간해 생생함이 끝까지 유지되지만, 대신 두 스톱 사이의 모든 hue를 훑고 지나간다(예: 파랑→핑크가 보라를 거쳐 간다 — 의도한 룩일 수도, 서프라이즈일 수도 있다) [SKILL-BETTER-COLORS].
/* 기본값(sRGB): 지정 안 하면 이것 — 중간이 탁해진다 */
.a { background: linear-gradient(to right, #eff6ff, #3b82f6); }
/* 최선 기본값: 직선 보간이면서 균일한 밝기 */
.b { background: linear-gradient(in oklab, #eff6ff, #3b82f6); }
/* hue 휠을 도는 룩이 필요할 때 */
.c { background: linear-gradient(in oklch longer hue, #eff6ff, #3b82f6); }
그라디언트 보간 공간 지정 문법은 Baseline Newly available이다(2024-06-11, Firefox가 마지막으로 지원을 완료). Widely는 그 날짜에서 30개월 뒤다 — 읽는 시점이 그 전이면 지정하지 않았을 때의 기본 sRGB 결과가 허용 가능한지 확인하거나 @supports 폴백을 둔다 [WEB-BASELINE].
밴딩은 스톱 간 대비가 낮은 넓은 영역에서 8비트 디스플레이의 계단이 드러나는 현상이다. 대비를 넓히거나 영역을 줄이거나 미세한 노이즈 텍스처를 얹는다. shorter hue/longer hue 키워드로 oklch 보간이 도는 방향을 고를 수 있다.
§5. 다크모드 재조정과 전환 메커니즘
언제: 프로젝트가 다크모드를 지원하기로 했을 때 연다.
tokens.md§2는 "다크는 반전이 아니라 역할별 재정의"라고 이미 선언한다 — 이 절은 그 재정의를 실행하는 구체 체크리스트다.
반전은 출발점이지 결과물이 아니다. 라이트 팔레트를 기계적으로 뒤집은 뒤, 거의 항상 아래 세 가지를 수동으로 다듬어야 한다 [SKILL-BETTER-COLORS]. 이는 프로젝트 계약 — 다크모드를 지원하기로 했다면 지킨다.
- 생생함을 낮춘다. 흰 배경 위에서 자신감 있게 읽히던 색이 거의 검정 위에서는 네온으로 읽힌다. 다크 외형에서는 강조색을 1~2스텝 덜 생생하게 조정한다.
- 어두운 끝을 분리한다. 옅은 배경에서 구분되던 스텝이 어두운 표면에서는 서로 뭉개진다. 어두운 끝에 스텝을 더 둔다.
- 대비 비대칭을 재검사한다. 대비는 거울상이 아니다 — 라이트에서 통과한 쌍이 반전되면 실패할 수 있다. 두 외형 모두 실제 렌더 배경에 대해 전경을 다시 검사한다.
전환 메커니즘은 하나를 고르고 전체에 쓴다. 메커니즘을 섞는 것이 흔한 실패다 — 일부 토큰은 미디어쿼리로, 다른 토큰은 클래스로 설정하면 사용자가 시스템 선호를 오버라이드하는 순간 반쯤만 테마된 인터페이스가 된다 [SKILL-BETTER-COLORS].
| 메커니즘 | 맞는 상황 | 주의 |
|---|---|---|
prefers-color-scheme 단독 |
테마 토글 UI가 없을 때 | persist·hydrate할 상태가 없어야 성립한다 |
.dark 클래스 |
사용자가 시스템 설정을 오버라이드할 수 있을 때 | 미디어쿼리는 초깃값만 정하고, 클래스가 최종 결정이다 |
light-dark() |
color-scheme도 함께 선언하는 프로젝트 |
클래스 기반 토글이라도 color-scheme 속성을 함께 갱신해야 값이 반영된다. Baseline Newly available(2024-05-13. Widely는 30개월 뒤)이므로 읽는 시점이 그 전이면 지원 범위를 확인한다 [WEB-BASELINE] |
어느 행인지는 기본 테마(시스템 따름·라이트·다크)와 사용자 토글 유무가 정한다. 둘 다 브리프의 값이다 — 브리프에 없으면 프로젝트의 기존 테마 파일이 쓰는 방식을 따르고, 임의로 토글을 더하거나 빼지 않는다.
:root {
color-scheme: light dark;
--surface: light-dark(<밝은 테마 표면>, <어두운 테마 표면>);
--ink: light-dark(<밝은 테마 본문>, <어두운 테마 본문>);
}
값은 3단계에서 정한 것을 넣는다. 성격이 다른 표면 세 쌍의 예다 — 형식을 보이려는 것이니 그대로 쓰지 않고, 대비는 각자 다시 잰다.
- 쿨 회색:
light-dark(oklch(0.97 0.005 250), oklch(0.2 0.02 250)) - 중간 명도 유채:
light-dark(oklch(0.86 0.06 120), oklch(0.3 0.05 120)) - 깊은 유채 다크:
light-dark(oklch(0.95 0.02 30), oklch(0.18 0.04 300))
다크모드 완결성 — 값 밖의 다섯 가지. 다크모드를 지원하기로 했다면 지킨다(프로젝트 계약). 마지막 두 줄은 관찰 후보다.
color-scheme을 선언한다. CSScolor-scheme(위 예시)이나<meta name="color-scheme" content="light dark">로 알려야 스크롤바·폼 컨트롤·기본 캔버스색이 테마를 따른다. 한 테마만 제공하면 그 값만 적는다.- 첫 페인트 전에 테마를 정한다(FOUC 방지). 사용자 선택을 저장하는 토글이 있으면
<head>안 인라인 스크립트로 저장값을 읽어<html>에 클래스·color-scheme을 붙인 뒤 CSS가 그려지게 한다. 외부 JS나 하이드레이션 뒤에 정하면 흰 화면이 한 번 번쩍인다. 저장소 접근이 막힌 환경(try/catch)에서는 시스템 선호로 떨어진다.<script> try { const t = localStorage.getItem("theme"); if (t) { document.documentElement.dataset.theme = t; document.documentElement.style.colorScheme = t; } } catch {} </script> - 브라우저 UI 색을 테마마다 맞춘다.
<meta name="theme-color" content="<밝은 테마 표면색>" media="(prefers-color-scheme: light)">와 다크용 한 줄을 둔다. 이 값을 브라우저 탭에서 쓰는 것은 Android Chrome이고, Safari는 26부터 설치된 웹앱에서만 쓴다(MDN 호환성 데이터, 확인 2026-10-06)[WEB-BASELINE]. Safari 탭의 툴바 색을 정하는 규칙은 문서화되지 않았으므로 특정 색을 약속하지 않는다 —html·body에 테마별background-color를 실제 표면색으로 선언하고(투명·그라디언트만 두지 않는다), 상단 고정 헤더가 있으면 iPhone 실기기의 라이트·다크에서 툴바와 어울리는지 확인한다. 사용자 토글이 시스템 선호와 다를 수 있으면 토글 때 스크립트로content와 루트 배경을 함께 갱신한다. - 이미지·일러스트를 테마별로 본다. 흰 바탕을 품은 PNG·스크린샷은 다크 위에서 흰 상자가 된다.
<picture><source media="(prefers-color-scheme: dark)">로 전용 자산을 주거나, 사진은 밝기를 살짝 낮추는 필터를 후보로 둔다. 어느 쪽이든 실제 렌더로 확인한다. - (관찰 후보) 순수
#000표면과 다크 본문 굵기. 순수 검정 위의 밝은 글자는 번져 보일 수 있어(할레이션) 거의 검정 표면이 흔한 시작값이다. 같은 이유로 다크에서 본문 웨이트를 한 단계 낮추는 선택이 있다. 실제 기기에서 비교해 정한다.
테마 전환 순간 색·배경·테두리·그림자가 동시에 바뀌어 뭉개지는(smear) 것을 막는 전환 억제 레시피는 구현 기법이므로 motion.md를 따른다 — 이 문서에서는 값만 다룬다.
§6. 대비
하드 게이트: WCAG 2 대비. 일반 텍스트 AA 4.5:1, 큰 텍스트(18pt≈24px 이상, 또는 bold 14pt≈18.7px 이상) AA 3:1 [WCAG-READ]. 로고·로고타입의 예외는 이 텍스트 대비(1.4.3)의 것이다. UI 컴포넌트와 이해에 필요한 그래픽 객체는 인접 색과 3:1(1.4.11) — 예외는 비활성 컴포넌트, 사용자 에이전트가 정한 외형을 작성자가 바꾸지 않은 컴포넌트, 특정 표현 자체가 정보 전달에 필수인 그래픽이다. 이해에 필요하지 않은 장식 그래픽은 처음부터 범위 밖이다 [WCAG-NONTEXT]. 이 수치는 협상 대상이 아니다.
수정 절차: 명도를 먼저 바꾼다. 대비가 실제로 반응하는 채널은 명도다 — hue와 채도는 측정값을 훨씬 덜 움직이므로, hue를 바꿔 대비를 고치려는 시도는 대체로 헛수고다. hue·채도를 고정한 채 전경을 배경에서 지각적으로 더 멀리 옮기고 재측정한다. hue를 고정하는 이유는 "대비 수정"이 조용히 "팔레트 변경"이 되는 것을 막기 위해서다 [SKILL-BETTER-COLORS]. APCA 기준으로는 지각 명도 약 75% 안팎 배경에서 순수 검정도 Lc 60 안팎에 그친다[SKILL-BETTER-COLORS] — WCAG 2 비율로는 이 배경에서 검정 텍스트가 여유 있게 통과하므로 두 척도를 섞지 않는다. WCAG 2 기준으로 흑·백 어느 전경도 여유가 적은 구간은 중간 회색(CIELAB L* 약 50 부근, 최대 약 4.6:1)이며, 그런 배경에서는 전경보다 배경을 바꾸는 편이 낫다. 명도를 밀면 갬멀을 벗어날 수 있으니 렌더 가능하도록 채도를 함께 낮춘다. 값을 바꾼 뒤에는 항상 재측정한다 — 고쳐졌다고 가정하지 않는다.
APCA는 규범이 아니라 보조 진단이다. 2026년 9월 확인 기준 WCAG 3 초안 본문은 대비 알고리즘 자체를 아직 정하지 않았고(APCA라는 이름조차 명시되어 있지 않다), APCA 자체 문서도 스스로를 "beta"로 표시하며 미래 표준의 평가 후보일 뿐이라고 밝힌다 [APCA-STATUS]. 그래서 designpaca는 WCAG 2 비율을 대비의 하드 게이트로 유지한다. APCA는 "라이트 모드에서 통과한 쌍이 다크 모드에서 실패하는가" 같은 추가 신호가 필요할 때만 보조로 참고한다. 쓴다면 본문 텍스트는 Lc 75(선호 90), 라벨·헤드라인 같은 비본문은 Lc 60(선호 75)을 시작값으로 둔다 — 실측 조정 [SKILL-BETTER-COLORS].
렌더 타임 계산값은 렌더된 결과를 측정한다. color-mix(), 상대 색 문법, light-dark()는 모두 렌더 타임에 해석되므로 선언값이 아니라 실제 렌더 결과를 측정해야 한다. backdrop-filter가 걸린 표면은 뒤로 스크롤되는 콘텐츠에 따라 실효 색이 바뀌므로, 가장 밝은·가장 어두운 콘텐츠에서 테스트하거나 표면을 충분히 불투명하게 만든다 [SKILL-BETTER-COLORS].
§7. 의미와 상태색
시맨틱 색을 비파괴 액션에 쓰지 않는다. 상태색(danger, success 등)은 그 상태를 실제로 표시하는 곳에만 쓴다. 삭제 같은 파괴적 행동이 아닌 일반 강조에 danger 색을 빌려 쓰면 사용자가 실제로 위험한 것으로 오독한다. 프로젝트 계약: 하나의 색은 하나의 의미만 진다 — 역할이 같으면 같은 토큰을 쓴다. 강조색이 "인터랙티브"를 뜻하면 그 색이 정적 텍스트에 쓰이는 순간 클릭 불가능한 것을 클릭하라고 말하는 셈이다. 관찰 후보: hue가 15도 안팎으로 가깝고 명도·채도도 비슷한 두 색은 사용자에게 같은 색으로 읽힐 수 있으니 의미가 다르면 명도·채도까지 벌린다(외부 스킬 관찰값, 근거 미검증) [SKILL-BETTER-COLORS].
결정 단위당 채운 주요 액션은 하나다. 결정 단위는 사용자가 한 번에 하나를 고르는 범위다 — 히어로 하나, 폼 하나, 다이얼로그 하나, 반복 모듈 하나. 채운 색이 주요 강조를 인코딩할 때는 결정 단위마다 주 행동 하나만 색을 채우고 나머지는 중성으로 둔다. 상품 카드별 "담기"·요금제 열별 가입처럼 반복 모듈은 모듈마다 하나가 정상 관례이며, 요금제는 추천 열만 채우고 나머지는 외곽선으로 두는 것을 시작값으로 한다. 구매/정기구독처럼 결과가 다른 두 경로는 세그먼트 선택 + 단일 CTA와 병렬 두 CTA 중 브리프의 목표 행동 우선순위로 고른다. 색은 배경에 올리지 라벨에 올리지 않는다 — 채운 버튼은 방 건너편에서도 주요 행동으로 읽히지만, 중성 버튼 위 강조색 텍스트는 링크로 읽힌다. 서로 다른 상태·카테고리를 인코딩하는 경우라면 여러 색 배경도 괜찮다 [SKILL-BETTER-COLORS]. 프로젝트 계약.
색만으로 의미를 전달하지 않는다. 이 규칙 자체(비색각 사용자·회색조 인쇄를 위한 아이콘·라벨 병기)는 하드 게이트이며 정본은 accessibility.md다.
문화적 의미. 색은 문화마다 다르게 읽힌다. 서구 관행에서 빨강=위험·손실, 초록=성공·이익이라는 연상이 흔히 쓰이지만, 중국어권 금융 UI는 반대로 상승을 빨강, 하락을 초록으로 표시한다 [SKILL-BETTER-COLORS]. 한국 증권 화면의 "빨강=상승" 관행도 이번 조사에서 한국거래소 등의 1차 공식 규정을 찾지 못했다 — 국제 표준이 아니라 시장 관행일 가능성이 높다. 그러므로 손익·상태처럼 문화적으로 뒤집힐 수 있는 색은 하드코딩하지 않고, 데이터 제공자(증권사·거래소 API)와 브리프가 실제로 쓰는 관례를 확인해 로케일별 토큰으로 둔다. 확인하지 못한 관행을 확인된 사실처럼 쓰지 않는다.
§8. 역할 확장 목록
언제: 필요할 때만 연다. tokens.md §2의 7개 역할이 최소 집합이며 기본값이다. 컴포넌트가 요구할 때마다 무분별하게 역할을 추가하면 팔레트가 맨 처음 만든 화면의 모양으로 굳는다 — 실제로 그 역할을 렌더하는 화면이 생겼을 때만 아래에서 골라 추가한다.
| 확장 역할 | 용도 | 위계 |
|---|---|---|
| 비활성 텍스트(disabled) | 조작 불가 상태의 라벨·값 | 프로젝트 계약 — 대비는 충분히 유지하되 활성 상태처럼 읽혀서는 안 된다 |
| 반전(inverse) | 어두운/밝은 표면 위에 반대 톤으로 놓이는 텍스트 | 프로젝트 계약 |
| on-accent | 강조 배경 위 텍스트·아이콘(--accent-ink와 별도 구분이 필요할 때) |
프로젝트 계약 |
| sunken | 입력창·웰처럼 표면보다 가라앉아 보이는 배경 | 관찰 후보 |
| scrim | 모달·오버레이 뒤에 어둡게 깔리는 층 | 프로젝트 계약 |
| 포커스 링 | 키보드 포커스 표시 | 인접 색과 3:1 이상 — 하드 게이트, 정본은 accessibility.md |
| separator vs border | 구분선(콘텐츠를 나눔) vs 테두리(컨트롤을 둘러쌈) | 관찰 후보 — 오늘 값이 같아도 역할은 분리해 둔다. 입력창을 재스타일링하는 순간 둘이 갈라진다 [SKILL-BETTER-COLORS] |
이 목록은 역할 재고이지 강제 목록이 아니다. tokens.md §2의 "역할에 토큰이 없으면 토큰을 추가한다"는 원칙을 그대로 따르되, 추가할 이름을 고를 때 여기서 고른다.
§9. 넓은 색역 — P3
언제: 고채도 브랜드 색이 화면에서 탁하거나 밋밋해 보일 때만 연다(점진적 향상).
모든 sRGB 색은 Display P3 색역 안에 있지만 역은 성립하지 않는다. 차이는 채도가 높은 색에서만 의미가 있다 — 낮은 채도의 색은 두 색역에서 같게 보이므로 P3 값을 따로 둘 이유가 없다 [SKILL-BETTER-COLORS].
순서가 중요하다. sRGB 값을 먼저 선언하고, @media (color-gamut: p3) 안에서 더 채도 높은 P3 값으로 오버라이드한다.
:root {
--accent: #3b82f6; /* sRGB 먼저 */
}
@media (color-gamut: p3) {
:root {
--accent: color(display-p3 0.28 0.52 0.95);
}
}
클리핑은 우아하지 않다. sRGB로 표시 못 하는 P3 값을 지원 안 하는 디스플레이에 그대로 보내면 이웃 스텝들을 하나의 렌더 색으로 뭉갠다 — 최대 생생함은 hue마다 달라서(시안이 빨강·보라보다 훨씬 낮다) 일부 스텝에서만 클리핑이 일어나 램프가 고르지 않게 무너진다. 프로젝트 계약 — P3 값을 쓰면 sRGB 폴백을 먼저 선언한다. 폴백 없이는 우아하게 저하되지 않고 램프가 무너진다 [SKILL-BETTER-COLORS].
§10. 기존 팔레트 감사 5단계
언제: 리디자인이나 정리·일관화 작업에서 팔레트를 재구조화하기 전에 연다.
- 모든 리터럴을 수집한다.
hex,rgb(,hsl(,oklch(, 프로젝트 유틸리티 클래스 접두어를 grep한다. 스타일시트뿐 아니라 SVGfill/stroke, 차트 설정, 이메일 템플릿까지 포함한다 — 색은 스타일시트 밖에도 숨는다. - 각 hue 계열 안에서 지각된 명도로 정렬한다. 근접 중복이 이웃으로 즉시 드러난다.
- 근접 중복을 합친다. 대략 1램프 스텝보다 가까운 두 색은 드리프트한 하나의 색이다 — 가장 많이 쓰인 값을 남기고 나머지는 폐기한다. 절대 평균 내지 않는다.
- 생존자마다 §8의 역할을 배정한다. 어떤 역할과도 맞지 않는 색은 누락된 토큰이거나 실수다 — 어느 쪽인지 정해 finding에 적는다.
- 역할당 램프가 1개를 넘는지 센다. 넘는다면 팔레트가 구조를 벗어난 것이지 제품에 색이 더 필요한 게 아니다.
[SKILL-BETTER-COLORS]
아무것도 바꾸기 전에 인벤토리부터 보고한다. 팔레트 통합은 아무도 건드리라 하지 않은 화면의 렌더 결과까지 바꾸므로, 사용자가 수락할 때까지는 제안으로 남긴다.
§11. 표기법은 프로젝트를 따른다
프로젝트가 이미 쓰는 표기법을 재사용한다. hex를 쓰는 프로젝트가 잘못하고 있는 게 아니다 — 값 하나 고치려고 두 번째 표기법을 더하지 않는다. hex와 oklch()가 섞인 것보다 일관된 hex 체계가 낫다 [SKILL-BETTER-COLORS].
변환은 다음 세 경우에만 한다. 사용자가 요청했을 때, 합의된 마이그레이션이 스코프일 때, 프로젝트가 표기법을 표준화하는 중이고 이 값이 낙오자일 때. 이 문서가 열렸다는 이유만으로 변환하지 않는다.
변환 스코프 안에서도 CSS 키워드(currentColor, inherit, transparent, initial, unset)는 그대로 두고, 그라디언트 함수는 색 스톱만 바꾸고 보간 방식은 건드리지 않는다. 대량 변환은 정리(cleanup)가 아니라 마이그레이션이다 — 렌더된 모든 색을 반올림 오차만큼 바꾸고 아무도 요청하지 않은 파일까지 건드리므로, 부수효과가 아니라 그 자체가 별도 작업이어야 한다 [SKILL-BETTER-COLORS].
§12. 구현 중 자기 점검 (납품 판정은 preflight.md X6·A3·A24)
납품 판정은 preflight.md X6이 하고, 라이트·다크 대비 측정은 A3, 색만으로 의미 전달 금지는 A24가 판정한다.
tokens.md§2의 역할 토큰이 먼저 채워져 있다. 원시값 계층은 다중 hue·정밀 다크모드가 실제로 필요할 때만 추가했다.- 대비를 고쳤다면 명도를 먼저 바꾸고 hue를 고정했으며, 값을 바꾼 뒤 재측정했다.
- 그라디언트를 썼다면 보간 공간을 의도적으로 골랐고, Newly available 문법에는 결과가 허용 가능한 기본 sRGB 폴백이 있다.
- 다크모드를 반전이 아니라 재조정했다(생생함 낮추기·어두운 끝 분리·대비 비대칭 재검사 3항목).
- 전환 메커니즘을 하나만 골라 전체에 썼다(미디어쿼리·클래스·
light-dark()혼용 없음). - 다크모드를 지원하면
color-scheme선언, 첫 페인트 전 테마 결정(번쩍임 없음), 테마별theme-color, 테마별 이미지 확인을 마쳤다(§5). - 상태색·강조색이 하나의 색=하나의 의미를 지킨다. 결정 단위당 채운 주요 액션이 하나다(반복 모듈은 모듈마다 하나).
- 손익·상태처럼 문화적으로 뒤집힐 수 있는 색은 데이터 제공자·브리프의 실제 관례를 확인했다(지어내지 않았다).
- P3를 썼다면 sRGB 폴백이 먼저 선언돼 있다.
- 리디자인이면 기존 팔레트를 5단계로 감사해 인벤토리를 먼저 보고했다.
- 표기법은 프로젝트 것을 따랐고, 이 문서를 열었다는 이유만으로 값을 변환하지 않았다.