Tailwind CSS v4의 다크 모드 전략, CSS 커스텀 프로퍼티 기반 런타임 테마 전환, 다중 테마 패턴, 그리고 next-themes와의 통합을 실전 중심으로 다룹니다.
7장에서 애니메이션과 트랜지션 기법을 배웠습니다. 이 장에서는 Tailwind CSS v4의 다크 모드 전략과 테마 시스템을 다룹니다. v4에서 변경된 다크 모드 설정, CSS 커스텀 프로퍼티를 활용한 런타임 테마 전환, 그리고 실전 프로젝트에서의 테마 관리 패턴을 살펴봅니다.
v4에서 dark: 변형은 기본적으로 prefers-color-scheme: dark 미디어 쿼리를 사용합니다. 별도의 설정 없이 바로 사용할 수 있습니다.
<div class="bg-white dark:bg-neutral-900">
<h1 class="text-neutral-900 dark:text-neutral-100">제목</h1>
<p class="text-neutral-600 dark:text-neutral-400">본문 텍스트</p>
</div>사용자가 직접 테마를 선택할 수 있게 하려면, dark: 변형을 클래스 기반으로 전환해야 합니다. v4에서는 @custom-variant를 사용합니다.
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));이제 <html class="dark">로 다크 모드를 제어할 수 있습니다.
<!-- html에 dark 클래스 추가/제거로 제어 -->
<html class="dark">
<body class="bg-white dark:bg-neutral-900">
<h1 class="text-neutral-900 dark:text-neutral-100">제목</h1>
</body>
</html>v3에서는 tailwind.config.js의 darkMode: 'class'로 설정했지만, v4에서는 @custom-variant로 CSS 안에서 직접 정의합니다. &:where(.dark, .dark *) 구문은 .dark 클래스가 있는 요소와 그 자손 모두에 적용됩니다.
dark: 변형을 모든 요소에 일일이 적용하는 대신, CSS 커스텀 프로퍼티를 사용하면 한 곳에서 테마를 관리할 수 있습니다.
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
/* 라이트 테마 (기본) */
@theme {
--color-background: oklch(0.99 0 0);
--color-surface: oklch(1 0 0);
--color-surface-alt: oklch(0.97 0 0);
--color-foreground: oklch(0.15 0 0);
--color-foreground-muted: oklch(0.45 0 0);
--color-border: oklch(0.90 0 0);
--color-primary: oklch(0.60 0.22 250);
--color-primary-hover: oklch(0.53 0.24 252);
--color-primary-foreground: oklch(1 0 0);
}
/* 다크 테마 오버라이드 */
.dark {
--color-background: oklch(0.12 0.01 260);
--color-surface: oklch(0.17 0.01 260);
--color-surface-alt: oklch(0.22 0.01 260);
--color-foreground: oklch(0.93 0 0);
--color-foreground-muted: oklch(0.60 0 0);
--color-border: oklch(0.28 0.01 260);
--color-primary: oklch(0.65 0.20 250);
--color-primary-hover: oklch(0.72 0.18 250);
--color-primary-foreground: oklch(0.15 0 0);
}이제 컴포넌트에서 dark: 변형 없이 의미론적 토큰만 사용합니다.
<!-- dark: 변형이 필요 없음 — 토큰이 자동으로 전환됨 -->
<div class="bg-background text-foreground">
<div class="bg-surface border-border rounded-lg p-6">
<h2 class="text-foreground">제목</h2>
<p class="text-foreground-muted">설명 텍스트</p>
<button class="bg-primary text-primary-foreground hover:bg-primary-hover">
버튼
</button>
</div>
</div>의미론적 토큰 패턴의 가장 큰 장점은 컴포넌트가 테마에 대해 알 필요가 없다는 것입니다. bg-background, text-foreground만 사용하면 라이트/다크 모드 전환이 자동으로 처리됩니다. 새로운 테마를 추가해도 컴포넌트 코드를 수정할 필요가 없습니다.
CSS 커스텀 프로퍼티를 활용하면 라이트/다크를 넘어 다양한 테마를 지원할 수 있습니다.
@import "tailwindcss";
@theme {
--color-background: oklch(0.99 0 0);
--color-surface: oklch(1 0 0);
--color-primary: oklch(0.60 0.22 250);
--color-foreground: oklch(0.15 0 0);
}
/* 다크 테마 */
[data-theme="dark"] {
--color-background: oklch(0.12 0.01 260);
--color-surface: oklch(0.17 0.01 260);
--color-primary: oklch(0.65 0.20 250);
--color-foreground: oklch(0.93 0 0);
}
/* 세피아 테마 */
[data-theme="sepia"] {
--color-background: oklch(0.95 0.02 80);
--color-surface: oklch(0.97 0.015 80);
--color-primary: oklch(0.55 0.15 45);
--color-foreground: oklch(0.25 0.03 60);
}
/* 고대비 테마 */
[data-theme="high-contrast"] {
--color-background: oklch(1 0 0);
--color-surface: oklch(1 0 0);
--color-primary: oklch(0.30 0.25 250);
--color-foreground: oklch(0 0 0);
}<html data-theme="dark">
<!-- 전체 UI에 다크 테마 적용 -->
</html>function setTheme(theme: string) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
// 시스템 테마 감지
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme);
} else if (prefersDark.matches) {
setTheme('dark');
}Next.js 프로젝트에서는 next-themes 라이브러리와 자연스럽게 통합됩니다.
// app/providers.tsx
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
);
}@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-background: oklch(0.99 0 0);
--color-foreground: oklch(0.15 0 0);
}
.dark {
--color-background: oklch(0.12 0.01 260);
--color-foreground: oklch(0.93 0 0);
}'use client';
import { useTheme } from 'next-themes';
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="p-2 rounded-lg bg-surface-alt hover:bg-border transition-colors"
aria-label="테마 전환"
>
{/* 아이콘 */}
</button>
);
}next-themes의 attribute="class"를 사용할 때, v4의 @custom-variant dark 설정과 일치해야 합니다. attribute="data-theme"을 사용한다면 @custom-variant도 그에 맞게 수정하세요.
서버 사이드 렌더링 환경에서 테마 전환 시 FOUC(Flash of Unstyled Content)가 발생할 수 있습니다.
<head>
<script>
(function() {
const theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
</script>
</head>테마 전환 시 모든 요소에 트랜지션이 적용되면 어색할 수 있습니다. disableTransitionOnChange(next-themes)를 사용하거나, 직접 구현합니다.
html.disable-transitions,
html.disable-transitions * {
transition: none !important;
}function setThemeWithoutTransition(theme: string) {
document.documentElement.classList.add('disable-transitions');
setTheme(theme);
// 다음 프레임에서 트랜지션 복원
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document.documentElement.classList.remove('disable-transitions');
});
});
}@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* === 중성 색상 === */
--color-neutral-50: oklch(0.985 0.002 250);
--color-neutral-100: oklch(0.97 0.004 250);
--color-neutral-200: oklch(0.92 0.006 250);
--color-neutral-300: oklch(0.87 0.008 250);
--color-neutral-400: oklch(0.71 0.01 250);
--color-neutral-500: oklch(0.55 0.012 250);
--color-neutral-600: oklch(0.45 0.012 250);
--color-neutral-700: oklch(0.37 0.01 250);
--color-neutral-800: oklch(0.27 0.008 250);
--color-neutral-900: oklch(0.21 0.006 250);
--color-neutral-950: oklch(0.13 0.004 250);
/* === 의미론적 토큰 (라이트 기본) === */
--color-background: var(--color-neutral-50);
--color-surface: white;
--color-surface-alt: var(--color-neutral-100);
--color-foreground: var(--color-neutral-900);
--color-foreground-muted: var(--color-neutral-500);
--color-border: var(--color-neutral-200);
/* === 브랜드 === */
--color-primary: oklch(0.60 0.22 250);
--color-primary-hover: oklch(0.53 0.24 252);
--color-primary-foreground: white;
/* === 기능 색상 === */
--color-success: oklch(0.65 0.19 150);
--color-warning: oklch(0.80 0.15 80);
--color-error: oklch(0.58 0.24 25);
}
/* === 다크 테마 === */
.dark {
--color-background: var(--color-neutral-950);
--color-surface: var(--color-neutral-900);
--color-surface-alt: var(--color-neutral-800);
--color-foreground: var(--color-neutral-100);
--color-foreground-muted: var(--color-neutral-400);
--color-border: var(--color-neutral-700);
--color-primary: oklch(0.65 0.20 250);
--color-primary-hover: oklch(0.72 0.18 250);
--color-primary-foreground: var(--color-neutral-950);
--color-success: oklch(0.70 0.17 150);
--color-warning: oklch(0.85 0.13 80);
--color-error: oklch(0.65 0.22 25);
}Tailwind CSS v4의 다크 모드는 @custom-variant로 유연하게 설정할 수 있으며, CSS 커스텀 프로퍼티와 결합하면 의미론적 토큰 기반의 강력한 테마 시스템을 구축할 수 있습니다. 라이트/다크를 넘어 다중 테마, 컴포넌트 수준 테마, 런타임 테마 전환까지 — CSS만으로 모든 것이 가능합니다.
다음 장에서는 Tailwind v4의 커스텀 변형 정의와 플러그인 시스템을 다룹니다. @custom-variant, @plugin 디렉티브, 그리고 기존 JS 플러그인에서 CSS 기반 플러그인으로의 전환을 살펴봅니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
Tailwind CSS v4의 @custom-variant, @plugin 디렉티브, CSS 기반 확장 시스템, 그리고 기존 JS 플러그인에서의 마이그레이션을 실전 중심으로 다룹니다.
Tailwind CSS v4의 애니메이션 시스템, @keyframes 정의, @starting-style 활용, transition-behavior, 그리고 성능 최적화된 애니메이션 패턴을 다룹니다.
Tailwind CSS v4로 프로덕션 디자인 시스템을 구축합니다. 토큰 계층 설계, 컴포넌트 라이브러리, 테마 시스템, 접근성, 팀 협업 전략을 실전 중심으로 다룹니다.