🌙 CSS + JavaScript 패턴

다크모드 토글
(Dark Mode Toggle)

클릭 한 번으로 라이트/다크 전환. localStorage로 설정을 기억하고, CSS 변수로 전체 테마를 바꾸는 다크모드 토글 버튼을 완성해봐요.

🌙 CSS Variables 💾 localStorage ⚡ 즉시 전환 📋 복사 가능한 코드
① 개념

다크모드 토글 원리

data-theme="dark" 속성 + CSS 변수 override로 전체 테마를 한 번에 바꾸는 방법을 알아봐요

다크모드를 구현하는 가장 현대적인 방법은 CSS 변수(Custom Properties)를 이용하는 거예요. :root에 색상 변수를 정의하고, [data-theme="dark"] 선택자로 덮어쓰면 HTML 속성 하나만 바꿔도 전체 색상이 바뀌는 마법이 일어나요.

🖱️ 버튼 클릭 사용자 액션
JS: setAttribute
data-theme="dark" 속성 토글
CSS 변수 override
[data-theme="dark"] 색상 일괄 변경
🌙 다크 화면 전환 완료
🎨

CSS 변수 Override

:root에 라이트 테마 색상을 정의하고, [data-theme="dark"]에서 같은 변수를 다크 색상으로 덮어써요.

💾

localStorage 저장

사용자가 선택한 테마를 localStorage에 기억시켜요. 다음에 방문해도 선택이 유지돼요.

🖥️

시스템 설정 감지

prefers-color-scheme 미디어 쿼리로 OS 다크모드 설정을 감지해서 기본값으로 사용해요.

깜빡임 방지

페이지 로드 전 <head>에서 테마를 적용하면 화면이 깜빡이지 않아요.

💡
왜 class 대신 data-theme을 쓰나요?
class="dark"를 써도 되지만, data-theme="dark"가 더 명시적이에요. HTML 속성으로 현재 테마 상태를 한눈에 파악할 수 있고, CSS 선택자로도 명확히 구분돼요. 또한 기존 클래스 이름과 충돌할 위험이 없어요.
② 데모

실제로 눌러보세요

3가지 스타일의 토글 버튼 — 전부 같은 미니 프리뷰에 반영돼요

🎛️ 통합 데모 — 어떤 버튼이든 같은 프리뷰에 적용돼요

바이브툴킷 미니 카드
이 미니 카드의 배경색과 텍스트 색이 CSS 변수로 제어돼요. 버튼을 누르면 data-theme 속성이 바뀌고, CSS 변수 override로 즉시 반영돼요.
☀️ Light Mode

🌙 아이콘 버튼

이모지로 현재 상태를 직관적으로

미니 카드
배경과 텍스트가 바뀌어요
☀️ Light

🔘 슬라이드 스위치

iOS 스타일 pill 형태 토글

미니 카드
배경과 텍스트가 바뀌어요
☀️ Light

🔤 텍스트 버튼

현재 모드를 텍스트로 명시

미니 카드
배경과 텍스트가 바뀌어요
☀️ Light
어떤 스타일을 쓸까요?
아이콘 버튼은 공간이 좁은 헤더/내비게이션에 적합해요. 슬라이드 스위치는 설정 화면에서 직관적이에요. 텍스트 버튼은 현재 상태를 명시적으로 보여줘야 할 때 좋아요.
③ 코드

완전한 HTML + CSS + JS 코드

복붙하면 바로 동작하는 완성 코드 — localStorage 포함

핵심 JavaScript 로직

JavaScript
// 테마 토글 핵심 코드
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();

완전한 HTML 파일 (아이콘 버튼 버전)

HTML
<!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>

iOS 슬라이드 스위치 CSS만 추출

