디자인 시스템 작업하다 보면 색 하나에서 파생색을 여럿 만들게 된다. 버튼 기본색이 있으면 hover는 살짝 어둡게, 배경은 아주 연하게, 테두리는 그 중간쯤. 예전엔 이걸 SCSS의 darken(), lighten(), rgba()로 처리했다.
근데 SCSS 색상 함수는 빌드 타임에 값이 고정된다. 브랜드 색을 CSS 변수로 런타임에 바꾸고 싶어도, darken($primary, 10%)는 $primary가 SCSS 변수일 때만 동작한다. var(--primary)를 넣으면 그냥 안 된다. 다크 모드 만들 때 이 벽에 자주 부딪혔다.
color-mix()는 이걸 CSS 런타임에서 해결한다. 두 색을 지정한 비율로 섞는 함수인데, var()를 그대로 넣을 수 있다.
기본 문법
color-mix(in srgb, red 70%, blue);
in srgb는 어떤 색 공간에서 섞을지다. 첫 번째 색에 70%를 주면 나머지 30%가 두 번째 색이다. 비율은 한쪽만 써도 되고, 생략하면 50:50이다.
파생색 만들기
핵심은 흰색·검은색과 섞어서 밝기를 조절하는 거다. darken은 검정을, lighten은 흰색을 섞으면 된다.
:root {
--primary: #3b82f6;
}
.btn {
background: var(--primary);
}
/* 10% 어둡게 — darken 대체 */
.btn:hover {
background: color-mix(in srgb, var(--primary) 90%, black);
}
/* 아주 연한 배경 — 12%만 섞기 */
.card {
background: color-mix(in srgb, var(--primary) 12%, white);
}
/* 반투명 테두리 — rgba 대체 */
.chip {
border: 1px solid color-mix(in srgb, var(--primary) 40%, transparent);
}
transparent와 섞으면 투명도까지 나온다. rgba()로 알파값 따로 계산하던 걸 대체할 수 있다.
다크 모드에서 진가
여기서 SCSS로는 안 되던 게 된다. --primary만 테마별로 바꾸면 파생색이 전부 알아서 따라온다.
:root {
--primary: #3b82f6;
}
[data-theme='dark'] {
--primary: #60a5fa;
}
.btn:hover, .card, .chip을 다시 손댈 필요가 없다. 변수 하나 바뀌면 섞인 결과도 런타임에 다시 계산된다. 색 팔레트를 컴포넌트마다 하드코딩하지 않아도 되는 게 제일 크다.
색 공간을 in oklch로 바꾸면 사람 눈에 더 균일한 밝기 변화가 나온다. 미묘한 그라데이션이 필요하면 srgb 대신 oklch를 써보는 걸 추천한다.
color-mix()는 이미 모든 최신 브라우저에서 Baseline으로 쓸 수 있다. SCSS를 색 계산 하나 때문에 붙들고 있었다면, 이제 그 부분은 CSS 변수 + color-mix()로 넘길 수 있다. 브랜드 색 하나에서 UI 전체 색을 파생시키는 구조를 만들면 유지보수가 확실히 편해진다.