화면의 색을 최대 대비로 바꿔 쓰는 사람들이 있습니다. 검은 배경에 노란 글씨, 파란 배경에 흰 글씨처럼요. 저시력 사용자에게는 이게 글자를 읽을 수 있느냐 없느냐의 문제고, 편두통이나 빛 과민이 있는 사람은 반대로 자극을 줄인 저대비 팔레트를 쓰기도 합니다. 공통점은 하나예요 — 색을 사용자가 직접 정한다는 것. 그리고 그 순간, 우리가 CSS에 적어둔 색은 상당수 무시됩니다.

이 글은 그 “무시당하는 순간"을 다룹니다. 내가 칠한 색이 OS에 덮이는 고대비 모드에서, 화면이 무너지지 않게 만드는 법이요. 사실 이건 다크모드 제대로 구현하기의 후속편이에요 — 그 글 끝에서 “다크 토큰마저 OS가 덮어쓴다"고 슬쩍 흘렸던 이야기를 여기서 제대로 풉니다. 익숙한 도구(미디어 쿼리·CSS 변수)를 그대로 쓰지만, 사고방식은 정반대로 뒤집어야 하고요. 다크모드 글을 안 봤어도 괜찮습니다 — 필요한 배경은 이 글 안에서 짚어갈게요.

고대비는 내가 칠하는 게 아닙니다

다크모드를 만들 때 우리는 색을 골랐습니다. 어두운 배경엔 어떤 강조색이 어울릴지 고민하고, 토큰을 설계하고, 대비비를 맞췄죠. 주도권이 개발자에게 있었어요.

고대비 모드는 정반대입니다. 사용자가 OS에서 고대비를 켜면, 브라우저는 사용자가 고른 팔레트로 화면을 다시 칠합니다. 내가 정한 배경색, 글자색, 테두리색이 그 팔레트의 몇 가지 색으로 강제 치환돼요. 열심히 페인트칠한 벽에 관리사무소가 와서 규정 색으로 다시 칠하고 가는 셈입니다. 이걸 forced-colors 모드(강제 색 모드)라고 불러요 — 대표적으로 윈도우의 고대비 설정(윈도우 11에서는 ‘대비 테마’)이 이걸 켭니다.

그래서 다크모드용으로 아무리 예쁜 다크 토큰을 만들어도, forced-colors가 켜지면 그 색은 대부분 안 먹힙니다. 다크모드 지원과 고대비 지원은 별개의 작업이에요. 이 글을 끝까지 보면, forced-colors가 켜졌을 때 무엇이 덮이고 무엇이 안 덮이는지 알고, 경계·상태·포커스가 사라지지 않게 만들고, 사용자가 더 높은 대비를 원할 때(prefers-contrast) 응답하고, 이걸 직접 테스트할 수 있게 됩니다.

다크모드와 고대비 모드에서 색의 주도권이 누구에게 있는지 비교하는 다이어그램 - 왼쪽 다크모드는 개발자가 팔레트를 직접 골라 적용하고 브라우저가 그대로 그리지만, 오른쪽 고대비 모드는 사용자가 OS에서 고른 제한된 팔레트가 개발자의 색을 강제로 덮어써서 forced-colors가 활성화됩니다. 다크모드는 색을 고르는 일, 고대비는 색을 양보하고 응답하는 일이라는 설명이 붙어 있습니다
다크모드와 고대비 모드에서 색의 주도권이 누구에게 있는지 비교하는 다이어그램 - 왼쪽 다크모드는 개발자가 팔레트를 직접 골라 적용하고 브라우저가 그대로 그리지만, 오른쪽 고대비 모드는 사용자가 OS에서 고른 제한된 팔레트가 개발자의 색을 강제로 덮어써서 forced-colors가 활성화됩니다. 다크모드는 색을 고르는 일, 고대비는 색을 양보하고 응답하는 일이라는 설명이 붙어 있습니다

세 가지 용어부터

앞으로 나올 세 가지를 먼저 한 줄씩 정리하고 갈게요.

  • forced-colors 모드: OS가 사용자 팔레트로 색을 강제 치환하는 상태. @media (forced-colors: active)로 감지합니다.
  • 시스템 색 키워드: Canvas(배경), CanvasText(본문 글자), LinkText(링크)처럼 현재 사용자 팔레트의 색을 가리키는 이름. forced-colors 안에서 이 이름을 쓰면 사용자가 고른 색을 그대로 따릅니다.
  • prefers-contrast: 사용자가 더 높은/낮은 대비를 원하는지에 대한 선호. @media (prefers-contrast: more)로 감지합니다. forced-colors와 달리, 여기서는 내가 여전히 색을 통제하되 더 강한 대비 버전을 내주는 거예요.

