스타일 · Tailwind CSS · 반응형 · 상태 · 다크모드

Tailwind CSS · 반응형, 상태, 다크모드

지금까지는 "기본 모양"을 만드는 클래스였습니다. 이번엔 접두사를 붙여 "언제 적용할지"를 정합니다. 화면이 넓을 때만(md:), 마우스를 올렸을 때만(hover:), 어두운 테마일 때만(dark:) 같은 조건을 클래스 앞에 붙이는 방식이죠.

접두사는 클래스 앞에 붙이는 "스위치"입니다. bg-blue-500은 항상 켜진 전등이고, hover:bg-blue-500은 "마우스를 올리면 켜지는" 전등이에요. 같은 색이라도 켜지는 조건이 다릅니다.

1. 반응형 — 모바일 우선(mobile-first)

Tailwind? 의 반응형은 "작은 화면이 기본"이라는 원칙으로 동작합니다. 접두사가 없는 클래스는 모든 크기에 적용되는 기본값이고, sm: md: lg: xl: 2xl: 접두사가 붙은 클래스는 그 폭 이상에서만 적용됩니다. "최소 폭" 기준이라 작은 화면에는 영향을 주지 않아요.

옷을 입을 때 속옷(기본 클래스)은 늘 입고, 날이 추워지면(화면이 커지면) 그 위에 외투(md:)를 덧입는다고 생각하면 됩니다. 작은 화면=얇게, 큰 화면=덧입기.

v4의 기본 브레이크포인트는 다음과 같습니다(공식 기준).

접두사최소 폭px 환산의미
(없음)00px모든 크기 — 기본값
sm:40rem640px작은 화면 이상
md:48rem768px태블릿 이상
lg:64rem1024px노트북 이상
xl:80rem1280px데스크톱 이상
2xl:96rem1536px큰 데스크톱 이상

흔한 패턴 두 가지 — 글자 크기와 그리드 열 수를 화면에 따라 바꿉니다.

<!-- 기본 작게 → md 이상 보통 → lg 이상 크게 -->
<p class="text-sm md:text-base lg:text-lg">화면 따라 커지는 글자</p>

<!-- 모바일은 1열, md 이상에서 3열 그리드 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
  <div>...</div> <div>...</div> <div>...</div>
</div>

읽는 법: grid-cols-1(기본 1열)이 모든 화면에 깔리고, 폭이 48rem(md)을 넘으면 md:grid-cols-3가 덮어써서 3열이 됩니다.

임의 브레이크포인트 — min-[600px]:

정해진 sm/md/lg로 부족하면 대괄호로 직접 폭을 지정할 수 있습니다. min-[600px]:flex 는 "폭 600px 이상에서 flex". 반대로 max-[600px]:hidden 처럼 max- 로 "그 폭 미만"도 됩니다. md:max-lg:flex 처럼 두 개를 겹치면 "md~lg 사이 구간만"도 가능해요.

데모는 좁아서 반응형이 잘 안 보입니다

아래 라이브 데모의 실행 결과 창(iframe)은 폭이 좁아 sm:/md: 변화가 잘 드러나지 않습니다. 그래서 반응형은 위처럼 코드로 설명하고, 아래 라이브 데모는 직접 만져볼 수 있는 hover·focus·group·peer·dark 같은 상태 위주로 준비했습니다.

2. 상태 variant — hover, focus, ...

variant(배리언트)는 "어떤 상태일 때 적용"을 뜻하는 접두사입니다. 마우스를 올렸을 때, 클릭 중일 때, 입력칸이 비활성일 때 등 평소 CSS?:hover :focus 가상 클래스에 해당하는 것들을 클래스 앞에 붙여 씁니다.

