이제 다크모드는 :root에 color-scheme: light dark를 켜고, 색 토큰을 light-dark(밝은색, 어두운색) 한 줄로 적으면 됩니다. 시스템 선호를 자동으로 존중하고, 변수 정의가 절반으로 줄며, 두 테마가 항상 같은 자리에서 관리됩니다. 단, 최신 브라우저 기준(2024년 5월 도입)이라 구형용 폴백은 함께 둡니다.
요약
color-scheme: light dark를 켜면 스크롤바·폼 기본 UI까지 명암이 맞고, 시스템 선호가 자동 반영된다.light-dark(a, b)는 한 줄에 두 테마 색을 같이 적어 변수 정의를 절반으로 줄인다.- 예전의 JS 토글·중복 변수보다 코드량이 적고, 한쪽 테마만 빠뜨리는 실수가 줄어 일관성이 좋다.
- 2024년 5월 3대 엔진 도입(Baseline 신규)이라 동작은 하지만, 구형 브라우저용 폴백을 함께 두는 게 안전하다.
저는 색을 맡은 사람입니다. 디자이너가 정한 팔레트를 실제 화면에서 ‘눈이 편하게, 그리고 어떤 환경에서도 읽히게’ 살려내는 게 제 일이죠. 그중에서도 다크모드는 늘 골치였습니다. 색을 두 벌 관리한다는 건 결국 ‘진실의 출처가 두 곳’이라는 뜻이고, 출처가 둘이면 언젠가 어긋납니다. 그런데 최근에 이 영역이 함수 하나로 정리됐습니다. 오늘은 그 이야기를 합니다.
예전엔 왜 그렇게 번거로웠을까요?
과거의 다크모드는 보통 두 갈래였습니다. 하나는 @media (prefers-color-scheme: dark) 미디어 쿼리 안에서 같은 변수를 다시 선언하는 방식. 다른 하나는 JS로 <html>에 클래스를 토글하고, 그 클래스마다 색 변수를 또 한 벌 선언하는 방식이었죠. 어느 쪽이든 같은 토큰을 두 번 적어야 했습니다. 토큰이 40개면 80줄, 게다가 새 색을 추가할 때마다 ‘두 곳 다’ 손대야 했습니다. 빠뜨리면 다크모드에서만 글자가 안 보이는 버그가 났고요.
그래서, 지금은 어떻게 바뀌었나요?
핵심은 두 가지입니다. 먼저 :root에 color-scheme: light dark를 선언합니다. 이 한 줄이 “이 사이트는 두 스킴을 지원한다”고 브라우저에 알리고, 스크롤바·기본 폼 컨트롤 같은 브라우저 UI의 명암까지 알아서 맞춰줍니다. 그다음 색 토큰을 light-dark() 함수로 적습니다.
light-dark()는 활성 색 스킴에 따라 라이트면 첫 번째 값, 다크면 두 번째 값을 돌려줍니다. prefers-color-scheme 미디어 쿼리를 따로 쓰지 않아도 되죠. 코드로 보면 이렇게 단순해집니다.
토큰만 갈아끼우는 다크/라이트
버튼으로 테마를 바꿔보세요. light-dark()로 더 간단해진 영역입니다.
검색과 AI에 찾아지게
같은 구조에 색 토큰만 바꿔 테마를 만듭니다.
위 위젯은 색 토큰 한 묶음만 바꿔 테마를 만드는 모습을 보여줍니다. 실제 코드에서 light-dark()를 쓰면 토큰 선언 자체가 이렇게 짧아집니다.
:root {
color-scheme: light dark; /* 두 스킴 지원 선언 */
--bg: light-dark(#ffffff, #0e1116);
--text: light-dark(#16181d, #e8eaed);
--brand: light-dark(#0a7d66, #34e3b8);
}
body { background: var(--bg); color: var(--text); }
예전 같으면 위 세 토큰을 라이트용 한 벌, 다크용 한 벌로 총 여섯 줄을 적고 미디어 쿼리로 감쌌을 겁니다. 지금은 세 줄이면 끝이고, 두 테마가 한 줄 안에 나란히 있어 한눈에 짝이 맞는지 보입니다.
위 코드의 토큰이 두 스킴에서 어떤 색으로 갈리는지, 실제 값 그대로 칩으로 펼쳐 둡니다. 한 줄 안에 라이트·다크가 짝으로 들어 있다는 게 이렇게 보입니다.
다크의 브랜드 색(#34e3b8)이 라이트(#0a7d66)보다 밝고 채도가 낮은 게 보이시죠 — 앞서 말한 ‘어두운 배경에서 번지지 않게 조율’이 이 한 쌍에 들어 있습니다.
그럼 JS 토글 버튼은 이제 필요 없나요?
시스템 설정을 그대로 따라가게 둘 거라면 JS가 거의 필요 없습니다. 다만 “사용자가 사이트 안에서 직접 테마를 고르게” 하려면 여전히 약간의 제어가 필요합니다. 이때도 옛날처럼 변수를 두 벌 토글하는 게 아니라, <html>의 color-scheme(또는 작은 클래스 하나)만 바꾸면 light-dark()가 알아서 다른 값을 돌려줍니다. 손대는 곳이 ‘색 80줄’에서 ‘스킴 한 줄’로 줄어드는 셈입니다.
color-scheme를 켜는 게 왜 그렇게 중요한가요?
두 가지 이유입니다. 첫째, light-dark()는 color-scheme에 light dark가 선언돼 있어야 비로소 동작합니다. 이 선언이 함수의 ‘스위치’예요. 둘째, 이 속성은 우리가 직접 색을 칠하지 않는 브라우저 기본 UI—스크롤바, 입력창의 기본 테두리, 자동완성 배경—의 명암까지 같이 맞춰줍니다. 다크 배경 위에 ‘하얗게 튀는 스크롤바’가 사라지는 거죠. 그래서 저는 이걸 ‘다크모드의 0번 단계’라고 부릅니다.
최신 기능인데, 실무에 바로 써도 괜찮나요?
핵심만 짚으면: light-dark()는 2024년 5월 세 개의 주요 브라우저 엔진에 모두 도입되며 ‘Baseline 신규 사용 가능(newly available)’ 상태가 됐습니다. 즉 최신 브라우저에서는 잘 돌지만, ‘널리·안정적으로 사용 가능(widely available)’ 단계는 그보다 시간을 두고 옵니다. 그래서 저는 light-dark()를 점진적 향상으로 씁니다. 구형 브라우저에는 평범한 단색 폴백을 먼저 깔고, 그 위에 light-dark()를 얹는 식이죠. 새 브라우저는 멋진 자동 테마를, 옛 브라우저는 최소한 ‘읽히는 화면’을 받습니다.
대비(명암)는 어떻게 지키나요?
도구가 편해졌다고 색 감각까지 자동이 되진 않습니다. 다크모드에서 가장 흔한 실수가 검은 배경에 순백(#fff) 글자를 그대로 얹는 것입니다. 대비가 너무 강하면 글자가 번지고 잔상이 남아요. 저는 흰색을 살짝 낮추고(예: 부드러운 라이트 그레이), 배경은 완전한 검정 대신 약간 푸른 기가 도는 진회색을 씁니다. 그러면서도 본문 대비는 WCAG 권장선을 넘기게 둡니다. light-dark()는 ‘어떤 색을 쓸지’는 여전히 사람이 정하게 두고, ‘언제 바꿀지’만 대신해 줍니다. 딱 좋은 분업이죠.
이 함수가 아직 못 하는 것
솔직하게 한계도 적겠습니다. 현재 light-dark()는 색 값(과 이미지)만 받습니다. 그리고 이름 그대로 라이트·다크 두 가지만 다룹니다. ‘고대비’ 같은 제3의 테마를 만들고 싶다면 이 함수만으로는 안 되고, 별도 클래스나 미디어 쿼리를 함께 써야 합니다. 위 위젯에 ‘액센트’ 테마를 따로 넣은 것도 같은 이유에서—세 번째 테마는 토큰 묶음을 바꾸는 방식으로 다룹니다. W3C에서 두 가지를 넘어서는 확장을 논의 중이지만, 오늘 기준으로는 ‘둘’입니다.
| 항목 | JS 토글 · 중복 변수 | color-scheme + light-dark() |
|---|---|---|
| 코드량 | 토큰을 라이트·다크 두 벌 | 한 줄에 두 색 → 정의 절반 |
| 유지보수 | 새 색마다 두 곳 수정 | 한 곳에서 짝으로 관리 |
| 일관성 | 한쪽 빠뜨리면 깨짐 | 두 값이 나란히 → 누락 ↓ |
| 브라우저 기본 UI | 스크롤바·폼 따로 안 맞음 | color-scheme가 명암 자동 |
다른 담당자와의 연결
색은 혼자 일하지 않습니다. 이 테마 기법은 곧바로 옆 자리의 작업과 이어집니다. 색의 의미를 정하는 컬러 담당자의 노트에서 ‘왜 이 색인가’를, 팔레트를 짜는 과정에서 ‘라이트·다크 두 벌을 어떻게 동시에 설계하는가’를 보실 수 있습니다. 그리고 이 모든 색이 화면에 앉는 틀은 디자인(UI)을 만드는 방식에서 다룹니다.
light-dark()는 지금 실무에 써도 되나요?
light-dark()를 쓰려면 무엇을 먼저 해야 하나요?
예전 방식(JS 토글·변수 두 벌)과 무엇이 다른가요?
light-dark()의 한계는 무엇인가요?
다크모드에서 가장 흔한 실수는 무엇인가요?
색을 맡은 사람의 노트
‘왜 이 색인가’를 정하는 일.
팔레트는 이렇게 짭니다
라이트·다크를 동시에 설계.
디자인은 이렇게 합니다
색이 앉는 틀을 만드는 방식.
이 글의 브라우저 지원 정보는 작성 시점(2026-06-24) 기준이며, light-dark()는 2024년 5월 3대 엔진 도입(Baseline 신규)으로 확인했습니다. 지원 범위는 시간이 지나며 변동하므로 적용 전 최신 호환성을 다시 확인하시기 바랍니다. 날조된 사례·수치는 사용하지 않았습니다.