forced-colors와 prefers-contrast는 자주 헷갈리는데, 경계는 분명합니다. forced-colors는 OS가 강제(내 색이 덮임), prefers-contrast는 사용자 선호에 내가 응답(내 색을 더 세게). 둘 다 대응하면 좋고요.

forced-colors에 응답하기

forced-colors가 켜지면 브라우저가 알아서 많은 걸 해줍니다. 배경색·글자색·테두리색이 사용자 팔레트로 치환되고, 시맨틱 <button>·링크·폼 요소는 적절한 시스템 색으로 매핑돼요. 그래서 시맨틱 HTML로 잘 만든 페이지는 대체로 그냥 잘 동작합니다. 문제가 생기는 건 우리가 색에 의미를 담아둔 지점이에요.

무엇이 덮이고, 무엇이 안 덮이는지 감을 잡아둡시다.

  • 덮인다: color, background-color, border-color → 시스템 색으로 치환. box-shadow, text-shadow아예 제거.
  • 안 덮인다: <img> 사진, 그리고 forced-color-adjust: none으로 명시적으로 빼둔 요소.

여기서 가장 흔한 사고가 나옵니다 — 배경색만으로 만든 경계. 카드나 버튼을 배경색 차이로만 구분했다면, forced-colors에서 그 배경들이 전부 같은 시스템 색이 되면서 경계가 증발합니다. 그림자로 띄운 카드도 그림자가 사라져 납작해지고요.

forced-colors 모드에서 UI 요소가 살아남는지 비교하는 다이어그램 - 왼쪽은 배경색과 그림자만으로 만든 버튼과 카드로, forced-colors가 켜지면 배경이 같은 시스템 색으로 치환되고 그림자가 제거되어 경계가 사라집니다. 오른쪽은 테두리와 outline으로 형태를 전달한 버튼과 카드로, 테두리 색이 시스템 색으로 치환되며 경계가 그대로 살아남습니다. 색이 아니라 형태로 정보를 전달하라는 원칙이 강조되어 있습니다
forced-colors 모드에서 UI 요소가 살아남는지 비교하는 다이어그램 - 왼쪽은 배경색과 그림자만으로 만든 버튼과 카드로, forced-colors가 켜지면 배경이 같은 시스템 색으로 치환되고 그림자가 제거되어 경계가 사라집니다. 오른쪽은 테두리와 outline으로 형태를 전달한 버튼과 카드로, 테두리 색이 시스템 색으로 치환되며 경계가 그대로 살아남습니다. 색이 아니라 형태로 정보를 전달하라는 원칙이 강조되어 있습니다

해법은 색이 아니라 형태로 경계를 전달하는 것입니다. 테두리를 쓰면 그 테두리색이 시스템 색으로 치환되면서 경계가 살아남아요.

css
@media (forced-colors: active) {
  .card {
    /* 배경색 대신 테두리로 경계를 만든다 — forced-colors는 커스텀 배경을 덮어버린다 */
    border: 1px solid CanvasText;
  }
}

버튼도 마찬가지예요. <div>에 배경색만 입혀 버튼처럼 꾸몄다면, forced-colors에서 그냥 배경에 녹아버립니다. 시맨틱 <button>을 쓰면 브라우저가 알아서 버튼용 시스템 색을 입혀주고, 커스텀 버튼이라면 테두리로 형태를 명시하세요.

css
@media (forced-colors: active) {
  .btn-custom {
    border: 1px solid ButtonText;   /* 색이 아니라 테두리로 '버튼임'을 전달 */
  }
}

포커스 표시도 조심해야 합니다. 포커스 링을 box-shadow로 그렸다면 forced-colors에서 사라져서, 키보드 사용자가 지금 어디에 있는지 알 수 없게 돼요. outline은 치환되어 살아남으니, 포커스는 outline으로 그리는 게 안전합니다.

css
:focus-visible {
  /* box-shadow는 forced-colors에서 제거되므로 outline으로 그린다 */
  outline: 2px solid CanvasText;
  outline-offset: 2px;
}

forced-color-adjust는 아껴서