variant적용 시점
hover:마우스를 올렸을 때
focus: / focus-visible:포커스됐을 때 / 키보드 포커스일 때만
focus-within:자식 중 하나라도 포커스됐을 때
active:누르고 있는 동안
visited:방문한 링크
disabled:비활성 상태(disabled)
checked: / required:체크된 입력 / 필수 입력
placeholder:입력칸의 안내 글자(placeholder)
first: / last:형제 중 첫째 / 막내
odd: / even:홀수 번째 / 짝수 번째(줄무늬 표)
empty:내용이 비어 있을 때
before: / after:가상 요소(앞/뒤에 끼우는 장식)
<!-- 평소엔 파랑, 올리면 진한 파랑, 누르면 더 진하게 -->
<button class="bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white px-4 py-2 rounded">
  버튼
</button>

<!-- 줄무늬 표: 짝수 줄만 회색 배경 -->
<tr class="even:bg-gray-100">...</tr>

<!-- 가상 요소: 필수 항목 뒤에 빨간 별 붙이기 (content 필요) -->
<label class="after:content-['*'] after:text-red-500 after:ml-1">이름</label>

before:/after: 는 거의 항상 content-[''](또는 content-['★'] 같은 내용)와 짝으로 씁니다. 내용이 없으면 가상 요소가 화면에 안 나타나요.

버튼 하나로 hover·focus를 직접 만져보세요. (마우스를 올리거나 Tab으로 포커스)

3. group / peer — 부모·형제 상태로 제어

상태 variant는 보통 "자기 자신"의 상태를 봅니다. 그런데 "부모에 마우스를 올리면 자식 색이 바뀌게" 하고 싶을 때가 있죠. 이때 두 가지 도구를 씁니다.

  • group — 부모 요소에 class="group" 을 달면, 자식에서 group-hover: group-focus:부모의 상태를 보고 반응합니다.
  • peer — 앞선 형제 요소에 class="peer" 를 달면, 뒤따르는 형제에서 peer-checked: peer-focus:그 형제의 상태를 보고 반응합니다.
group은 "부모가 웃으면 아이도 따라 웃기", peer는 "옆 친구가 손을 들면 나도 반응하기"입니다. group은 위→아래(부모→자식), peer는 옆→옆(형제→형제) 방향이에요.
<!-- group: 카드(부모)에 올리면 안의 화살표가 움직임 -->
<a class="group block p-4 border rounded">
  더 보기
  <span class="inline-block group-hover:translate-x-1 transition">→</span>
</a>

<!-- peer: 체크박스(형제)가 켜지면 뒤의 라벨 색이 바뀜 -->
<input type="checkbox" class="peer" id="agree">
<label for="agree" class="peer-checked:text-green-600 peer-checked:line-through">
  동의함
</label>

주의: peer 는 HTML 구조상 먼저 나온 형제의 상태만 볼 수 있습니다(CSS의 형제 선택자 한계). 그래서 입력칸을 라벨보다 에 둡니다.

카드에 마우스를 올려보세요(group). 화살표가 움직이고 테두리 색이 바뀝니다.

체크박스를 직접 켜고 꺼보세요(peer). 켜면 라벨 모양과 안내 문구가 바뀝니다.

위 데모에서 두 번째 문단은 라벨 바깥이라 반응하지 않는 게 정상입니다. peer-checked:같은 부모 안에서 체크박스 뒤에 오는 형제에만 닿습니다.

4. data-* / aria-* variant

요즘 컴포넌트는 "열림/닫힘", "선택됨" 같은 상태를 data-statearia-expanded 같은 속성으로 표시합니다. Tailwind는 이 속성값을 직접 보고 스타일을 바꿀 수 있어요.

<!-- data-state 가 open 일 때만 적용 -->
<div data-state="open" class="data-[state=open]:bg-blue-50 data-[state=closed]:hidden">
  패널
</div>

<!-- aria-expanded 가 true 일 때 화살표 회전 -->
<button aria-expanded="true" class="aria-[expanded=true]:rotate-180">▾</button>
Shadcn / Radix와의 연계

이 패턴은 Shadcn UI(내부적으로 Radix)에서 핵심입니다. Radix가 컴포넌트 상태를 data-[state=open], data-[side=top] 등으로 자동으로 달아 주면, 우리는 그에 맞춰 data-[state=open]:animate-in 같은 클래스로 모양만 입히면 됩니다. 자세한 건 Shadcn 챕터에서 다룹니다.

