CSS

배경색이 바뀌면 글자색도 따라가야 한다 — contrast-color() 없이 살아남는 법

브라우저 지원이 부족한 CSS contrast-color()를 color-mix(), relative color syntax, custom properties로 근사 구현하는 방법을 정리한다.

10 min read
CSScontrast-color접근성색상
배경색이 바뀌면 글자색도 따라가야 한다 — contrast-color() 없이 살아남는 법

사용자가 프로필 색상을 직접 고르는 서비스를 만든다고 가정하자. 밝은 노란색 배경에 흰색 텍스트를 올리면? 아무것도 보이지 않는다. 짙은 남색에 검정 텍스트를 올려도 마찬가지다. 배경색에 따라 텍스트 색상을 흑 또는 백으로 자동 전환해야 읽을 수 있다. 이것이 대비색 문제이고, CSS contrast-color() 함수가 정확히 이 문제를 풀기 위해 제안됐다. 하지만 2026년 2월 현재, 이 함수를 지원하는 브라우저는 Safari Technology Preview 정도에 그친다. 기다릴 수 없으니, 다른 CSS 기능으로 우회해야 한다.

[💡 잠깐! 이 용어는?] contrast-color(): CSS Color Module Level 6에 제안된 함수로, 주어진 배경색에 대해 WCAG 대비율 기준을 만족하는 텍스트 색상(보통 흑 또는 백)을 자동 반환한다.


왜 이것이 중요한가

Notion, Slack, GitHub — 사용자가 레이블 색상이나 프로필 배경을 직접 지정하는 서비스는 어디에나 있다. 임의의 배경색에 대해 읽기 쉬운 텍스트 색상을 보장해야 하는데, JavaScript로 명도를 계산해서 분기하는 것은 성능과 유지보수 면에서 비효율적이다.

WCAG 2.1은 텍스트와 배경 간 최소 대비율 4.5:1(일반 텍스트 기준)을 요구한다. 비유하면, "글자가 배경에 묻히지 않도록 최소한의 명암 격차를 보장하라"는 교통 표지판의 최소 크기 규정 같은 것이다. 대비율이 이 기준 아래로 떨어지면, 시력이 좋은 사람도 읽기 불편하다.


대안 1 — color-mix() + light-dark()

CSS light-dark() 함수는 현재 색상 스킴(light/dark)에 따라 두 색상 중 하나를 선택한다.

light-dark-approach.css
:root {
  --bg: #3498db;
  color-scheme: light dark;
}
 