가끔은 색 자체가 정보인 경우가 있습니다. 색상 팔레트 견본, 상태를 색으로 나타낸 배지, 브랜드 로고 스와치처럼요. 이런 요소까지 시스템 색으로 덮이면 정보가 사라지죠. 이럴 때만 forced-color-adjust: none으로 그 요소를 색 강제에서 제외합니다.

css
.color-swatch {
  forced-color-adjust: none;   /* 색 자체가 정보인 요소만 예외 처리 */
}

주의할 건, 이걸 body나 전역에 걸어 forced-colors 자체를 무력화하지 말 것. 사용자가 일부러 켠 접근성 기능을 개발자가 꺼버리는 셈이라, 저시력 사용자에겐 화면이 다시 안 보이게 됩니다. 꼭 필요한 개별 요소에만, 아껴서 쓰세요.

prefers-contrast: 사용자가 더 세게 원할 때

forced-colors가 “OS가 색을 강제하는” 상황이라면, prefers-contrast: more는 “사용자가 더 높은 대비를 원한다“는 부드러운 신호입니다. 여기서는 내 색을 뺏기는 게 아니라, 내가 더 강한 대비 버전을 준비해 응답하면 돼요. 다크모드에서 토큰을 갈아끼우던 것과 똑같은 방식입니다.

css
@media (prefers-contrast: more) {
  :root {
    --color-text-subtle: #1a1a2e;   /* 흐린 회색 보조 텍스트를 진하게 */
    --color-border: #000000;         /* 옅은 테두리를 또렷하게 */
  }
}

흐릿한 보조 텍스트, 옅은 구분선, 은은한 플레이스홀더 — 평소엔 “세련된” 이 저대비 요소들이 대비를 원하는 사용자에겐 가장 먼저 안 보이는 것들입니다. prefers-contrast: more에서 이들만 또렷하게 올려주면, 디자인을 해치지 않으면서 필요한 사람에게 필요한 대비를 줄 수 있어요.

직접 만든 고대비 토글, 그리고 왜 멈췄나

솔직한 경험담 하나. 이 블로그에도 헤더에 고대비 토글 버튼을 직접 붙여본 적이 있습니다. 클릭하면 사이트 전체가 자체 고대비 테마로 바뀌는, 다크 토글의 사촌 같은 기능이었죠. 그런데 지금은 꺼두었습니다.

이유는 이랬어요. 커스텀 고대비 테마는 모든 컴포넌트의 대비를 내 손으로 다 맞춰야 합니다. 본문만이 아니라 카드, 목차(TOC), 태그, 카테고리, 페이지네이션, 관련 글 목록까지 — 한 군데라도 빠지면 거기서 대비가 무너지거든요. 페이지네이션 숫자 하나의 대비를 맞추느라 시간을 쏟다 보면, 이게 맞나 싶어집니다. 게다가 다크 토글과 고대비 토글이 동시에 켜지면 어떤 색이 이겨야 하는지도 정리해야 하고요.

그러다 깨달은 게 있습니다. forced-colors와 prefers-contrast는 브라우저와 OS가 이미 그 무거운 일을 대신 해준다는 거예요. 내가 시스템 색 키워드로 경계만 잘 남겨두면, 모든 컴포넌트의 대비를 일일이 손보지 않아도 사용자 팔레트가 전역에 일관되게 적용됩니다. 그래서 지금 제 결론은 이렇습니다 — 커스텀 고대비 토글은 나중에 얹는 선택이고, 기반은 forced-colors를 존중하는 것. 순서를 거꾸로 잡으면 밤을 새우게 됩니다. 어떻게 알고 있냐구요..? …저도 알고 싶지 않았습니다. ㅜㅜ

어려운 게 맞습니다 — 그래도 하는 이유

이게 저 혼자만의 엄살은 아니에요. 언젠가 웹 접근성 행사에서 카카오의 디지털 접근성 책임자 김혜일 님이 이런 말을 남긴 적이 있습니다 — 다크모드보다도 더 어려운 게 고대비 모드라고요. 이 글에서 내내 확인한 그대로죠. 색을 내 마음대로 고르는 다크모드와 달리, 고대비는 사용자와 OS가 정해둔 규칙 안에서 화면이 무너지지 않게 지켜내야 하니까요.

그런데 그 말에는 중요한 뒷문장이 있었습니다 — 그럼에도 카카오는 사용자를 위해 고대비 모드에 힘을 쏟고 있다는 거예요. 어렵다는 건 안 하는 핑계가 아니라, 제대로 해내면 그만큼 의미가 큰 영역이라는 뜻이기도 하죠. 수백만 명이 쓰는 서비스가 이 “어려운 일"에 자원을 들이는 이유는 단순합니다. 그 화면 너머에, 고대비가 아니면 글자를 아예 못 읽는 사용자가 실제로 있으니까요.

