이 글에서 정리하는 내용
Tailwind CSS v4 기준으로 dark: variant를 사용할 때 먼저 정해야 하는 기준을 정리합니다. 다크 모드는 어두운 색을 몇 개 추가하는 작업이 아니라, 라이트 모드와 같은 정보 구조를 다른 조도 환경에서도 읽히게 다시 맞추는 작업입니다. 시스템 설정을 따를지, 사용자가 직접 선택하게 할지, .dark 클래스와 data-theme 중 무엇을 기준으로 삼을지까지 함께 봅니다.
dark:를 붙였는데 다크 모드가 안 되는 이유- 다크 모드를 켜는 기준부터 정하기
.dark클래스 방식과data-theme방식 비교- 화면 단위로 다크 모드 스타일 잡기
- 코드가 지저분해지기 전에 정해야 할 기준
- 다시 적용할 때 확인할 체크포인트
dark:를 붙였는데 다크 모드가 안 되는 이유

Tailwind CSS에서 dark:bg-slate-900, dark:text-white 같은 클래스를 처음 보면이 클래스만 붙이면 다크 모드 기능이 생길 것처럼 보입니다. 실제로는 그렇지 않습니다. dark:는 테마를 전환하는 버튼이 아니라이미 다크 모드 상태가 되었을 때 적용할 스타일을 예약해두는 조건부 스타일입니다.
이 차이를 놓치면 코드가 맞는데도 화면이 바뀌지 않는 것처럼 느껴집니다. 예를 들어 카드에 bg-white dark:bg-slate-900를 넣었는데 운영체제 설정도 라이트 모드이고, html에 dark 클래스도 없다면 다크 스타일이 적용될 조건 자체가 없습니다. 클래스가 틀린 게 아니라, 다크 모드 상태를 만드는 기준이 없는 상태입니다.
Tailwind CSS의 기본 다크 모드 기준은 시스템 설정입니다. 사용자가 운영체제나 브라우저에서 다크 모드를 사용 중이면 dark: 스타일이 적용됩니다. 사이트 안에서 직접 토글 버튼을 만들고 싶다면 이 기본 기준만으로는 부족합니다. html 요소에 클래스를 붙이거나, data-theme 값을 바꾸는 식으로 테마 상태를 직접 관리해야 합니다.
카드 한 개에서 먼저 보면 이해가 빠릅니다
<article class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-950"> <p class="text-sm text-slate-500 dark:text-slate-400">Dashboard</p> <h3 class="mt-2 text-lg font-semibold text-slate-900 dark:text-white">이번 달 주문 현황</h3> <p class="mt-3 text-sm text-slate-600 dark:text-slate-300"> 라이트 모드에서는 흰 배경과 짙은 글자색을 사용하고, 다크 모드에서는 어두운 배경과 낮은 대비의 보조 텍스트를 사용합니다. </p>
</article> 이 카드에서 bg-white와 dark:bg-slate-950은 서로 같은 역할의 색상입니다. 하나는 기본 표면색이고, 다른 하나는 다크 상태의 표면색입니다. dark:를 어두운 색 추가로만 보면 클래스가 금방 늘어납니다. 반대로 상태별 같은 역할의 색상 짝으로 보면 수정할 때 기준이 생깁니다.
다크 모드를 켜는 기준부터 정하기
다크 모드를 구현하기 전에 먼저 정할 것은 색상이 아니라 전환 기준입니다. 화면을 어둡게 만드는 방식은 크게 세 가지로 나눌 수 있습니다. 시스템 설정만 따르는 방식, 사용자가 직접 라이트와 다크를 선택하는 방식, 그리고 light, dark, system 세 가지를 모두 제공하는 방식입니다.
가장 단순한 방식은 시스템 설정을 그대로 따르는 구조입니다. 별도 버튼이 필요 없고, Tailwind의 기본 동작을 그대로 사용하면 됩니다. 블로그 본문, 문서 페이지, 작은 포트폴리오처럼 사용자가 테마를 직접 고를 필요가 크지 않은 화면에서는 이 방식으로 시작할 수 있습니다.
관리자 화면이나 자주 방문하는 서비스라면 사용자가 직접 선택한 테마를 저장하는 흐름이 더 자연스럽습니다. 낮에는 라이트 모드, 밤에는 다크 모드를 쓰는 사람이 있는 반면, 시스템 설정과 상관없이 특정 테마를 고정하고 싶은 사용자도 있습니다. 이 경우에는 localStorage, 쿠키, 서버 저장값처럼 사용자 선택을 기억할 위치가 필요합니다.
| 방식 | 어울리는 상황 | 주의할 점 |
|---|---|---|
| system | 운영체제 설정을 그대로 따르는 간단한 페이지 | 사용자가 사이트 안에서 직접 바꿀 수 없습니다. |
| light / | 토글 버튼으로 테마를 명확히 전환하는 서비스 | 사용자 선택값을 저장하고 초기 로딩 때 반영해야 합니다. |
| light / / system | 설정 화면이 있는 앱이나 관리자 대시보드 | 저장된 선택값과 실제 적용 테마를 구분해야 합니다. |
system 옵션까지 지원할 때는 저장값과 실제 적용값이 달라질 수 있습니다. 사용자가 system을 선택했다면 저장값은 system이지만, 실제 화면에는 현재 시스템 설정에 따라 light나 dark가 적용됩니다. 이 둘을 구분하지 않으면 설정 버튼에는 시스템 모드라고 표시되는데 실제 화면은 다크 모드로 보이는 상황을 설명하기 어려워집니다.
.dark 클래스 방식과 data-theme 방식 비교
수동 토글을 만들 때 가장 흔한 방식은 html 요소에 dark 클래스를 붙이는 구조입니다. Tailwind CSS v4에서는 @custom-variant를 사용해 dark:가 어떤 선택자를 기준으로 동작할지 지정할 수 있습니다. 이 기준을 바꾸면 기존의 dark:bg-*, dark:text-* 같은 클래스는 그대로 두고, 다크 모드를 켜는 조건만 프로젝트에 맞게 바꿀 수 있습니다.
.dark 클래스를 기준으로 쓰는 방식
@import "tailwindcss"; @custom-variant dark (&:where(.dark, .dark *)); <html class="dark scheme-light dark:scheme-dark"> <body class="bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100"> <main>페이지 내용</main> </body>
</html> .dark 방식은 구조가 단순합니다. html에 dark 클래스가 있으면 다크 모드이고, 없으면 라이트 모드입니다. 개인 프로젝트, 작은 서비스, 포트폴리오에서는 이 방식이 빠르게 적용됩니다. 다만 테마가 dark 하나에서 끝나지 않을 때는 의미가 조금 애매해집니다. 예를 들어 admin, shop, event처럼 테마 축이 늘어날 수 있다면 클래스 하나만으로 상태를 표현하기 어렵습니다.
data-theme를 기준으로 쓰는 방식
@import "tailwindcss"; @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); <html data-theme="dark" class="scheme-light dark:scheme-dark"> <body class="bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100"> <main>페이지 내용</main> </body>
</html> data-theme 방식은 테마 상태를 속성으로 드러냅니다. 값이 dark인지, light인지, 나중에 brand나 admin 같은 값이 추가되는지 확인하기 쉽습니다. 다크 모드만 놓고 보면 .dark 방식보다 길어 보이지만, 테마 확장을 고려하면 상태를 더 명확하게 표현할 수 있습니다.
둘 중 하나가 항상 더 낫다고 단정할 수는 없습니다. 단순한 다크 모드 토글이면 .dark가 빠르고, 여러 테마나 디자인 토큰 확장이 예상되면 data-theme가 관리하기 좋습니다. 중요한 건 프로젝트 안에서 기준을 섞지 않는 것입니다. 어떤 화면은 .dark, 어떤 화면은 data-theme를 기준으로 만들면 나중에 테마가 안 먹는 영역을 찾기 어려워집니다.
화면 단위로 다크 모드 스타일 잡기
다크 모드를 적용할 때 바로 카드나 버튼부터 바꾸면 화면 일부만 맞고 전체 톤은 어긋나기 쉽습니다. 실제 페이지에서는 전체 배경, 섹션 배경, 카드 배경, 텍스트, 테두리, hover 상태가 같이 움직여야 합니다. 그래서 먼저 잡을 기준은 컴포넌트가 아니라 화면의 바닥색입니다.
<body class="min-h-screen bg-slate-50 text-slate-900 scheme-light dark:bg-slate-950 dark:text-slate-100 dark:scheme-dark"> <main class="mx-auto max-w-5xl px-4 py-10"> <section class="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900"> <h1 class="text-2xl font-bold text-slate-950 dark:text-white">주문 관리</h1> <p class="mt-2 text-sm text-slate-600 dark:text-slate-400"> 카드 내부의 제목, 설명, 테두리, 배경을 같은 역할끼리 맞춥니다. </p> </section> </main>
</body> 여기서 body는 페이지 전체의 기본 배경과 글자색을 담당합니다. section은 카드 또는 패널의 표면색을 담당합니다. h1과 p는 텍스트 위계를 담당합니다. 이렇게 역할을 나누면 다크 모드에서 색상이 어긋났을 때 어느 계층을 수정해야 하는지 빠르게 찾을 수 있습니다.
scheme-light와 dark:scheme-dark도 같이 확인해야 합니다. 이 유틸리티는 color-scheme CSS 속성과 연결됩니다. input, select, date picker, scrollbar처럼 브라우저가 기본으로 그리는 UI는 배경색만 바꾼다고 항상 자연스럽게 따라오지 않습니다. 폼 요소가 많은 관리자 화면에서는 이 차이가 더 빨리 보입니다.
버튼과 hover 상태도 같이 맞춰야 합니다
<button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-900 disabled:opacity-50 dark:bg-white dark:text-slate-950 dark:hover:bg-slate-200 dark:focus-visible:outline-white"> 저장하기
</button> 버튼은 기본 색상만 바꾸면 끝나지 않습니다. hover, disabled까지 같이 확인해야 합니다. 라이트 모드에서는 진한 버튼이 강조 역할을 하고, 다크 모드에서는 흰 버튼이 강조 역할을 할 수 있습니다. 이때 hover 색상이 빠지면 기본 상태는 괜찮아도 마우스를 올렸을 때 대비가 갑자기 무너질 수 있습니다.
코드가 지저분해지기 전에 정해야 할 기준