HTML + CSS
/* 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);
}
④ CSS 변수 시스템

CSS 변수로 테마 구성하는 법

:root[data-theme="dark"]의 변수 override 패턴을 이해하면 어떤 복잡한 테마도 만들 수 있어요

CSS
/* ✅ 라이트 테마 (기본값) */
: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 테두리, 구분선
⚠️
깜빡임(FOUC) 방지 팁
<body> 아래 <script>에서 테마를 적용하면, 페이지가 먼저 라이트로 렌더링됐다가 다크로 전환되면서 화면이 잠깐 깜빡여요. 이를 막으려면 <head> 안에 인라인 <script>를 넣어서 HTML 파싱 전에 테마를 적용해야 해요.

<head>...<script>document.documentElement.setAttribute('data-theme', localStorage.getItem('theme') || 'light');</script></head>
⑤ 프롬프트

AI에게 다크모드 요청하는 법

이 프롬프트를 그대로 복사해서 Claude, ChatGPT, Cursor에 붙여넣어봐요

🤖 AI 프롬프트 예시 1 — 기본 요청

"내 HTML 페이지에 다크모드 토글 버튼을 추가해줘. CSS 변수(:root와 [data-theme=dark])로 색상을 관리하고, localStorage에 설정을 저장해서 새로고침해도 유지되게 해줘. OS의 prefers-color-scheme도 감지해서 기본값으로 쓰고, 아이콘은 🌙/☀️를 사용해줘."

🤖 AI 프롬프트 예시 2 — 기존 코드에 추가

"이 CSS에 다크모드 변수를 추가해줘. 지금 하드코딩된 색상들(#fff, #111827 등)을 CSS 변수로 바꾸고, [data-theme=dark] 선택자에서 어두운 색상으로 override되게 해줘. transition으로 부드럽게 전환되게 하고, 토글 버튼 JavaScript도 써줘."

🤖 AI 프롬프트 예시 3 — iOS 스위치 스타일

"iOS 스타일의 토글 스위치(pill 모양에 흰 동그라미가 좌우로 이동)로 다크모드를 전환하는 컴포넌트를 만들어줘. 체크됐을 때 초록색(#16a34a)으로 바뀌고, CSS transition으로 부드럽게 움직이게 해줘. 옆에 '라이트 모드 / 다크 모드' 텍스트도 같이 바뀌게 해줘."

⑥ FAQ

자주 묻는 질문

prefers-color-scheme와 localStorage 중 어떤 걸 우선해야 하나요?
localStorage(사용자 선택)를 우선하는 게 좋아요. 사용자가 직접 토글을 눌러 선택한 설정이 OS 설정보다 중요하기 때문이에요. 코드 순서는 이렇게 해요:

const saved = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');

localStorage에 저장된 값이 있으면 그걸 쓰고, 없으면(처음 방문) OS 설정을 확인해서 기본값을 결정해요.
transition 적용 시 페이지 로드 때 애니메이션이 실행되는 문제가 있어요
페이지 로드 시 테마를 적용할 때 transition이 함께 실행되어 의도치 않은 애니메이션이 나타날 수 있어요. 두 가지 해결 방법이 있어요:

방법 1: 로드 후 transition 활성화
document.documentElement.classList.add('transitions-enabled'); 를 테마 적용 후 실행하고, CSS에서 .transitions-enabled *에만 transition을 적용해요.

방법 2: requestAnimationFrame 활용
requestAnimationFrame(() => { document.body.style.transition = 'background .3s, color .3s'; }); 로 첫 렌더링 이후에 transition을 추가해요.
이미지도 다크모드에 맞게 바꿀 수 있나요?
네, 세 가지 방법이 있어요:

1. CSS filter 활용
[data-theme="dark"] img { filter: brightness(0.8) contrast(1.1); } — 다크 배경에서 이미지가 너무 밝지 않게 해요.

2. picture 태그 + media query
<source media="(prefers-color-scheme: dark)" srcset="logo-dark.png"> 로 OS 설정에 따라 다른 이미지를 로드해요.

3. 배경 이미지 CSS 변수
:root { --hero-bg: url('light-bg.jpg'); }, [data-theme="dark"] { --hero-bg: url('dark-bg.jpg'); } 로 배경 이미지도 테마에 따라 바꿀 수 있어요.
다음 단계

이런 것도 배워봐요