CSS 변수와 prefers-color-scheme으로 다크모드를 지원하고,
토글 버튼으로 수동 전환까지 — 복붙 한 번에 내 사이트에 바로 적용해요.
배경을 어둡게, 텍스트를 밝게 — 눈의 피로를 줄이고 배터리를 아끼는 UI 테마
다크모드(Dark Mode)는 화면 배경을 어두운 색으로, 텍스트와 아이콘을 밝은 색으로 표시하는 UI 테마예요. iOS 13과 Android 10이 시스템 차원에서 다크모드를 도입한 이후 웹에서도 필수 기능으로 자리 잡았어요. 사용자의 절반 이상이 다크모드를 선호하거나 야간에 사용하기 때문에, 대응하지 않으면 사용성이 크게 떨어질 수 있어요.
웹에서 다크모드를 감지하는 핵심 도구는 CSS 미디어 쿼리 prefers-color-scheme: dark예요.
사용자의 OS 설정이 다크모드일 때 이 쿼리가 true가 되어, 별도의 JavaScript 없이도 자동으로 다크 스타일을 적용할 수 있어요.
어두운 환경에서 밝은 화면은 눈을 피로하게 해요. 다크모드는 빛 방출을 줄여 장시간 사용에 편해요.
OLED 디스플레이에서는 검정 픽셀이 꺼져 있어 다크모드가 배터리를 최대 30%까지 아낄 수 있어요.
사용자가 원하는 테마로 볼 수 있다는 것 자체가 배려예요. 체류 시간과 만족도가 올라가요.
다크모드를 지원하는 사이트는 전문적이고 완성도 높은 인상을 줘요. 브랜드 신뢰도에도 도움이 돼요.
prefers-color-scheme이란? 사용자의 OS 또는 브라우저 설정에서 선호하는 색상 테마를 감지하는 CSS 미디어 쿼리예요. @media (prefers-color-scheme: dark) { ... } 안에 작성한 스타일은 다크모드 사용자에게만 적용돼요. Chrome, Firefox, Safari, Edge 모두 지원하는 표준 기능이에요.
프로젝트 규모와 요구사항에 따라 3가지 방법 중 선택하세요
추천: 바이브 코딩으로 사이트를 만든다면 방법 ③ (CSS 변수 + JS 토글)을 추천해요. 처음엔 코드가 조금 많아 보이지만, 아래 완성 코드를 통째로 복붙하면 돼요. 사용자 경험이 훨씬 좋아지고, 나중에 색상 변경도 변수 하나만 바꾸면 돼요.
CSS 변수(Custom Properties)를 사용하면 색상을 한 곳에서만 정의하고, 다크모드에서는 그 변수값만 바꾸면 전체 스타일이 자동으로 바뀌어요.
/* 라이트모드 기본값 */ :root { --bg: #ffffff; --text: #111827; --surface: #f9fafb; --border: #e5e7eb; } /* OS가 다크모드일 때 변수값만 교체 */ @media (prefers-color-scheme: dark) { :root { --bg: #1e1e2e; --text: #cdd6f4; --surface: #282a36; --border: #2d2d3f; } } /* 나머지 스타일은 변수를 참조 — 테마 바뀌어도 안 건드려도 됨 */ body { background: var(--bg); color: var(--text); } .card { background: var(--surface); border: 1px solid var(--border); }
CSS 변수 + 미디어쿼리 + JS 토글 버튼까지 한 번에 적용되는 완전한 코드예요
/* ① CSS 변수 정의 — <head> 안 <style> 태그에 추가 */ :root { --bg: #ffffff; --bg-surface: #f9fafb; --text: #111827; --text-muted: #6b7280; --border: #e5e7eb; --accent: #4f46e5; } /* ② OS 다크모드 자동 대응 */ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #1e1e2e; --bg-surface: #282a36; --text: #cdd6f4; --text-muted: #a6adc8; --border: #2d2d3f; --accent: #7c6dfa; } } /* ③ JS 토글로 강제 다크모드 */ [data-theme="dark"] { --bg: #1e1e2e; --bg-surface: #282a36; --text: #cdd6f4; --text-muted: #a6adc8; --border: #2d2d3f; --accent: #7c6dfa; } /* ④ 전환 애니메이션 (선택) */ *, *::before, *::after { transition: background-color 0.25s, color 0.25s, border-color 0.25s; } /* ⑤ 기본 스타일 — 변수만 참조 */ body { background: var(--bg); color: var(--text); } .card { background: var(--bg-surface); border: 1px solid var(--border); } /* ⑥ 토글 버튼 스타일 */ #theme-toggle { position: fixed; bottom: 24px; right: 24px; width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--border); background: var(--bg-surface); color: var(--text); font-size: 1.2rem; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,.15); transition: transform .2s, box-shadow .2s; display: flex; align-items: center; justify-content: center; } #theme-toggle:hover { transform: scale(1.1); } <!-- ⑦ HTML — </body> 바로 위에 추가 --> <button id="theme-toggle" aria-label="다크모드 전환">🌙</button> <script> // 저장된 테마 불러오기 const saved = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const isDark = saved === 'dark' || (!saved && prefersDark); if (isDark) document.documentElement.setAttribute('data-theme', 'dark'); const btn = document.getElementById('theme-toggle'); btn.textContent = isDark ? '☀️' : '🌙'; btn.addEventListener('click', () => { const current = document.documentElement.getAttribute('data-theme'); if (current === 'dark') { document.documentElement.removeAttribute('data-theme'); document.documentElement.setAttribute('data-theme', 'light'); localStorage.setItem('theme', 'light'); btn.textContent = '🌙'; } else { document.documentElement.setAttribute('data-theme', 'dark'); localStorage.setItem('theme', 'dark'); btn.textContent = '☀️'; } }); </script>
트랜지션 주의: transition을 * 전체에 적용하면 페이지 로드 시 깜빡임이 생길 수 있어요. <script>를 <head> 안에서 가장 먼저 실행하거나, transition을 특정 클래스에만 적용하는 방식으로 해결할 수 있어요.
라이트 / 다크 / 시스템 버튼을 눌러서 카드 미리보기가 실시간으로 바뀌는 걸 확인해보세요
이 카드는 라이트모드와 다크모드에서 각각 다른 색상을 보여줘요. CSS 변수 하나만 바꾸면 전체가 자동으로 전환돼요.
시스템을 누르면 현재 여러분의 OS 다크모드 설정을 따라가요. OS가 다크모드라면 카드가 어둡게, 라이트모드라면 밝게 표시돼요.
다크모드 구현 중 가장 많이 막히는 부분들을 모았어요
localStorage를 사용하면 돼요. 사용자가 다크모드를 선택했을 때 localStorage.setItem('theme', 'dark')로 저장하고, 페이지 로드 시 localStorage.getItem('theme')으로 읽어와서 적용하면 돼요.
중요한 점은 이 코드를 <head> 안에서 최대한 일찍 실행해야 한다는 거예요. </body> 바로 위에 두면 페이지가 잠깐 라이트모드로 보였다가 전환되는 "깜빡임(FOUC)" 현상이 생겨요. <head> 안에서 인라인 스크립트로 실행하세요.
세션이 끝나도 유지하려면 localStorage, 세션 동안만 유지하려면 sessionStorage를 쓰면 돼요. 바이브 코딩 프로젝트라면 localStorage가 더 적합해요.
네, HTML <picture> 태그와 media 속성을 조합하면 돼요. 다크모드용 이미지와 라이트모드용 이미지를 각각 준비하고 아래처럼 작성하면 자동으로 전환돼요.
<picture><source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)"><img src="logo-light.svg" alt="로고"></picture>
CSS로도 가능해요. @media (prefers-color-scheme: dark) { img.logo { content: url('logo-dark.svg'); } } — 단, CSS content 속성은 일부 브라우저에서 지원이 불안정해서 <picture> 방식이 더 안전해요.
이미지에 filter: invert(1) hue-rotate(180deg)를 다크모드에서 적용하면 색상을 반전시키는 효과를 줄 수도 있어요. 단순 아이콘이나 도식에 유용해요.
color: inherit을 쓴 요소는 부모의 --text 변수를 제대로 받아오지 못할 수 있어요. 명시적으로 color: var(--text)를 지정해주는 게 가장 확실한 해결책이에요.
다크모드에서는 순수한 흰색(#ffffff)보다 약간 회색빛이 도는 색상(#cdd6f4, #e2e8f0 등)이 눈에 훨씬 편해요. 순백색 텍스트는 어두운 배경과 대비가 너무 강해 오히려 피로감을 줄 수 있어요.
접근성 가이드라인(WCAG AA)에서는 배경과 텍스트의 명도 대비가 최소 4.5:1 이상이어야 해요. WebAIM 대비 검사기로 선택한 색상 조합이 기준을 통과하는지 꼭 확인하세요.
*, *::before, *::after { transition: background-color 0.25s, color 0.25s, border-color 0.25s; } 를 CSS에 추가하면 돼요. 모든 요소에 0.25초 트랜지션이 적용돼서 테마 전환이 부드러워요.
단, transition: all은 피하세요. 레이아웃 관련 속성(width, height, transform 등)까지 포함되어 성능 문제가 생길 수 있어요. 색상 관련 속성만 명시적으로 지정하세요.
페이지 첫 로드 때 트랜지션이 적용되면 깜빡임 현상이 생길 수 있어요. body.ready { transition: ... }처럼 클래스를 조건부로 붙이거나, JS에서 테마 적용 후 약간의 딜레이를 주고 트랜지션 클래스를 추가하는 방법으로 해결할 수 있어요.
일반적으로 사용자가 직접 누른 토글 버튼이 OS 설정보다 우선해야 해요. 사용자의 명시적 선택을 더 존중하는 것이 좋은 UX예요.
권장 우선순위: localStorage 저장값 → OS 설정 → 라이트모드(기본값) 순서로 적용해요. 즉, 사용자가 직접 토글하면 localStorage에 저장하고, 다음 방문 때 그 값을 먼저 읽어와요. localStorage 값이 없으면 OS 설정을 따라가요.
data-theme 속성이 없을 때는 미디어쿼리가 작동하고, data-theme="dark"나 data-theme="light"가 있으면 미디어쿼리를 무시하고 해당 테마를 적용해요. 이 패턴이 가장 깔끔한 구현이에요.
다크모드를 마스터했다면 이 도구들도 도전해보세요