튜토리얼

CSS light-dark() 기능: 기본 테마 전환

7분 읽기

다크 모드 구현에는 역사적으로 모든 사용자 정의 속성을 재정의하는 prefers-color-scheme 미디어 쿼리, 사용자 토글을 처리하는 JavaScript, 기본 설정을 유지하는 localStorage, 페이지 로드 시 잘못된 테마가 깜박이는 것을 방지하는 <head>의 인라인 스크립트 등 적지 않은 양의 상용구가 필요했습니다. CSS light-dark() 기능은 이 모든 것을 제거하지는 않지만 문제의 CSS 노출 영역을 극적으로 줄입니다.

light-dark()는 정확히 두 가지 색상 값을 취하고 활성 색상 구성표가 밝을 때 첫 번째 값을 반환하거나 활성 색상 구성표가 어두울 때 두 번째 값을 반환하는 CSS 색상 함수입니다. 이는 색상에 대한 삼항 연산자와 동일한 의미론적 CSS입니다.

명암()이란 무엇인가요?

함수 서명은 간단합니다.

color: light-dark(<light-color>, <dark-color>);

활성 색 구성표가 밝은 경우 브라우저는 <light-color>를 사용합니다. 어두워지면 <dark-color>를 사용합니다. "활성 색 구성표"는 color-scheme CSS 속성에 의해 결정되며, 이는 시스템 prefers-color-scheme 미디어 쿼리 또는 요소에 설정된 명시적 값에 응답합니다.

이 기능은 다음에서 지원됩니다.

  • Chrome/Edge: 버전 123 이후(2024년 3월)
  • Firefox: 버전 120 이후(2023년 11월)
  • Safari: 버전 17.5 이후(2024년 6월)

글로벌 지원은 2026년 초 기준 약 85%입니다. 비교적 최근에 추가된 것이지만 대체 전략을 통해 프로덕션에서 사용할 수 있을 만큼 브라우저 적용 범위가 빠르게 성장하고 있습니다.

color-scheme 속성의 작동 방식

light-dark()는 단독으로 작동하지 않습니다. 이는 color-scheme CSS 속성이 올바르게 설정되었는지에 전적으로 달려 있습니다. 이것이 없으면 함수는 반환할 값을 결정할 컨텍스트가 없습니다.

color-scheme 속성은 문서나 요소가 지원하는 색 구성표를 선언합니다. :root에 설정하는 것이 시작점입니다.

:root {
  color-scheme: light dark;
}

이 단일 선언은 페이지가 밝은 색 구성표와 어두운 색 구성표를 모두 지원한다는 것을 브라우저에 알려줍니다. 그런 다음 브라우저는 다음을 수행합니다.

  1. 사용자의 prefers-color-scheme 시스템 기본 설정을 읽습니다.
  2. 해당 방식을 적용합니다
  3. 페이지의 모든 light-dark() 호출을 적절한 값으로 확인합니다.

이를 사용하면 테마 인식 색상을 정의하는 것이 단일 선언을 작성하는 문제가 됩니다.