.badge {
  background-color: var(--bg);
  color: light-dark(#000000, #ffffff);
}

한계가 명확하다. light-dark()시스템 또는 페이지 레벨의 color-scheme에 반응할 뿐, 개별 요소의 배경색 명도에 따라 전환되지 않는다. "한 페이지에 밝은 뱃지와 어두운 뱃지가 공존하는" 상황에서는 쓸 수 없다.


대안 2 — Relative Color Syntax + oklch 명도 분기

CSS Relative Color Syntax를 사용하면 기존 색상의 채널 값을 추출하고 조작할 수 있다. oklch 색공간의 명도(L) 채널은 인간의 밝기 인지와 잘 맞아서, 명도 기반 분기에 가장 적합하다.

oklch-contrast.css
.badge {
  --bg: #e74c3c;
  background-color: var(--bg);
 
  /*
    oklch에서 L(명도)을 추출해 분기:
    L > 0.6이면 어두운 텍스트, L <= 0.6이면 밝은 텍스트
  */
  color: oklch(from var(--bg) calc((0.6 - l) * 999) 0 0);
}

[💡 잠깐! 이 용어는?] oklch: 색상을 Lightness(명도), Chroma(채도), Hue(색상) 세 축으로 표현하는 색공간. sRGB보다 인간의 색 인지에 가까워서, "이 색이 밝은지 어두운지"를 판단하기에 더 정확하다.

핵심은 calc((0.6 - l) * 999) 부분이다. 시소 원리와 같다. 명도 0.6을 축(기준점)으로 놓고, 배경색 명도가 기준보다 높으면 한쪽(검정)으로, 낮으면 반대쪽(흰색)으로 확 기울어지도록 큰 배율(999)을 곱한다.

  • l이 0.7(밝은 배경): (0.6 - 0.7) * 999 = -99.9 → clamp되어 0 (검정)
  • l이 0.3(어두운 배경): (0.6 - 0.3) * 999 = 299.7 → clamp되어 1 (흰색)

Relative Color Syntax는 Chrome 119+, Safari 16.4+, Firefox 128+에서 지원된다.


대안 3 — Custom Properties + calc() (레거시 호환)

JavaScript 없이 순수 CSS만으로 RGB 값 기반 명도를 계산하는 방법이다. 가장 넓은 브라우저 호환성을 확보한다.

custom-properties-contrast.css
.badge {
  --r: 52;
  --g: 152;
  --b: 219;
 
  background-color: rgb(var(--r) var(--g) var(--b));
 
  /*
    상대 밝기(relative luminance) 근사 계산
    공식: 0.2126*R + 0.7152*G + 0.0722*B
    결과가 128보다 크면 밝은 배경 → 검정 텍스트
  */
  --luminance: calc(
    0.2126 * var(--r) + 0.7152 * var(--g) + 0.0722 * var(--b)
  );
 
  --text-lightness: calc((var(--luminance) - 128) * -999);
  color: rgb(
    clamp(0, var(--text-lightness), 255)
    clamp(0, var(--text-lightness), 255)
    clamp(0, var(--text-lightness), 255)
  );
}

[💡 잠깐! 이 용어는?] 상대 밝기(Relative Luminance): 색상이 인간의 눈에 얼마나 밝게 보이는지를 수치화한 값. 녹색(G)이 가장 큰 가중치(0.7152)를 갖는 이유는, 인간의 눈이 녹색광에 가장 민감하기 때문이다.

장점은 CSS Custom Properties와 calc()만 쓰기 때문에 모든 모던 브라우저에서 동작한다는 것이다. 단점은 #3498db 같은 hex 값을 직접 쓸 수 없고, RGB 채널을 수동으로 분리하거나 Sass 같은 전처리기의 도움이 필요하다는 점이다.


세 가지 대안 비교

기준color-mix() + light-dark()oklch Relative ColorCustom Properties + calc()
정확도낮음 (페이지 레벨 분기)높음 (요소별 분기)중간 (근사 계산)
코드 간결성간결간결장황
브라우저 지원Chrome 123+, Safari 17.4+Chrome 119+, Safari 16.4+모든 모던 브라우저
hex 색상 직접 사용가능가능불가 (RGB 분리 필요)
요소별 독립 동작불가가능가능
권장 상황라이트/다크 모드 전환동적 배경색 대응레거시 호환 필요 시

실전 — 동적 뱃지 컴포넌트

실무에서 가장 자주 마주치는 시나리오는 태그(Tag)나 뱃지(Badge)다. 사용자가 색상을 자유롭게 지정하고, 그 위에 텍스트가 올라가는 구조다.

dynamic-badge.css
.badge {
  --badge-color: var(--user-selected-color, #3498db);
 
  background-color: var(--badge-color);
  color: oklch(from var(--badge-color) calc((0.6 - l) * 999) 0 0);
 
  padding: 4px 12px;
  border-radius: 12px;
  font-size: 14px;
  font-weight: 600;
}
 
.badge--danger  { --badge-color: #e74c3c; }
.badge--warning { --badge-color: #f1c40f; }
.badge--info    { --badge-color: #2c3e50; }

JavaScript를 한 줄도 쓰지 않고, CSS 변수 하나만 바꾸면 배경색과 텍스트 색이 모두 자동으로 적절하게 조정된다.


접근성 주의사항

CSS만으로 대비색을 자동 계산하는 방법들은 편리하지만, WCAG 대비율 4.5:1을 완벽히 보장하지는 않는다. oklch 명도 기준 0.6은 경험적으로 좋은 결과를 내지만, 모든 색상에서 AA 기준을 통과하는 것은 아니다. 특히 중간 명도의 채도 높은 색상(선명한 초록, 선명한 주황)에서는 흑과 백 모두 대비율 4.5:1 미만이 될 수 있다.

현실적인 전략은 두 가지를 병행하는 것이다.

  1. CSS 자동 분기를 기본 구현한다
  2. 디자인 시스템 레벨에서 사용 가능한 색상 팔레트를 제한해, 모든 팔레트 색상에 대해 대비율을 사전 검증한다

색상 팔레트를 제한하면 자유도가 줄어드는 것 같지만, 비유하면 "아무 물감이나 써라"보다 **"검증된 물감 30가지 중에서 골라라"**가 사고를 줄이는 것과 같다.


마무리

contrast-color()가 모든 브라우저에서 지원되는 날이 오면, 이 모든 우회 방법은 퇴역한다. 그날이 올 때까지 oklch Relative Color Syntax가 가장 실용적인 대안이다. 코드가 간결하고, 요소별로 독립 동작하며, 모던 브라우저 지원 범위도 충분하다. 레거시 호환이 필요하면 Custom Properties + calc()를 선택하면 된다. 어떤 방법을 쓰든 원칙은 하나다 — 배경색이 바뀌면 텍스트 색도 자동으로 따라가야 한다.