그러니 우리가 할 일은 그 어려움을 통째로 떠안는 게 아니라, forced-colors를 존중하는 것부터 한 겹씩 쌓는 거예요. 거기서부터가 “어려운 일"을 지속 가능하게 만드는 첫걸음입니다.

테스트하는 법

눈으로 확인하지 않으면 고대비 대응은 반쪽입니다. 두 가지 방법이 있어요.

  1. 실제 OS 설정: 윈도우의 ‘대비 테마’(설정 → 접근성 → 대비 테마)를 켜고 사이트를 봅니다. 가장 정확해요.
  2. 브라우저 에뮬레이션: 크롬·엣지 개발자도구의 Rendering 탭에서 Emulate CSS media feature forced-colorsactive로 바꾸면, OS 설정을 안 건드리고도 forced-colors 화면을 볼 수 있습니다. prefers-contrast도 같은 탭에서 에뮬레이트됩니다.

에뮬레이션으로 빠르게 훑고, 배포 전에 실제 대비 테마로 한 번 더 확인하는 흐름을 권합니다.

한 장 요약

  • 고대비 모드(forced-colors)는 다크모드와 별개 — 다크는 내가 색을 고르고, 고대비는 OS가 내 색을 덮는다. 다크 토큰을 잘 만들어도 forced-colors엔 안 먹힘
  • @media (forced-colors: active)로 감지, 시스템 색 키워드(Canvas·CanvasText·ButtonText…)로 사용자 팔레트를 따름
  • 덮이는 것: 배경·글자·테두리 색(치환), 그림자(제거) / 배경색만으로 만든 경계는 사라진다 → 테두리·outline으로 형태를 전달
  • 포커스는 box-shadow가 아니라 **outline**으로(그림자는 제거됨). forced-color-adjust: none은 색이 정보인 요소에만 아껴서
  • prefers-contrast: more는 사용자 선호 신호 — 흐린 보조 텍스트·옅은 구분선만 또렷하게 올려 응답. 테스트는 개발자도구 에뮬레이션 + 실제 대비 테마

정리하며

고대비 대응의 핵심은 마음가짐 하나로 요약됩니다 — 색을 뺏겨도 무너지지 않게. 색이 사라져도 형태(테두리·outline·시맨틱 구조)가 남아 있으면, 사용자가 어떤 팔레트를 강제하든 화면은 읽힙니다. 그리고 그건 사실 좋은 마크업의 부산물이기도 해요. 색에 기대지 않고 구조로 의미를 전달하는 습관은, 고대비 사용자뿐 아니라 모두에게 더 튼튼한 화면을 만들어주니까요.

다크모드로 색을 고르는 즐거움을 누렸다면, 고대비는 그 색을 기꺼이 내려놓는 연습입니다. 둘 다 할 줄 알면, 밝은 곳에서든 어두운 곳에서든 최대 대비가 필요한 순간에든 — 아무도 화면 밖으로 밀려나지 않습니다.


질문으로 다시 보기

다크모드를 지원하면 고대비 모드도 되는 건가요?
아닙니다. 다크모드는 개발자가 어두운 팔레트를 직접 골라 적용하는 것이고, 고대비 모드(forced-colors)는 OS가 사용자가 고른 제한된 팔레트로 개발자의 색을 강제로 덮어씁니다. 방향이 반대라, 다크 토큰을 아무리 잘 만들어도 forced-colors가 켜지면 그 색은 무시됩니다. forced-colors 미디어 쿼리로 따로 대응해야 합니다.
forced-colors 모드에서 왜 내 버튼이 안 보이나요?
배경색만으로 버튼을 구분했기 때문입니다. forced-colors는 커스텀 배경색을 시스템 색으로 덮어버려서, 배경색 차이로만 표현한 경계는 사라집니다. 테두리(border)나 outline으로 형태를 전달하면 그 값이 시스템 색으로 치환되어 살아남습니다. 가능하면 시맨틱 button 요소를 쓰는 것도 도움이 됩니다.

이 시리즈의 다른 글

프론트엔드 × 접근성 시리즈 전체 보기 — 다크모드, SPA 포커스 관리, 폼 UX, 모달까지 “일반 프론트엔드 주제에 접근성 시각을 연결"하는 연재입니다.