5. 다크모드 — dark:

어두운 테마용 스타일은 dark: 접두사로 답니다. 평소 색을 기본으로 두고, 어두울 때 덮어쓸 색을 dark: 로 더해요.

<!-- 밝을 땐 흰 배경/검은 글자, 어두울 땐 짙은 배경/밝은 글자 -->
<div class="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
  본문
</div>
v4의 다크모드 기본은 "시스템 설정"입니다

Tailwind v4 에서 dark: 의 기본 동작은 운영체제의 다크모드 설정(prefers-color-scheme)을 따릅니다. 즉 따로 설정하지 않으면 OS가 어두우면 dark: 가 켜집니다. 버튼으로 직접 토글하는 방식(.dark 클래스)으로 바꾸려면, CSS 파일에 아래 한 줄을 추가해 dark variant를 재정의해야 합니다.

/* app.css — .dark 클래스 토글 방식으로 변경 (v4) */
@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *));

이렇게 두면 <html class="dark"> (또는 그 안 어떤 조상에든 .dark)가 있을 때만 dark: 가 적용됩니다. 그 다음 자바스크립트로 document.documentElement.classList.toggle("dark") 처럼 클래스를 켜고 끄면 됩니다.

이 사이트의 다크 토글과 라이브 데모

이 문서 우상단의 🌙 버튼으로 테마를 바꿀 수 있습니다. 라이브 데모의 실행 창은 현재 사이트 테마를 그대로 따라갑니다 (테마가 어두우면 데모도 .dark 클래스로 실행돼요). 테마를 바꾼 뒤 데모의 실행 버튼을 다시 누르면 새 테마로 다시 그려집니다.

6. variant 조합 순서

여러 조건을 한 번에 걸 수도 있습니다. 접두사를 이어 붙이면 "모두 만족할 때"가 됩니다. 읽는 순서는 왼쪽이 바깥, 오른쪽이 안쪽이라고 보면 편해요.

<!-- md 이상이면서, 마우스를 올렸을 때만 -->
<a class="underline md:hover:text-blue-600">링크</a>

<!-- 다크모드이면서, md 이상일 때만 배경 변경 -->
<div class="dark:md:bg-gray-800">...</div>

<!-- 부모에 올렸고(group) + 다크모드일 때 -->
<span class="dark:group-hover:text-white">...</span>

순서를 바꿔도(hover:md:md:hover:) 결과는 대개 같습니다. 둘 다 "두 조건 AND"라서요. 팀 안에서 한 가지 순서로 통일해 두면 읽기 편합니다.

한눈에 — variant 카탈로그

분류variant적용 시점
반응형sm: md: lg: xl: 2xl:그 폭 이상
반응형(임의)min-[600px]: max-[600px]:지정 폭 이상 / 미만
마우스/포커스hover: focus: focus-visible: focus-within: active:올림 / 포커스 / 누름
폼 상태disabled: checked: required: placeholder:입력 요소 상태
구조first: last: odd: even: empty:형제 위치 / 비어 있음
가상 요소before: after:앞/뒤 장식(content 필요)
부모/형제group-hover: group-focus: peer-checked: peer-focus:부모/형제 상태
속성data-[state=open]: aria-[expanded=true]:속성값 일치
다크dark:다크모드(시스템 또는 .dark)
조합md:hover: dark:md:모든 조건 AND
이 프로젝트와의 관계

이 프로젝트 프론트엔드는 Tailwind CSS 4 로 화면을 짜며, 반응형 레이아웃·버튼 호버·다크모드 토글이 모두 이 variant들로 표현됩니다. 특히 data-[state=...] / aria-[...] variant는 Shadcn UI 컴포넌트의 동작 표현에 그대로 쓰이고, 실제 화면 구성은 웹앱 7장 UI 구성(07-ui) 에서 확인할 수 있습니다.

다음 단계