배경색이 바뀌면 텍스트 색도 바뀌어야 한다

 

사용자가 프로필 색상을 직접 고르는 서비스를 만든다고 가정하자. 밝은 노란색 배경에 흰색 텍스트를 올리면 아무것도 안 보인다. 반대로 짙은 남색 배경에 검정 텍스트를 올려도 마찬가지다. 배경색에 따라 텍스트 색상을 자동으로 흑 또는 백으로 전환해야 읽을 수 있다. 이것이 대비색(contrast color) 문제다.

 

CSS에는 이 문제를 정확히 해결하기 위한 contrast-color() 함수가 제안되어 있다. 하지만 2026년 2월 현재, 이 함수를 지원하는 브라우저는 Safari Technology Preview 정도에 그친다. Chrome, Firefox, Edge에서는 아직 사용할 수 없다. 그래서 다른 CSS 기능을 조합해 비슷한 효과를 만드는 방법이 필요하다.

 

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

 


 

왜 이것이 중요한가

 

1. 동적 테마와 사용자 정의 색상

 

Notion, Slack, GitHub 같은 서비스에서 사용자가 레이블 색상, 프로필 배경, 태그 색상을 직접 지정하는 기능은 이제 흔하다. 이때 임의의 배경색에 대해 읽기 쉬운 텍스트 색상을 보장해야 한다. JavaScript로 명도를 계산해서 분기하는 방법도 있지만, CSS만으로 해결할 수 있다면 성능과 유지보수 측면에서 훨씬 유리하다.

 

2. WCAG 접근성 기준

 

WCAG 2.1은 텍스트와 배경 간 최소 대비율 4.5:1(일반 텍스트 기준)을 요구한다. 비유하면 "글자가 배경에 묻히지 않도록 최소한의 명암 차이를 보장하라"는 규칙이다. 대비율이 이 기준을 못 미치면 시력이 좋은 사람도 읽기 불편하고, 저시력 사용자는 아예 읽을 수 없다.

 


 

대안 1: color-mix() + light-dark() 조합

 

CSS light-dark() 함수는 현재 색상 스킴(light/dark)에 따라 두 색상 중 하나를 선택한다. 이것을 배경색의 명도에 연동하면 contrast-color()와 비슷한 효과를 낼 수 있다.

 

color-mix-light-dark.css

:root {
  --bg: #3498db;

  /* 배경색을 기반으로 color-scheme을 결정 */
  color-scheme: light dark;
}

.badge {
  background-color: var(--bg);

  /* light 모드면 검정, dark 모드면 흰색 */
  color: light-dark(#000000, #ffffff);
}

 

이 방법의 한계는 light-dark()시스템 또는 페이지 레벨의 color-scheme에 반응한다는 점이다. 개별 요소의 배경색 명도에 따라 자동 전환되는 것이 아니라, 페이지 전체의 라이트/다크 모드에 종속된다. 따라서 "요소별로 다른 배경색에 맞춰 텍스트 색을 바꾸는" 용도에는 적합하지 않다.

 


 

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

 

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

 

oklch-contrast.css

.badge {
  --bg: #e74c3c;
  background-color: var(--bg);

  /*
    oklch에서 L(명도)을 추출해 분기:
    L > 0.6이면 어두운 텍스트, L <= 0.6이면 밝은 텍스트

    아이디어: 명도를 극단값(0 또는 1)으로 매핑
  */
  color: oklch(from var(--bg) calc((0.6 - l) * 999) 0 0);
}

 

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

 

이 방법의 핵심은 calc((0.6 - l) * 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 (흰색)

 

비유하면 시소 원리와 같다. 명도 0.6을 기준점(축)으로 놓고, 배경색 명도가 기준보다 높으면 한쪽(검정)으로, 낮으면 반대쪽(흰색)으로 확 기울어지도록 큰 배율(999)을 곱하는 것이다.

 

주의: Relative Color Syntax는 Chrome 119+, Safari 16.4+, Firefox 128+에서 지원된다. IE나 구형 브라우저에서는 동작하지 않는다.

 


 

대안 3: CSS Custom Properties + calc()

 

JavaScript 없이 순수 CSS만으로, RGB 값을 기반으로 명도를 계산하는 방법이다.

 

custom-properties-contrast.css

.badge {
  /* RGB 채널을 개별 변수로 분리 */
  --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)
  );

  /*
    명도에 따라 텍스트 색상 분기
    luminance > 128이면 0(검정), 아니면 255(흰색)
    큰 배율을 곱해 이진 분기 효과
  */
  --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)
  );
}

 

이 방법의 장점은 가장 넓은 브라우저 호환성을 확보할 수 있다는 것이다. CSS Custom Properties와 calc()은 모든 모던 브라우저에서 지원된다. 단점은 색상을 반드시 RGB 채널별로 분리해서 입력해야 한다는 점이다. #3498db 같은 hex 값을 직접 쓸 수 없고, 채널을 수동으로 분리하거나 전처리기(Sass 등)의 도움이 필요하다.

 


 

비교 테이블

 

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

 


 

실용적 사례: 태그/뱃지 컴포넌트

 

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

 

dynamic-badge.css

/* oklch 방식으로 구현한 동적 뱃지 */
.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은 경험적으로 좋은 결과를 내지만, 모든 색상에서 WCAG AA 기준을 통과하는 것은 아니다. 특히 중간 명도의 채도 높은 색상(예: 선명한 초록, 선명한 주황)에서는 흑과 백 모두 대비율이 4.5:1 미만일 수 있다.

 

완벽한 접근성을 보장하려면 다음 두 가지를 병행하는 것이 좋다.

 

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

 

색상 팔레트를 제한하는 것이 자유도를 줄이는 것처럼 보일 수 있지만, 접근성과 일관성 측면에서는 오히려 더 나은 결과를 만든다. 비유하면 "아무 물감이나 쓸 수 있다"보다 "검증된 물감 30가지 중에서 고르세요"가 사고를 줄이는 것과 같다.

 


 

마무리

 

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