05 — Panda · Tailwind Theme Config
이 문서가 답하는 질문: Panda CSS의
conditionsAPI, Tailwind v3의darkMode: 'class', Tailwind v4의@variant와@theme— 셋이 같은 selector([data-theme="dark"])로 수렴하도록 설정하려면 정확히 무엇을 써야 하는가? 그리고 둘 다 한 프로젝트에 공존시킬 때 충돌이 없는가? 한 줄 답 (Pyramid Top): 정답은 “[data-theme="dark"]selector를 유일한 진실로 정하고, Panda는conditions: { dark: '[data-theme=dark] &' }, Tailwind v3는darkMode: ['selector', '[data-theme="dark"]'], Tailwind v4는@variant dark재정의” — 세 시스템이 같은 selector를 보면 컴포넌트 한 벌이 양쪽에서 동시에 작동한다. Panda의conditions는 임의 확장이 가능해 brand, contrast, locale도 같은 메커니즘으로.
Why — 왜 존재하는가
한 프로젝트에 두 라이브러리가 공존하는 경우는 점점 흔해진다 — 레거시는 Tailwind, 신규 컴포넌트는 Panda. 다크모드 토글이 한 라이브러리에서만 작동하면 페이지의 절반이 라이트로 남는다.
| 풀려는 문제 | 단순 해법 | 한계 |
|---|---|---|
| Tailwind + Panda 공존 | 각자 다른 selector | 페이지 반쪽만 다크 |
| 사용자 토글 신호 분기 | 두 곳에서 토글 처리 | 동기화 버그 |
| brand 추가 | 둘 다 따로 설정 | 일관성 붕괴 |
custom condition (e.g. _jiraDark) | Panda만 지원 (Tailwind v3는 어려움) | 차원 어긋남 |
Panda의 conditions는 임의 selector를 condition 키로 등록할 수 있다 — _dark, _jira, _highContrast 등. Tailwind v4의 @variant도 v3 대비 비슷한 유연성을 갖췄다. 두 시스템의 condition 정의를 같은 selector로 정렬하는 것이 핵심.
How — 어떻게 동작하는가
What — 구체 사양 / 수치 / 예시
1) Panda CSS — conditions 정의
// panda.config.ts
import { defineConfig } from '@pandacss/dev';
export default defineConfig({
// selectors라는 이름의 1급 시민
conditions: {
// 기본 다크 — next-themes 호환
dark: '[data-theme=dark] &',
light: '[data-theme=light] &',
// 시스템 추적
osDark: '@media (prefers-color-scheme: dark)',
osLight: '@media (prefers-color-scheme: light)',
// 브랜드 (멀티 브랜드 챕터 참조)
jira: '[data-brand=jira] &',
confluence: '[data-brand=confluence] &',
// 직교 결합 — brand × theme
jiraDark: '[data-brand=jira][data-theme=dark] &',
// 접근성
highContrast: '@media (prefers-contrast: more)',
reducedMotion: '@media (prefers-reduced-motion: reduce)',
forcedColors: '@media (forced-colors: active)',
},
theme: {
semanticTokens: {
colors: {
'bg.surface': {
value: {
base: '{colors.gray.50}',
_dark: '{colors.gray.900}',
_highContrast: '{colors.white}',
},
},
'bg.accent': {
value: {
base: '{colors.blue.500}',
_dark: '{colors.blue.400}',
_jira: '{colors.blue.600}',
_jiraDark: '{colors.blue.300}',
},
},
},
},
},
});사용:
import { css } from 'styled-system/css';
<button className={css({
bg: 'bg.accent', // 모든 컨텍스트에서 자동
color: 'fg.on.accent',
_hover: { bg: 'bg.accent.hover' },
_reducedMotion: { transition: 'none' },
})}>
Click
</button>Panda는 컴파일 시 모든 condition 조합을 CSS variable swap으로 컴파일.
2) Tailwind v3 — darkMode: 'selector' (3.4.1+)
// tailwind.config.js
module.exports = {
// 두 selector 모두 매치 — next-themes 호환
darkMode: ['selector', '[data-theme="dark"]'],
content: ['./src/**/*.{tsx,html}'],
theme: {
extend: {
// semantic token을 CSS variables로 정의
colors: {
'bg-surface': 'var(--color-bg-surface)',
'bg-accent': 'var(--color-bg-accent)',
'fg-default': 'var(--color-fg-default)',
'fg-on-accent': 'var(--color-fg-on-accent)',
},
},
},
plugins: [
// 커스텀 variant 추가 (v3에서 brand 등)
function ({ addVariant }) {
addVariant('jira', '[data-brand="jira"] &');
addVariant('confluence', '[data-brand="confluence"] &');
addVariant('high-contrast', '@media (prefers-contrast: more)');
},
],
};/* globals.css — CSS variables를 직접 정의 */
:root {
--color-bg-surface: #fafafa;
--color-bg-accent: #3370b8;
}
[data-theme="dark"] {
--color-bg-surface: #18181b;
--color-bg-accent: #4a8cd4;
}
[data-brand="jira"] {
--color-bg-accent: #0052cc;
}마크업 — clean, no dark: prefix:
<button class="bg-bg-accent text-fg-on-accent">Click</button>Tailwind v3의 전통 패턴(
bg-white dark:bg-gray-900)을 굳이 안 따른다. 대신 semantic token을 Tailwind utility 이름으로 등록.
3) Tailwind v4 — @theme + @variant
/* app.css */
@import "tailwindcss";
/* dark variant를 [data-theme=dark]로 재정의 */
@variant dark (&:where([data-theme=dark], [data-theme=dark] *));
/* brand variant 신규 정의 */
@variant jira (&:where([data-brand=jira] *));
@variant confluence (&:where([data-brand=confluence] *));
@variant high-contrast (@media (prefers-contrast: more));
@theme {
/* primitive */
--color-blue-500: #3370b8;
--color-blue-400: #4a8cd4;
--color-gray-50: #fafafa;
--color-gray-900: #18181b;
/* semantic — light 기본 */
--color-bg-surface: var(--color-gray-50);
--color-bg-accent: var(--color-blue-500);
}
@layer base {
[data-theme="dark"] {
--color-bg-surface: var(--color-gray-900);
--color-bg-accent: var(--color-blue-400);
}
[data-brand="jira"] {
--color-bg-accent: #0052cc;
}
}이제 bg-bg-surface 같은 semantic-named utility가 모든 컨텍스트에서 자동.
4) Panda + Tailwind 동시 사용 — 같은 selector
07-panda-tailwind-interop의 핵심 주제지만, 테마 측면에서 정렬할 것:
| 시스템 | dark selector | brand selector |
|---|---|---|
| HTML | <html data-theme="dark"> | <html data-brand="jira"> |
| Panda | _dark condition | _jira condition |
| Tailwind v3 | darkMode: ['selector', '[data-theme="dark"]'] | addVariant('jira', '[data-brand=jira] &') |
| Tailwind v4 | @variant dark (...) | @variant jira (...) |
같은 HTML attribute(data-theme, data-brand)를 보면 세 시스템이 함께 작동.
5) condition 우선순위 — 직교 결합이 중요한 이유
// panda.config.ts
conditions: {
dark: '[data-theme=dark] &',
jira: '[data-brand=jira] &',
jiraDark: '[data-brand=jira][data-theme=dark] &',
},
theme: {
semanticTokens: {
colors: {
'bg.accent': {
value: {
base: '#3370b8', // 기본 (light + 기본 brand)
_dark: '#4a8cd4', // 다크 + 기본 brand
_jira: '#0052cc', // 라이트 + jira
_jiraDark: '#4c9aff', // 다크 + jira (직교)
},
},
},
},
}직교 결합이 없으면 jira 다크에서 일반 다크 색이 나온다 — _jiraDark를 명시해야 cascade가 정확.
6) 토큰 SSOT → 두 라이브러리에 동시 주입
DTCG JSON 한 벌을 Style Dictionary로 변환:
# tokens.json (DTCG)
# → CSS variables (globals.css)
# → tailwind.config.ts theme.extend
# → panda preset
sd build07-panda-tailwind-interop에서 다루는 패턴. 핵심은 토큰 이름이 두 시스템에서 동일해야 한다는 것.
// styled-system/preset (Panda)
'bg.accent': { value: { base: '...', _dark: '...' } }
// Tailwind config
colors: { 'bg-accent': 'var(--color-bg-accent)' }이름 매핑 규칙: Panda의 bg.accent → Tailwind utility는 bg-bg-accent (또는 bg-accent 같이 prefix 정리).
7) Runtime API — 토글 코드
<html>에 attribute를 박는 코드는 Panda/Tailwind 모두 같다:
export function setTheme(theme: 'light' | 'dark') {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
export function setBrand(brand: 'jira' | 'confluence' | 'trello') {
document.documentElement.setAttribute('data-brand', brand);
localStorage.setItem('brand', brand);
}What-if — 잘못 쓰면 어떻게 깨지는가
- 함정 1 — Tailwind v3 default
darkMode로 두기: 기본은'media'. 사용자 토글 불가. 반드시'class'또는'selector'로. - 함정 2 — Panda
_darkcondition을 만들었는데<html data-theme>안 박음: condition은 selector 매치. HTML에 attribute 없으면 영원히base만. - 함정 3 — Tailwind v3에서
darkMode: ['selector', '[data-theme="dark"]']가 안 먹힘: v3.4.1 이전 버전이거나, selector 안에&위치를 잘못. 정확히[data-theme="dark"](공백 뒤&없이도 작동) 또는[data-theme="dark"] &두 형태 모두 가능 — 문서 참조. - 함정 4 — 두 시스템이 다른 selector를 보고 있음: Panda는
[data-theme=dark], Tailwind는.dark클래스. 결과: 한쪽 컴포넌트만 다크. 반드시 같은 selector로 정렬. - 함정 5 —
_dark와dark:둘 다 마크업에 직접:<button class={css({ _dark: {...} })} {...} class="dark:bg-gray-900">. 의도 분산. semantic token 한 곳에만 분기 정의. - 함정 6 — custom condition 이름 충돌: Panda는
_jira, Tailwind에는jira:— 다른 컴포넌트가 다른 키워드를 쓰면 디버깅 지옥. 팀 컨벤션으로 동일한 이름 강제. - 함정 7 —
@variant(Tailwind v4)에서&없이 작성: v4의@variant는 nesting 안에&를 반드시 포함해야 한다. 빠뜨리면 selector가 잘못 빌드.
Insight — 흥미로운 이야기
Panda CSS의
conditionsAPI는 Stitches의media+variants+compoundVariants3개를 하나의 1급 시민으로 합친 것이다.
Stitches(2020)는 반응형은 media, 상태는 &:hover, 복합은 compoundVariants로 흩어져 있었다. Panda의 Segun Adebayo(Chakra UI 창시자, Panda 메인테이너)는 이 셋이 사실 같은 추상임을 발견했다 — “condition = 임의의 CSS selector + media query를 condition 키로 등록 가능”. _hover, _dark, _landscape, _print, _brandJira 모두 동등.
이게 강력한 이유: 새 차원을 추가할 때 프레임워크 수정이 필요 없다. 디자인 시스템 팀이 _a11yVisualImpairment: '[data-vi=true] &' 같은 condition을 자기 프로젝트에서만 추가할 수 있다. Tailwind v3는 그게 addVariant 플러그인을 짜야 했고, v4는 @variant로 비슷한 유연성을 CSS 측에 가져왔다.
흥미로운 디테일: Adam Wathan(Tailwind)과 Segun Adebayo(Panda)는 서로의 결정을 공개적으로 인용한다. v4 발표 영상에서 Adam은 “Panda CSS와 Vanilla Extract가 보여준 CSS variables 우선 패턴을 우리도 받아들였다”고 언급했고, Panda 문서는 Tailwind의 utility-first 철학을 인정하면서 recipe 시스템으로 차별화한다. 두 진영의 경쟁이 아닌 수렴이 2023~2025년 CSS 진영의 큰 그림.
마지막 통찰: conditions는 사실 6장의 마지막 추상이다. dark mode, multi-brand, RTL, system preferences — 이 챕터의 모든 주제가 condition 한 개씩으로 환원된다. _dark, _jira, _rtl, _reducedMotion, _forcedColors — Panda 사용자는 같은 API로 7개 챕터를 푼다. 추상화가 7개 문제를 1개로 줄였다.
요약
[data-theme="dark"]selector를 모든 시스템의 공통 좌표로 정하라.- Panda는
conditions._dark, Tailwind v3는darkMode: ['selector', '...'], Tailwind v4는@variant dark (...). - Panda의
conditions는 임의 selector 등록이 가능해 brand, contrast, locale, motion 모두 같은 API로. - 두 시스템이 다른 selector를 보면 페이지 반쪽만 테마 적용 — 반드시 정렬.
- Tailwind v4의
@variant는 v3의 plugin 작성 부담을 CSS 측 한 줄로 줄였다.