클릭 한 번으로 라이트/다크 전환. localStorage로 설정을 기억하고, CSS 변수로 전체 테마를 바꾸는 다크모드 토글 버튼을 완성해봐요.
data-theme="dark" 속성 + CSS 변수 override로 전체 테마를 한 번에 바꾸는 방법을 알아봐요
다크모드를 구현하는 가장 현대적인 방법은 CSS 변수(Custom Properties)를 이용하는 거예요.
:root에 색상 변수를 정의하고, [data-theme="dark"] 선택자로 덮어쓰면
HTML 속성 하나만 바꿔도 전체 색상이 바뀌는 마법이 일어나요.
data-theme="dark"
속성 토글
[data-theme="dark"]
색상 일괄 변경
:root에 라이트 테마 색상을 정의하고, [data-theme="dark"]에서 같은 변수를 다크 색상으로 덮어써요.
사용자가 선택한 테마를 localStorage에 기억시켜요. 다음에 방문해도 선택이 유지돼요.
prefers-color-scheme 미디어 쿼리로 OS 다크모드 설정을 감지해서 기본값으로 사용해요.
페이지 로드 전 <head>에서 테마를 적용하면 화면이 깜빡이지 않아요.
class="dark"를 써도 되지만, data-theme="dark"가 더 명시적이에요.
HTML 속성으로 현재 테마 상태를 한눈에 파악할 수 있고, CSS 선택자로도 명확히 구분돼요.
또한 기존 클래스 이름과 충돌할 위험이 없어요.
3가지 스타일의 토글 버튼 — 전부 같은 미니 프리뷰에 반영돼요
이모지로 현재 상태를 직관적으로
iOS 스타일 pill 형태 토글
현재 모드를 텍스트로 명시
복붙하면 바로 동작하는 완성 코드 — localStorage 포함
// 테마 토글 핵심 코드 const toggle = () => { const isDark = document.body.getAttribute('data-theme') === 'dark'; document.body.setAttribute('data-theme', isDark ? 'light' : 'dark'); localStorage.setItem('theme', isDark ? 'light' : 'dark'); updateIcon(); }; // 아이콘/텍스트 업데이트 const updateIcon = () => { const isDark = document.body.getAttribute('data-theme') === 'dark'; document.getElementById('theme-btn').textContent = isDark ? '☀️' : '🌙'; }; // 페이지 로드 시 저장된 테마 적용 const saved = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); document.body.setAttribute('data-theme', saved); updateIcon();
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>다크모드 예제</title> <style> /* 1. CSS 변수 — 라이트 모드 기본값 */ :root { --bg: #ffffff; --text: #111827; --text-sub: #6b7280; --card-bg: #f9fafb; --card-border: #e5e7eb; --btn-bg: #f3f4f6; --btn-border: #d1d5db; } /* 2. 다크 모드 변수 override */ [data-theme="dark"] { --bg: #0f172a; --text: #f1f5f9; --text-sub: #94a3b8; --card-bg: #1e293b; --card-border: #334155; --btn-bg: #1e293b; --btn-border: #475569; } /* 3. 변수 적용 */ body { background: var(--bg); color: var(--text); transition: background .3s, color .3s; font-family: sans-serif; padding: 40px 24px; } .card { background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 12px; padding: 24px; margin-top: 20px; transition: background .3s, border-color .3s; } #theme-btn { width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid var(--btn-border); background: var(--btn-bg); font-size: 1.3rem; cursor: pointer; transition: all .2s; } </style> </head> <body> <button id="theme-btn" onclick="toggle()" aria-label="테마 전환">🌙</button> <div class="card"> <h2>다크모드 예제 카드</h2> <p>버튼을 눌러 테마를 바꿔보세요.</p> </div> <script> // 깜빡임 방지: 스크립트를 head에서 먼저 실행하거나 // 아래처럼 DOMContentLoaded 없이 즉시 실행해요 const saved = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); document.body.setAttribute('data-theme', saved); updateIcon(); function toggle() { const isDark = document.body.getAttribute('data-theme') === 'dark'; document.body.setAttribute('data-theme', isDark ? 'light' : 'dark'); localStorage.setItem('theme', isDark ? 'light' : 'dark'); updateIcon(); } function updateIcon() { const isDark = document.body.getAttribute('data-theme') === 'dark'; document.getElementById('theme-btn').textContent = isDark ? '☀️' : '🌙'; } </script> </body> </html>
/* HTML */ <label class="switch"> <input type="checkbox" onchange="toggle()"> <span class="slider"></span> </label> /* CSS */ .switch { position: relative; display: inline-block; width: 52px; height: 28px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; inset: 0; background: #d1d5db; border-radius: 28px; transition: background .25s; } .slider::before { content: ''; position: absolute; width: 22px; height: 22px; left: 3px; top: 3px; background: #fff; border-radius: 50%; transition: transform .25s; box-shadow: 0 1px 4px rgba(0,0,0,.2); } .switch input:checked + .slider { background: #16a34a; } .switch input:checked + .slider::before { transform: translateX(24px); }
:root와 [data-theme="dark"]의 변수 override 패턴을 이해하면 어떤 복잡한 테마도 만들 수 있어요
/* ✅ 라이트 테마 (기본값) */ :root { --bg: #ffffff; /* 페이지 배경 */ --text: #111827; /* 본문 텍스트 */ --text-sub: #6b7280; /* 보조 텍스트 */ --card-bg: #f9fafb; /* 카드 배경 */ --border: #e5e7eb; /* 테두리 색상 */ --accent: #4f46e5; /* 강조 색상 (변경 불필요한 경우 생략) */ } /* 🌙 다크 테마 — 같은 변수 이름으로 override */ [data-theme="dark"] { --bg: #0f172a; --text: #f1f5f9; --text-sub: #94a3b8; --card-bg: #1e293b; --border: #334155; } /* 💡 변수 적용 — 한 번 쓰면 테마 전환 시 자동으로 바뀌어요 */ body { background: var(--bg); color: var(--text); transition: background .3s, color .3s; } .card { background: var(--card-bg); border: 1px solid var(--border); } .sub-text { color: var(--text-sub); } /* 🖥️ OS 다크모드도 CSS만으로 대응 (JS 없이) */ @media (prefers-color-scheme: dark) { :root { --bg: #0f172a; --text: #f1f5f9; /* ... */ } }
| CSS 변수 | 라이트 모드 | 다크 모드 | 용도 |
|---|---|---|---|
| --bg | #ffffff | #0f172a | 페이지 전체 배경 |
| --text | #111827 | #f1f5f9 | 본문 텍스트 |
| --text-sub | #6b7280 | #94a3b8 | 설명, 날짜 등 보조 텍스트 |
| --card-bg | #f9fafb | #1e293b | 카드, 패널 배경 |
| --border | #e5e7eb | #334155 | 테두리, 구분선 |
<body> 아래 <script>에서 테마를 적용하면, 페이지가 먼저 라이트로 렌더링됐다가 다크로 전환되면서 화면이 잠깐 깜빡여요.
이를 막으려면 <head> 안에 인라인 <script>를 넣어서 HTML 파싱 전에 테마를 적용해야 해요.<head>...<script>document.documentElement.setAttribute('data-theme', localStorage.getItem('theme') || 'light');</script></head>
이 프롬프트를 그대로 복사해서 Claude, ChatGPT, Cursor에 붙여넣어봐요
"내 HTML 페이지에 다크모드 토글 버튼을 추가해줘. CSS 변수(:root와 [data-theme=dark])로 색상을 관리하고, localStorage에 설정을 저장해서 새로고침해도 유지되게 해줘. OS의 prefers-color-scheme도 감지해서 기본값으로 쓰고, 아이콘은 🌙/☀️를 사용해줘."
"이 CSS에 다크모드 변수를 추가해줘. 지금 하드코딩된 색상들(#fff, #111827 등)을 CSS 변수로 바꾸고, [data-theme=dark] 선택자에서 어두운 색상으로 override되게 해줘. transition으로 부드럽게 전환되게 하고, 토글 버튼 JavaScript도 써줘."
"iOS 스타일의 토글 스위치(pill 모양에 흰 동그라미가 좌우로 이동)로 다크모드를 전환하는 컴포넌트를 만들어줘. 체크됐을 때 초록색(#16a34a)으로 바뀌고, CSS transition으로 부드럽게 움직이게 해줘. 옆에 '라이트 모드 / 다크 모드' 텍스트도 같이 바뀌게 해줘."
const saved = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');transition이 함께 실행되어 의도치 않은 애니메이션이 나타날 수 있어요. 두 가지 해결 방법이 있어요:document.documentElement.classList.add('transitions-enabled'); 를 테마 적용 후 실행하고, CSS에서 .transitions-enabled *에만 transition을 적용해요.requestAnimationFrame(() => { document.body.style.transition = 'background .3s, color .3s'; }); 로 첫 렌더링 이후에 transition을 추가해요.
[data-theme="dark"] img { filter: brightness(0.8) contrast(1.1); } — 다크 배경에서 이미지가 너무 밝지 않게 해요.<source media="(prefers-color-scheme: dark)" srcset="logo-dark.png"> 로 OS 설정에 따라 다른 이미지를 로드해요.:root { --hero-bg: url('light-bg.jpg'); }, [data-theme="dark"] { --hero-bg: url('dark-bg.jpg'); } 로 배경 이미지도 테마에 따라 바꿀 수 있어요.