:root {
  color-scheme: light dark;

  --color-background: light-dark(#FFFFFF, #0F0F17);
  --color-text:       light-dark(#1A1A2E, #E8E8F0);
  --color-border:     light-dark(#DEE2E6, #2E2E4A);
  --color-accent:     light-dark(#2563EB, #60A5FA);
}

미디어 쿼리가 없습니다. 선택기가 재정의되지 않습니다. 색상당 하나의 선언, 두 값 모두 인라인입니다. 브라우저는 시스템 기본 설정에 따라 자동으로 전환을 처리합니다.

단일 구성표로 제한

color-scheme: light 또는 color-scheme: dark를 설정하면 시스템 기본 설정에 관계없이 단일 구성표가 적용됩니다.

/* Always light, regardless of OS preference */
.widget {
  color-scheme: light;
  background: light-dark(#FFFFFF, #0F0F17);
  /* Always resolves to #FFFFFF */
}

/* Always dark */
.dark-panel {
  color-scheme: dark;
  color: light-dark(#1A1A2E, #E8E8F0);
  /* Always resolves to #E8E8F0 */
}

이는 특정 모드에서 항상 나타나야 하는 UI 구성 요소에 유용합니다. 예를 들어 주변 페이지 테마에 관계없이 항상 어두운 배경을 가져야 하는 코드 편집기입니다.

only 키워드

only를 추가하면 캐스케이드가 해당 요소에 대한 구성표를 재정의하는 것을 방지할 수 있습니다.

.forced-light {
  color-scheme: only light;
}

이는 밝게 유지되어야 하는 어두운 모드 컨텍스트 내부에 요소가 있을 때 주로 유용합니다.

Prefers-color-scheme 미디어 쿼리 교체

미디어 쿼리를 사용하는 다크 모드에 대한 기존 접근 방식에서는 모든 색상 변수를 복제하거나 재정의해야 합니다.

/* Traditional approach — verbose */
:root {
  --bg: #FFFFFF;
  --text: #1A1A2E;
  --accent: #2563EB;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0F0F17;
    --text: #E8E8F0;
    --accent: #60A5FA;
  }
}

light-dark()를 사용하면 다음과 같이 축소됩니다.

/* light-dark() approach — one declaration per variable */
:root {
  color-scheme: light dark;

  --bg:     light-dark(#FFFFFF, #0F0F17);
  --text:   light-dark(#1A1A2E, #E8E8F0);
  --accent: light-dark(#2563EB, #60A5FA);
}

변수 자체가 자기 설명적이 됩니다. --accent: light-dark(#2563EB, #60A5FA)를 읽으면 두 값을 즉시 확인하고 관계를 이해할 수 있습니다. 미디어 쿼리 접근 방식은 두 개의 개별 블록에 밝은 값과 어두운 값을 분산시켜 팔레트 감사 및 업데이트를 더 어렵게 만듭니다.

미디어 쿼리가 여전히 필요한 경우

prefers-color-scheme 미디어 쿼리는 테마에 따라 변경되는 비색상 적응에 여전히 필요합니다.

@media (prefers-color-scheme: dark) {
  /* Non-color adjustments that light-dark() cannot express */
  img.logo {
    filter: invert(1) brightness(1.2);
  }

  .hero-image {
    opacity: 0.85;
  }
}

순전히 색상 변경인 경우에는 light-dark()가 더 깨끗합니다. 구조적 또는 색상 이외의 적응(이미지 필터, 불투명도, 디스플레이 속성)의 경우 미디어 쿼리는 여전히 올바른 도구입니다.

CSS 사용자 정의 속성과 결합

light-dark()는 모든 기능이 발휘되는 사용자 정의 속성 값 내에서 작동합니다. :root에서 모든 테마 인식 색상을 정의하고 페이지의 모든 구성 요소가 이러한 변수를 참조합니다. 색 구성표가 변경되면 모든 것이 동시에 업데이트됩니다.

전체 테마 시스템 예시

:root {
  color-scheme: light dark;

  /* Backgrounds */
  --color-bg-base:      light-dark(#FFFFFF,   #0F0F17);
  --color-bg-elevated:  light-dark(#F8F9FA,   #1A1A2E);
  --color-bg-overlay:   light-dark(#F1F3F5,   #252540);

  /* Text */
  --color-text-primary:   light-dark(#1A1A2E, #E8E8F0);
  --color-text-secondary: light-dark(#4A4A6A, #A8A8C0);
  --color-text-muted:     light-dark(#6C757D, #6A6A88);
  --color-text-inverse:   light-dark(#FFFFFF, #1A1A2E);

  /* Borders */
  --color-border:         light-dark(#DEE2E6, #2E2E4A);
  --color-border-strong:  light-dark(#ADB5BD, #4A4A6A);

  /* Interactive / brand */
  --color-accent:         light-dark(#2563EB, #60A5FA);
  --color-accent-hover:   light-dark(#1D4ED8, #93C5FD);
  --color-accent-subtle:  light-dark(#DBEAFE, #1E3A5F);

  /* Feedback */
  --color-success:  light-dark(#16A34A, #4ADE80);
  --color-warning:  light-dark(#D97706, #FCD34D);
  --color-danger:   light-dark(#DC2626, #F87171);

  --color-success-bg: light-dark(#F0FDF4, #052E16);
  --color-warning-bg: light-dark(#FFFBEB, #2D1A00);
  --color-danger-bg:  light-dark(#FEF2F2, #2D0A0A);
}

구성 요소는 테마 지정에 대해 아무것도 알 필요 없이 다음 변수를 참조합니다.

.card {
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  color: var(--color-text-primary);
}

.btn-primary {
  background: var(--color-accent);
  color: var(--color-text-inverse);
}

.btn-primary:hover {
  background: var(--color-accent-hover);
}

.alert-success {
  background: var(--color-success-bg);
  color: var(--color-success);
  border-left: 3px solid var(--color-success);
}

어두운 모드에서는 밝은 액센트 #2563EB#60A5FA로 이동합니다. 이는 의도적인 것입니다. 600중량의 파란색은 흰색에 대한 WCAG AA 대비를 통과하지만 어두운 배경에는 통과하지 못합니다. 400 무게는 어두운 표면에서도 접근 가능한 대비를 유지할 수 있을 만큼 색상을 밝게 합니다. Contrast Checker를 사용하여 각 조합이 목표 명암비를 충족하는지 확인하고, Shade Generator를 사용하여 각 모드에 적합한 색상을 찾습니다.

다른 함수 안에 light-dark() 중첩하기

light-dark()는 색상 값을 반환하므로 다른 함수 내부를 포함하여 색상이 유효한 모든 곳에서 사용할 수 있습니다.

:root {
  color-scheme: light dark;
  --brand: #3B82F6;

  /* Use light-dark() inside color-mix() */
  --brand-surface: color-mix(
    in oklch,
    var(--brand) 15%,
    light-dark(white, #09090b)
  );
}

이렇게 하면 브랜드 색상의 15% 색조인 표면 색상이 생성되며, 밝은 모드에서는 흰색, 어두운 모드에서는 검정색에 가까워 자동으로 테마를 인식합니다.

사용자 토글 추가(JavaScript)

시스템 기본 설정에 응답하는 것이 올바른 기본값이지만 사용자가 이를 재정의할 수 있어야 합니다. 이를 위해서는 JavaScript가 선택을 유지하고 브라우저 기본값을 재정의해야 합니다.

JavaScript로 색 구성표 제어하기

핵심 통찰력은 color-scheme가 JavaScript를 통해 설정할 수 있는 CSS 속성이라는 것입니다.

// Set the color scheme programmatically
document.documentElement.style.colorScheme = 'dark';
document.documentElement.style.colorScheme = 'light';

// Remove the override (reverts to system preference)
document.documentElement.style.colorScheme = '';

루트 요소의 인라인 스타일을 통해 color-scheme를 설정하면 스타일시트 선언이 재정의됩니다. 모든 light-dark() 값은 적절한 변형으로 다시 확인됩니다.

토글 구현 완료

const STORAGE_KEY = 'color-scheme-preference';

function initColorScheme() {
  const stored = localStorage.getItem(STORAGE_KEY);
  if (stored === 'light' || stored === 'dark') {
    document.documentElement.style.colorScheme = stored;
  }
  // If no stored preference, the CSS color-scheme: light dark; handles it via OS preference
}

function toggleColorScheme() {
  const current = getComputedStyle(document.documentElement)
    .colorScheme
    .trim();

  // Determine next value
  const next = current.includes('dark') ? 'light' : 'dark';

  document.documentElement.style.colorScheme = next;
  localStorage.setItem(STORAGE_KEY, next);

  // Update any toggle button state
  updateToggleButton(next);
}

function updateToggleButton(scheme) {
  const btn = document.getElementById('theme-toggle');
  if (!btn) return;
  btn.setAttribute('aria-label',
    scheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'
  );
  btn.dataset.scheme = scheme;
}

// Run before first paint to avoid flash
initColorScheme();

// Attach to toggle button after DOM is ready
document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('theme-toggle')
    ?.addEventListener('click', toggleColorScheme);
});

DOM이 완전히 구문 분석되기 전에 initColorScheme()를 호출하는 것이 중요합니다. 늦게 실행되면 저장된 기본 설정을 가진 사용자는 전환되기 전에 잠시 OS 기본 테마를 보게 됩니다. 이는 고전적인 "잘못된 테마의 깜박임"입니다. 이 스크립트를 <head>에 인라인으로 배치하거나 defer 속성을 주의 깊게 사용하십시오(defer 스크립트는 DOM 구문 분석 후에 실행되므로 너무 늦을 수 있음).

플래시 방지 패턴

가장 강력한 플래시 방지 접근 방식은 <head>에서 최소한의 인라인 스크립트를 실행합니다.

<head>
  <meta name="color-scheme" content="light dark">
  <script>
    const stored = localStorage.getItem('color-scheme-preference');
    if (stored) {
      document.documentElement.style.colorScheme = stored;
    }
  </script>
  <link rel="stylesheet" href="styles.css">
</head>

이 스크립트는 CSS가 적용되기 전에 동기적으로 실행되므로 브라우저는 첫 번째 페인트에서 올바른 color-scheme 값을 계산합니다. <meta name="color-scheme"> 태그는 CSS가 구문 분석되기 전에 예상되는 구성표를 브라우저에 알려줍니다. 이는 일부 브라우저의 어두운 모드 페이지에서 짧은 흰색 깜박임을 방지합니다.

JS 기반 테마 마이그레이션 가이드

기존의 많은 다크 모드 구현에서는 JavaScript로 전환되는 data-theme 속성을 사용하며 CSS 재정의는 [data-theme="dark"]로 범위가 지정됩니다. light-dark()로의 마이그레이션은 점진적입니다. 모든 것을 한 번에 변경할 필요는 없습니다.

1단계: :root에 색 구성표 추가

:root {
  color-scheme: light dark;
  /* Existing custom properties remain unchanged */
}

2단계: 변수를 하나씩 마이그레이션

개념 증명으로 단일 변수로 시작하십시오. 분할 선언 패턴을 통합 light-dark()로 바꿉니다.

/* Before */
:root {
  --bg: #FFFFFF;
}
[data-theme="dark"] {
  --bg: #0F0F17;
}

/* After */
:root {
  color-scheme: light dark;
  --bg: light-dark(#FFFFFF, #0F0F17);
}

3단계: 토글 업데이트

JavaScript 토글을 data-theme 설정에서 style.colorScheme 설정으로 변경합니다.

/* Before */
document.documentElement.setAttribute('data-theme', scheme);

/* After */
document.documentElement.style.colorScheme = scheme;

4단계: 데이터 테마 선택기 제거

모든 변수가 마이그레이션되면 [data-theme="dark"] CSS 블록을 제거하십시오.

전환 중에 두 가지 모두 유지

두 시스템을 동시에 실행할 수 있습니다. 아직 마이그레이션되지 않은 변수에 대해 [data-theme="dark"] 재정의를 유지합니다. 새로운 변수는 light-dark()를 사용합니다. JavaScript 토글은 전환 기간 동안 data-themestyle.colorScheme를 모두 설정합니다.

브라우저 지원 대체

light-dark()를 지원하지 않는 약 15%의 브라우저에 대해 명시적인 폴백을 제공합니다.

:root {
  /* Fallback: explicit light-mode values */
  --color-bg: #FFFFFF;
  --color-text: #1A1A2E;

  /* Progressive enhancement with light-dark() */
  --color-bg:   light-dark(#FFFFFF, #0F0F17);
  --color-text: light-dark(#1A1A2E, #E8E8F0);
}

/* Fallback dark mode for browsers without light-dark() */
@supports not (color: light-dark(white, black)) {
  @media (prefers-color-scheme: dark) {
    :root {
      --color-bg: #0F0F17;
      --color-text: #E8E8F0;
    }
  }
}

@supports not (color: light-dark(white, black)) 블록은 light-dark()를 이해하지 못하는 브라우저에만 적용됩니다. 최신 브라우저는 부정 조건이 false이기 때문에 이를 완전히 건너뜁니다.

주요 내용

  • light-dark(<light-value>, <dark-value>)는 밝은 색 구성표의 첫 번째 인수를 반환하고 어두운 색 구성표의 두 번째 인수를 반환합니다. "현재 테마에 맞게 적용된 이 색상"을 표현하는 CSS 고유의 방법입니다.
  • 요소(또는 조상)에 설정되는 color-scheme CSS 속성에 따라 다릅니다. prefers-color-scheme를 통한 자동 적응을 활성화하려면 항상 :root에서 color-scheme: light dark를 설정하십시오.
  • 기존 미디어 쿼리 접근 방식에 비해 가장 큰 장점은 두 테마 값을 단일 선언에 함께 배치하여 밝은 변형과 어두운 변형 간의 관계를 명시적이고 감사 가능하게 만드는 것입니다.
  • 사용자 재정의에는 JavaScript를 통해 document.documentElement.style.colorScheme를 설정해야 합니다. 플래시를 방지하려면 CSS가 로드되기 전에 localStorage에서 선택 사항을 유지하고 인라인 <head> 스크립트에 적용하세요.
  • data-theme 속성 기반 시스템에서의 마이그레이션은 점진적입니다. [data-theme="dark"] 재정의 패턴에서 light-dark() 인라인 패턴으로 한 번에 하나의 변수를 이동합니다.
  • 브라우저 지원은 2026년 기준 ~85%입니다. 이전 환경을 위해 @media (prefers-color-scheme: dark) 블록과 함께 @supports not 폴백을 제공합니다.
  • Contrast Checker를 사용하여 각 light-dark() 쌍의 밝은 색상 값과 어두운 색상 값이 각각의 배경에 대해 WCAG 대비 요구 사항을 통과하는지 확인하고, Shade Generator를 사용하여 각 모드에 대한 각 색상의 올바른 음영을 찾습니다.

관련 색상

관련 브랜드

관련 도구