다크 모드를 처음 넣을 때는 눈에 보이는 요소마다 dark:를 붙이게 됩니다. 카드 하나, 버튼 하나, input 하나 정도는 문제가 적습니다. 하지만 화면이 늘어나면 비슷한 색상 조합이 계속 반복됩니다. 그 시점부터는 “어떤 색을 쓸까”보다 “이 색이 어떤 역할인가”를 먼저 정해야 합니다.
예를 들어 bg-white dark:bg-slate-900 조합이 카드 표면색이라면, 프로젝트 안에서 같은 역할을 하는 영역은 최대한 같은 조합을 사용해야 합니다. 어떤 카드는 dark:bg-slate-900, 어떤 카드는 dark:bg-gray-800, 또 다른 카드는 dark:bg-zinc-900처럼 흩어지면 전체 톤을 바꿀 때 수정 범위가 커집니다.
작은 프로젝트에서는 공통 컴포넌트로 해결할 수 있습니다. 카드, 버튼, input처럼 반복되는 UI를 컴포넌트로 묶고 그 안에 라이트와 다크 스타일을 같이 둡니다. 규모가 커지면 색상 토큰을 별도로 잡는 방식도 검토할 수 있습니다. Tailwind 클래스만으로 빠르게 만드는 장점은 유지하되, 반복되는 역할은 한곳에서 관리해야 수정 비용이 줄어듭니다.
토글 스크립트는 적용 시점이 중요합니다
const root = document.documentElement;
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const theme = savedTheme ?? 'system';
const resolvedTheme = theme === 'system' ? (prefersDark ? 'dark' : 'light') : theme; root.dataset.theme = resolvedTheme; 이 코드는 data-theme 방식을 기준으로 한 간단한 예시입니다. .dark 방식을 선택했다면 root.classList.toggle('dark', resolvedTheme === 'dark')처럼 클래스 기준으로 맞추면 됩니다. 실제 프로젝트에서는 둘 중 하나를 기준으로 정하고 일관되게 사용하는 것이 중요합니다. 여기서 봐야 할 부분은 savedTheme와 resolvedTheme의 구분입니다. 사용자가 선택한 값이 system이라면 실제 적용값은 현재 시스템 설정을 계산해서 얻어야 합니다.
Next.js처럼 서버 렌더링이 들어가는 환경에서는 초기 테마 적용 시점도 따로 봐야 합니다. 서버가 라이트 모드 HTML을 먼저 보내고, 클라이언트에서 뒤늦게 다크 모드로 바꾸면 화면이 잠깐 깜빡일 수 있습니다. 테마 값을 쿠키로 관리하거나, 초기 스크립트를 먼저 실행하거나, hydration 이후에만 테마 라벨을 보여주는 식으로 프로젝트 구조에 맞게 처리해야 합니다.
다시 적용할 때 확인할 체크포인트
Tailwind CSS의 dark:는 문법 자체보다 적용 기준이 더 중요합니다. 먼저 이 프로젝트가 시스템 설정만 따르면 되는지, 사용자가 직접 테마를 선택해야 하는지 정해야 합니다. 그다음 .dark 클래스를 쓸지, data-theme 속성을 쓸지 하나로 고정합니다.
화면을 만들 때는 전체 배경부터 잡고, 그 위에 카드 표면색, 텍스트 위계, 테두리, hover 상태, 폼 요소 순서로 확인하는 흐름이 적합합니다. 특히 관리자 화면이나 상품 카드처럼 반복 UI가 많은 화면에서는 같은 역할의 색상 조합을 반복해서 쓰는지 봐야 합니다. 다크 모드는 색을 검게 만드는 작업이 아니라, 라이트 모드와 같은 정보 구조를 어두운 환경에서도 읽히게 다시 맞추는 작업입니다.
다음에 코드를 볼 때는 세 가지만 먼저 확인하면 됩니다. 첫째, 다크 모드를 켜는 기준이 어디에 있는지 확인합니다. 둘째, dark: 클래스가 같은 역할끼리 짝을 이루는지 확인합니다. 셋째, 첫 화면에서 테마가 늦게 적용되어 깜빡이는 문제가 없는지 확인합니다. 이 기준이 잡혀 있으면 Tailwind의 다크 모드는 클래스가 늘어나도 방향을 잃지 않습니다.
같이 읽으면 좋은 글
- Tailwind CSS 실무 로드맵: 기본 클래스부터 반응형·상태·동적 클래스까지
- Tailwind CSS 클래스 감지 원리: 동적 className이 빠지는 이유
- Tailwind CSS 클래스 적용 오류 해결: 스타일이 보이지 않을 때 확인할 것
“Tailwind CSS dark 사용법: 다크 모드 구현 기준 잡기”에 대한 1개의 생각