다크 모드 색을 미디어쿼리 두 벌로 쓰던 것을 한 줄로 — CSS light-dark()

  • 11th September 2026
  • 6 min read

다크 모드를 지원하는 CSS는 대개 이런 모양이다. 색마다 변수를 만들고, 미디어쿼리 안에서 같은 변수를 다시 선언한다.

:root {
  --bg: #ffffff;
  --ink: #1a1a1a;
  --line: #e0e0e0;
  --muted: #6b7280;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14171a;
    --ink: #f2f2f2;
    --line: #2c3136;
    --muted: #9aa4b2;
  }
}

동작은 한다. 대신 변수 목록을 두 벌 관리하게 된다. 색을 하나 추가하면 두 곳에 써야 하고, 한쪽만 고치면 어느 한 모드에서만 어긋난다. 변수가 스무 개쯤 되면 두 블록을 나란히 놓고 비교하는 일이 생긴다.

light-dark()는 두 값을 한 줄에 적는다.

:root {
  color-scheme: light dark;

  --bg:    light-dark(#ffffff, #14171a);
  --ink:   light-dark(#1a1a1a, #f2f2f2);
  --line:  light-dark(#e0e0e0, #2c3136);
  --muted: light-dark(#6b7280, #9aa4b2);
}

블록이 하나로 줄고, 한 색의 두 값이 같은 줄에 붙는다. 대비를 확인할 때 눈을 위아래로 옮기지 않아도 된다.

2024년 5월 13일 사파리 17.5가 합류하면서 Baseline에 들어왔다. 크롬 123(2024년 3월), 파이어폭스 120(2023년 11월)이 먼저 지원했으니 2년 넘게 쓸 수 있었던 기능이다. Baseline 진입 후 30개월이 지나는 2026년 11월쯤이면 "widely available"로 올라간다.

먼저 알아야 할 것 — color-scheme 없이는 다크 값이 나오지 않는다

가장 많이 걸리는 자리다. light-dark()만 쓰고 color-scheme을 빠뜨리면 OS가 다크 모드여도 라이트 값만 나온다. 크롬 152와 사파리 26.5에서 똑같이 확인된다.

선언 OS 라이트 OS 다크
color-scheme 없음라이트 값라이트 값
color-scheme: normal라이트 값라이트 값
color-scheme: light라이트 값라이트 값
color-scheme: dark다크 값다크 값
color-scheme: light dark라이트 값다크 값

헷갈리기 쉬운 점은 이때 prefers-color-scheme: dark 미디어쿼리는 참이라는 것이다. 브라우저는 OS가 다크라는 것을 알고 있고, 미디어쿼리로 물어보면 그렇다고 답한다. 그런데 light-dark()는 라이트 값을 준다. 이 둘이 보는 것이 다르기 때문이다.

  • prefers-color-scheme사용자가 무엇을 선호하는지를 본다.
  • light-dark()이 요소가 실제로 어느 스킴으로 그려지는지를 본다. 그것을 정하는 것이 color-scheme이고, 기본값 normal은 "라이트로 그린다"는 뜻이다.

MDN도 이 점을 전제로 적는다. "light-dark() 색 함수를 쓰려면 color-schemelight dark 값을 가져야 하며, 보통 :root에 설정한다."

그러니 light-dark()를 쓰는 문서에는 이 한 줄이 반드시 함께 간다.

:root { color-scheme: light dark; }

color-scheme 은 색 함수만을 위한 것이 아니다

이 한 줄은 light-dark()를 켜는 스위치이기만 한 것이 아니다. 브라우저가 직접 그리는 부분도 함께 바뀐다. 아래는 color-scheme: light dark만 선언하고 배경색·글자색을 CSS로 한 줄도 지정하지 않은 페이지다.

같은 페이지를 라이트 모드와 다크 모드에서 연 모습. CSS 로 배경색과 글자색을 지정하지 않았는데도 다크 모드에서는 배경이 검게, 글자가 희게, 입력란·체크박스·선택 상자·버튼이 모두 어두운 테마로 바뀌어 있다.
위가 라이트, 아래가 다크 — 선언한 것은 color-scheme: light dark 한 줄뿐이다

문서 배경, 기본 글자색, 입력란, 체크박스, 선택 상자, 버튼이 전부 따라 바뀐다. 스크롤바도 마찬가지다. 다크 모드에서 입력란만 하얗게 남아 눈을 찌르는 현상은 대개 이 선언이 없어서 생긴다. 색을 전부 직접 칠하는 사이트라도 이 한 줄은 넣어 두는 편이 낫다.

색 자리라면 거의 어디든 들어간다

크롬 152에서 실제로 계산값을 확인한 것들이다. 색이 오는 자리면 대체로 그대로 동작한다.

color:        light-dark(#1a1a1a, #f2f2f2);
border-color: light-dark(#e0e0e0, #2c3136);
box-shadow:   0 0 0 2px light-dark(#ddd, #333);      /* 색 자리에만 */
background:   linear-gradient(light-dark(#fff, #111), #888);
accent-color: light-dark(#2563eb, #60a5fa);
color:        color-mix(in srgb, light-dark(#000, #fff) 50%, #800000);
color:        light-dark(var(--l), var(--d));        /* 안에 var() 도 된다 */

currentColor와도 자연스럽게 섞인다. colorlight-dark()로 정하고 테두리를 currentColor로 잡으면 테두리도 같이 따라온다.

반대로 색이 아닌 값은 받지 않는다. 다음은 선언 자체가 무효가 되어 그 속성이 적용되지 않는다.

width:      light-dark(100px, 200px);          /* 무효 — 길이는 안 된다 */
box-shadow: light-dark(0 0 0 2px red, 0 0 0 8px blue);  /* 무효 — 그림자 전체는 안 된다 */

두 번째가 특히 헷갈린다. 그림자의 은 바꿀 수 있지만 번짐 크기까지 모드별로 다르게 하려면 여전히 미디어쿼리가 필요하다. 다크 모드에서 그림자를 더 넓게 퍼뜨리는 디자인이 흔한데, 그건 light-dark()로 안 된다.

이미지 형태는 아직 크롬만 된다

명세는 light-dark()에 이미지도 허용한다. MDN의 문법 정의에 이렇게 실려 있다.

light-dark( <color> , <color> )
light-dark( [<image> | none] , [<image> | none] )

되면 모드별 로고 교체가 CSS 한 줄이 된다. 실제로 크롬 152에서는 동작한다. 그런데 사파리 26.5에서는 무효다.

선언 크롬 152 사파리 26.5
color: light-dark(…, …)동작동작
background-image: light-dark(url(a), url(b))동작무효 (none)

사파리에서는 배경 이미지가 아예 없는 것이 된다. 폴백도 남지 않는다. Baseline 2024-05-13이라는 판정은 색 형태에 대한 것이고, 나중에 붙은 이미지 형태는 자기 시계로 따로 간다. 모드별 이미지 교체는 당분간 미디어쿼리나 <picture>로 하는 편이 안전하다.

변수에 담아도 쓰는 쪽 기준으로 해석된다

이 성질이 light-dark()를 쓸모 있게 만든다. :root에서 변수를 정의해도, 그 변수가 실제로 사용되는 요소의 color-scheme을 기준으로 값이 정해진다.

:root {
  color-scheme: light dark;
  --ink: light-dark(#1a1a1a, #f2f2f2);
}

.panel--light { color-scheme: light; }
.panel--dark  { color-scheme: dark; }

.panel p { color: var(--ink); }   /* 같은 변수, 다른 결과 */

실측하면 .panel--light 안에서는 #1a1a1a, .panel--dark 안에서는 #f2f2f2가 나온다. 변수를 두 벌 만들지 않고도 구역 단위로 스킴을 뒤집을 수 있다.

미디어쿼리 방식에서 같은 일을 하려면 어떤가. @media (prefers-color-scheme: dark)는 문서 전체에 걸리는 조건이라, "이 패널만 반대로"를 표현할 수 없다. 클래스를 따로 만들어 변수를 다시 선언하는 수밖에 없다. 다크 테마 위에 밝은 카드를 하나 얹거나, 코드 블록만 항상 어둡게 두는 식의 요구가 이 방식에서 특히 번거로웠다.

color-scheme은 상속되므로 그 구역 안의 폼 컨트롤과 스크롤바까지 함께 뒤집힌다. 색 변수만 바꿨을 때 남던 어색함이 사라진다.

미디어쿼리를 완전히 대체하지는 않는다

다음은 여전히 @media (prefers-color-scheme: dark)가 필요하다.

  • 색이 아닌 값 — 그림자 번짐 크기, 테두리 두께, 투명도 값 자체
  • 모드별 이미지 교체 — 사파리가 아직 안 되므로
  • 규칙 자체를 켜고 끄는 것 — 다크에서만 특정 요소를 감추거나 다른 레이아웃을 쓰는 경우
  • 필터·혼합 모드 — 다크에서 이미지 밝기를 낮추는 filter: brightness() 같은 것

정리하면 색은 light-dark()로, 색이 아닌 것은 미디어쿼리로 나누는 그림이 된다. 대개 색이 압도적으로 많으므로 미디어쿼리 블록은 남더라도 훨씬 짧아진다.

정리

  • light-dark(라이트값, 다크값)으로 두 모드의 색을 한 줄에 적는다. Baseline 2024-05-13(사파리 17.5)부터라 이미 쓸 수 있다.
  • :root { color-scheme: light dark; }가 반드시 함께 가야 한다. 없으면 OS가 다크여도 라이트 값만 나온다. prefers-color-scheme이 참인 것과는 별개다.
  • 그 한 줄은 브라우저가 그리는 부분—문서 배경, 입력란, 버튼, 스크롤바—도 함께 바꾼다. 색을 직접 다 칠하는 사이트에도 넣어 둘 값어치가 있다.
  • 색이 오는 자리면 대체로 동작한다. box-shadow색 부분만이고, 길이나 그림자 전체는 받지 않는다.
  • 이미지 형태는 크롬만 된다. 사파리 26.5에서는 무효가 되어 배경이 사라진다.
  • 변수에 담아도 쓰는 요소의 color-scheme 기준으로 해석되므로, 구역 단위로 스킴을 뒤집기 쉽다. 미디어쿼리로는 하기 번거롭던 일이다.

기능 자체는 2년 전에 나왔고 문법도 한 줄이라 새로 배울 것이 거의 없다. 실제로 막히는 지점은 color-scheme을 빠뜨려 "왜 다크가 안 먹지"로 시간을 쓰는 자리 하나다. 그 한 줄만 기억하면 된다.

참고