04 컴포넌트와 UI 요소
4장: 컴포넌트와 UI 요소
4-1. 재사용 가능한 컴포넌트 패턴
@utility — v4 커스텀 컴포넌트 클래스
v3 → v4 변경사항 —
@layer components { .btn { @apply ... } }방식 대신@utility를 사용한다. v4에서도@apply는 동작하지만@utility가 권장된다.
@utility란? — Tailwind의 유틸리티 클래스처럼 동작하는 나만의 클래스를 정의하는 문법이다. bg-blue-600 같은 기본 유틸리티와 동일한 우선순위로 적용되므로, 다른 유틸리티 클래스와 자연스럽게 조합할 수 있다.
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-1. @utility 컴포넌트</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8 <style type="text/tailwindcss">9 /* ── @utility로 커스텀 컴포넌트 정의 ── */10
11 /* 버튼 공통 스타일 */12 @utility btn {13 display: inline-flex;14 align-items: center;15 justify-content: center;16 padding: 0.5rem 1rem;17 border-radius: 0.5rem;18 font-weight: 600;19 font-size: 0.875rem;20 transition: all 0.15s ease;21 cursor: pointer;22 }23
24 /*25 theme() 함수로 Tailwind 기본 팔레트 색상을 참조한다.26 예: theme(--color-blue-600) → Tailwind의 blue-600 색상값27 */28 @utility btn-primary {29 background-color: theme(--color-blue-600);30 color: white;31 &:hover { background-color: theme(--color-blue-700); }32 }33
34 @utility btn-secondary {35 background-color: theme(--color-gray-100);36 color: theme(--color-gray-900);37 &:hover { background-color: theme(--color-gray-200); }38 }39
40 @utility btn-danger {41 background-color: theme(--color-red-600);42 color: white;43 &:hover { background-color: theme(--color-red-700); }44 }45
46 /* 카드 컴포넌트 */47 @utility card {48 background-color: white;49 border-radius: 1rem;50 box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);51 overflow: hidden;52 }53 </style>54</head>55<body class="bg-gray-50 p-8">56
57 <h2 class="text-lg font-bold mb-4">@utility 버튼</h2>58 <div class="flex gap-3 mb-8">59
60 <button class="btn btn-primary">저장</button>61 <button class="btn btn-secondary">취소</button>62 <button class="btn btn-danger">삭제</button>63 </div>64
65 <h2 class="text-lg font-bold mb-4">@utility 카드</h2>66
67 <div class="card p-6 max-w-sm">68 <h3 class="font-bold text-gray-900">카드 제목</h3>69 <p class="mt-2 text-sm text-gray-600">카드 내용이 여기에 들어간다.</p>70 </div>71
72</body>73</html>@apply — 기존 방식 (v4에서도 동작)
@apply는 @layer components 안에서 기존 유틸리티 클래스를 그대로 조합해 새 클래스를 만드는 방식이다. v4에서도 동작하지만 @utility를 권장한다.
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-1. @apply 방식</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8 <style type="text/tailwindcss">9 @layer components {10 .badge {11 @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;12 }13 .badge-blue { @apply bg-blue-100 text-blue-800; }14 .badge-green { @apply bg-green-100 text-green-800; }15 .badge-red { @apply bg-red-100 text-red-800; }16 .badge-gray { @apply bg-gray-100 text-gray-800; }17 }18 </style>19</head>20<body class="bg-gray-50 p-8">21
22 <div class="flex gap-3">23 <span class="badge badge-blue">신규</span>24 <span class="badge badge-green">완료</span>25 <span class="badge badge-red">오류</span>26 <span class="badge badge-gray">대기</span>27 </div>28
29</body>30</html>@utility vs @apply 차이 정리
항목 @utility@apply위치 최상위에 바로 작성 @layer components { }안에 작성문법 일반 CSS 속성 사용 Tailwind 유틸리티 클래스 나열 우선순위 유틸리티와 동일 레이어 components레이어 (유틸리티보다 낮음)권장 여부 v4 권장 v4에서도 동작하지만 레거시
4-2. 버튼 컴포넌트
기본 버튼 스타일
버튼은 시각적 피드백이 중요하다. hover: → active: → focus: → disabled: 순서로 상태별 스타일을 지정한다.
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-2. 버튼</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8</head>9<body class="bg-gray-50 p-8 space-y-6">10
11
12 <div>13 <p class="text-sm font-medium text-gray-500 mb-2">기본 버튼</p>14 <button class="bg-blue-600 hover:bg-blue-700 active:bg-blue-80015 text-white font-semibold16 px-4 py-2 rounded-lg17 transition-colors duration-15018 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-219 disabled:opacity-50 disabled:cursor-not-allowed">20 기본 버튼21 </button>22
23 <button disabled class="bg-blue-600 text-white font-semibold24 px-4 py-2 rounded-lg ml-225 disabled:opacity-50 disabled:cursor-not-allowed">26 비활성 버튼27 </button>28 </div>29
30
31 <div>32 <p class="text-sm font-medium text-gray-500 mb-2">외곽선(Outline) 버튼</p>33 <button class="border-2 border-blue-600 text-blue-60034 hover:bg-blue-50 active:bg-blue-10035 font-semibold px-4 py-2 rounded-lg36 transition-colors duration-15037 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">38 외곽선 버튼39 </button>40 </div>41
42
43 <div>44 <p class="text-sm font-medium text-gray-500 mb-2">고스트(Ghost) 버튼 — 배경 없이 텍스트만</p>45 <button class="text-gray-700 hover:bg-gray-100 active:bg-gray-20046 font-medium px-4 py-2 rounded-lg47 transition-colors duration-15048 focus:outline-none focus:ring-2 focus:ring-gray-400">49 고스트 버튼50 </button>51 </div>52
53
54 <div>55 <p class="text-sm font-medium text-gray-500 mb-2">아이콘 + 텍스트 버튼 — gap-2로 간격 조절</p>56 <button class="inline-flex items-center gap-257 bg-green-600 hover:bg-green-70058 text-white font-semibold px-4 py-2 rounded-lg59 transition-colors duration-150">60
61 <svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">62 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>63 </svg>64 추가65 </button>66 </div>67
68</body>69</html>버튼 크기 변형
크기는 px(좌우 패딩), py(상하 패딩), text-*(글자 크기), rounded-*(둥글기)의 조합으로 결정된다.
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-2. 버튼 크기</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8</head>9<body class="bg-gray-50 p-8">10
11 <div class="flex items-end gap-4 flex-wrap">12
13 <button class="px-2.5 py-1.5 text-xs font-medium bg-blue-600 text-white rounded-md">14 소형15 </button>16
17
18 <button class="px-4 py-2 text-sm font-semibold bg-blue-600 text-white rounded-lg">19 중형20 </button>21
22
23 <button class="px-6 py-3 text-base font-semibold bg-blue-600 text-white rounded-xl">24 대형25 </button>26 </div>27
28
29 <div class="mt-6 max-w-sm">30 <button class="w-full px-4 py-3 text-sm font-semibold bg-blue-600 text-white rounded-lg">31 전체 너비 버튼32 </button>33 </div>34
35</body>36</html>4-3. 카드 컴포넌트
기본 카드
실습 팁 — 이미지 자리에 picsum.photos를 사용하면 실제 이미지를 확인할 수 있다.
object-cover는 이미지가 영역을 꽉 채우면서 비율을 유지하게 해 준다.
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-3. 카드</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8</head>9<body class="bg-gray-100 p-8">10
11 <div class="max-w-sm">12 <div class="bg-white rounded-2xl shadow-md overflow-hidden">13
14 <img src="https://picsum.photos/seed/card1/400/300" alt="상품 이미지"15 class="w-full h-48 object-cover" />16 <div class="p-5">17 <span class="text-xs font-medium text-blue-600 bg-blue-50 px-2 py-1 rounded-full">18 카테고리19 </span>20
21 <h3 class="mt-2 text-lg font-bold text-gray-900 line-clamp-2">22 상품 또는 콘텐츠 제목이 여기에 들어간다23 </h3>24
25 <p class="mt-1 text-sm text-gray-500 line-clamp-3">26 설명 텍스트가 여기에 들어간다. 3줄이 넘으면 말줄임 처리된다.27 긴 텍스트를 넣어서 직접 확인해 보자.28 이렇게 세 줄을 넘기면 자동으로 잘린다.29 이 줄은 보이지 않을 것이다.30 </p>31 <div class="mt-4 flex items-center justify-between">32 <span class="text-xl font-bold text-gray-900">₩29,000</span>33 <button class="bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium34 px-3 py-1.5 rounded-lg transition-colors">35 구매36 </button>37 </div>38 </div>39 </div>40 </div>41
42</body>43</html>호버 효과 카드
group 클래스를 부모에 붙이면, 부모에 마우스를 올렸을 때 자식 요소들을 group-hover:로 제어할 수 있다.
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-3. 호버 카드</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8</head>9<body class="bg-gray-100 p-8">10
11 <div class="max-w-sm">12
13 <div class="group bg-white rounded-2xl shadow hover:shadow-xl14 transition-all duration-300 overflow-hidden cursor-pointer">15 <div class="overflow-hidden h-48">16
17 <img src="https://picsum.photos/seed/hover1/400/300" alt=""18 class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />19 </div>20 <div class="p-5">21
22 <h3 class="font-bold text-gray-900 group-hover:text-blue-600 transition-colors">23 호버해 보세요24 </h3>25 <p class="mt-1 text-sm text-gray-500">이미지 확대 + 제목 색상 변경</p>26 </div>27 </div>28 </div>29
30</body>31</html>가로형 카드
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-3. 가로형 카드</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8</head>9<body class="bg-gray-100 p-8">10
11 <div class="max-w-lg">12
13 <div class="flex bg-white rounded-2xl shadow-md overflow-hidden">14 <img src="https://picsum.photos/seed/hcard/200/200" alt=""15 class="w-32 sm:w-48 flex-shrink-0 object-cover" />16 <div class="flex flex-col justify-between p-4">17 <div>18 <h3 class="font-bold text-gray-900">가로형 카드 제목</h3>19 <p class="mt-1 text-sm text-gray-500 line-clamp-2">20 가로형 카드는 목록형 UI에 적합하다. 썸네일을 왼쪽에 고정하고 텍스트를 오른쪽에 배치한다.21 </p>22 </div>23 <div class="flex items-center gap-2 mt-3">24 <img src="https://picsum.photos/seed/avatar/100/100" alt=""25 class="size-6 rounded-full" />26 <span class="text-xs text-gray-500">작성자 · 2일 전</span>27 </div>28 </div>29 </div>30 </div>31
32</body>33</html>4-4. 폼(Form) 컴포넌트
v4 신규 —
field-sizing-content로 textarea가 입력 내용에 맞게 자동으로 높이가 늘어난다. (2024년 기준 Chrome 123+, Edge 123+ 지원. Safari·Firefox는 미지원이므로min-h-24로 최소 높이를 함께 지정한다.)
기본 입력 스타일
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-4. 폼</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8</head>9<body class="bg-gray-50 p-8">10
11
12 <form class="space-y-4 max-w-md" onsubmit="event.preventDefault(); alert('제출됨!')">13
14
15 <div>16 <label for="name" class="block text-sm font-medium text-gray-700 mb-1">이름</label>17
18 <input type="text" id="name" placeholder="홍길동"19 class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm20 placeholder:text-gray-40021 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent22 disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed23 transition-shadow duration-150" />24 </div>25
26
27 <div>28 <label for="email" class="block text-sm font-medium text-gray-700 mb-1">이메일</label>29 <input type="email" id="email" placeholder="example@email.com"30 class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm31 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />32 </div>33
34
35 <div>36 <label for="pw" class="block text-sm font-medium text-gray-700 mb-1">비밀번호</label>37
38 <input type="password" id="pw"39 class="w-full rounded-lg border border-red-400 bg-red-50 px-3 py-2 text-sm40 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent" />41 <p class="mt-1 text-xs text-red-600">비밀번호를 입력해 주세요.</p>42 </div>43
44
45 <div>46 <label for="msg" class="block text-sm font-medium text-gray-700 mb-1">메시지</label>47
48 <textarea id="msg" rows="4"49 class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm resize-none50 field-sizing-content min-h-2451 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"52 placeholder="내용을 입력하세요 — 줄이 늘어나면 높이가 자동으로 커진다"></textarea>53 </div>54
55
56 <div>57 <label for="role" class="block text-sm font-medium text-gray-700 mb-1">역할</label>58
59 <select id="role"60 class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm61 focus:outline-none focus:ring-2 focus:ring-blue-50062 appearance-none bg-no-repeat bg-right"63 style="background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-position: right 0.75rem center; padding-right: 2rem;">64 <option value="">선택하세요</option>65 <option value="admin">관리자</option>66 <option value="user">일반 사용자</option>67 <option value="viewer">뷰어</option>68 </select>69 </div>70
71
72 <button type="submit"73 class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold74 py-2.5 rounded-lg transition-colors duration-15075 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">76 제출77 </button>78 </form>79
80</body>81</html>체크박스 · 라디오 · 토글
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-4. 체크박스·라디오·토글</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8</head>9<body class="bg-gray-50 p-8 space-y-8">10
11
12 <div>13 <p class="text-sm font-bold text-gray-700 mb-3">체크박스</p>14 <label class="flex items-center gap-2 cursor-pointer group">15 <input type="checkbox"16 class="size-4 rounded border-gray-300 text-blue-60017 focus:ring-2 focus:ring-blue-500 cursor-pointer" />18
19 <span class="text-sm text-gray-700 group-hover:text-gray-900">이용약관 동의</span>20 </label>21 </div>22
23
24 <fieldset class="space-y-2">25 <legend class="text-sm font-bold text-gray-700 mb-2">배송 방법 (라디오)</legend>26 <label class="flex items-center gap-2 cursor-pointer">27 <input type="radio" name="delivery" value="regular"28 class="size-4 border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" />29 <span class="text-sm text-gray-700">일반 배송 (3~5일)</span>30 </label>31 <label class="flex items-center gap-2 cursor-pointer">32 <input type="radio" name="delivery" value="express"33 class="size-4 border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" />34 <span class="text-sm text-gray-700">빠른 배송 (1~2일) +₩3,000</span>35 </label>36 </fieldset>37
38
39 <div>40 <p class="text-sm font-bold text-gray-700 mb-3">토글 스위치</p>41
42 <label class="inline-flex items-center cursor-pointer gap-3">43 <span class="text-sm font-medium text-gray-700">알림 수신</span>44 <div class="relative">45 <input type="checkbox" class="sr-only peer" />46
47 <div class="w-11 h-6 bg-gray-200 peer-checked:bg-blue-600 rounded-full48 transition-colors duration-20049 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 peer-focus:ring-offset-2">50 </div>51
52 <div class="absolute top-0.5 left-0.5 size-5 bg-white rounded-full shadow53 transition-transform duration-200 peer-checked:translate-x-5">54 </div>55 </div>56 </label>57 </div>58
59</body>60</html>4-5. 내비게이션
상단 네비게이션 바 (모바일 메뉴 포함)
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-5. 네비게이션</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8</head>9<body class="bg-gray-50">10
11
12 <nav class="bg-white border-b border-gray-200 sticky top-0 z-50">13 <div class="max-w-6xl mx-auto px-4 sm:px-6">14 <div class="flex items-center justify-between h-16">15
16 <a href="#" class="flex items-center gap-2 font-bold text-lg text-gray-900">17 <div class="size-8 bg-blue-600 rounded-lg"></div>18 MyApp19 </a>20
21
22 <ul class="hidden md:flex items-center gap-1">23 <li><a href="#" class="px-3 py-2 rounded-lg text-sm font-medium text-gray-900 hover:bg-gray-100 transition-colors">홈</a></li>24 <li><a href="#" class="px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-100 transition-colors">소개</a></li>25 <li><a href="#" class="px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-100 transition-colors">서비스</a></li>26 <li><a href="#" class="px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-100 transition-colors">문의</a></li>27 </ul>28
29
30 <div class="hidden md:flex items-center gap-2">31 <a href="#" class="text-sm font-medium text-gray-600 hover:text-gray-900 px-3 py-2">로그인</a>32 <a href="#" class="bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold33 px-4 py-2 rounded-lg transition-colors">시작하기</a>34 </div>35
36
37 <button id="menu-btn" class="md:hidden p-2 rounded-lg text-gray-600 hover:bg-gray-100">38 <svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">39 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>40 </svg>41 </button>42 </div>43 </div>44
45
46 <div id="mobile-menu" class="hidden md:hidden border-t border-gray-200 px-4 py-3 space-y-1">47 <a href="#" class="block px-3 py-2 rounded-lg text-sm font-medium text-gray-900 hover:bg-gray-100">홈</a>48 <a href="#" class="block px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-100">소개</a>49 <a href="#" class="block px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-100">서비스</a>50 <a href="#" class="block px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-100">문의</a>51 </div>52 </nav>53
54
55 <div class="max-w-6xl mx-auto p-8 space-y-4">56 <p class="text-gray-500">↓ 스크롤해서 sticky 네비게이션을 확인해 보자.</p>57 <div class="h-[200vh]"></div>58 </div>59
60 <script>61 // 모바일 메뉴 토글62 const menuBtn = document.getElementById('menu-btn')63 const mobileMenu = document.getElementById('mobile-menu')64
65 menuBtn.addEventListener('click', () => {66 mobileMenu.classList.toggle('hidden')67 })68 </script>69
70</body>71</html>4-6. 모달 (Modal)
모달은 3개 레이어로 구성된다: 배경 오버레이 → 모달 패널 → 내부 콘텐츠(헤더/본문/푸터).
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-6. 모달</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8</head>9<body class="bg-gray-50 p-8">10
11
12 <button id="open-modal"13 class="bg-blue-600 hover:bg-blue-700 text-white font-semibold14 px-4 py-2 rounded-lg transition-colors">15 모달 열기16 </button>17
18
19 <div id="modal"20 class="hidden fixed inset-0 z-50 flex items-center justify-center p-4"21 role="dialog" aria-modal="true" aria-labelledby="modal-title">22
23
24 <div class="absolute inset-0 bg-black/60" id="modal-backdrop"></div>25
26
27 <div class="relative bg-white rounded-2xl shadow-2xl w-full max-w-md">28
29 <div class="flex items-center justify-between px-6 py-4 border-b border-gray-200">30 <h2 id="modal-title" class="text-lg font-bold text-gray-900">모달 제목</h2>31 <button id="close-modal"32 class="p-1 rounded-lg text-gray-400 hover:text-gray-600 hover:bg-gray-10033 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400">34 <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">35 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>36 </svg>37 <span class="sr-only">닫기</span>38 </button>39 </div>40
41
42 <div class="px-6 py-4">43 <p class="text-sm text-gray-600">44 모달 내용이 여기에 들어간다. 사용자에게 중요한 정보를 표시하거나 확인을 요청할 때 사용한다.45 </p>46 </div>47
48
49 <div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200">50 <button id="cancel-modal"51 class="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-10052 rounded-lg transition-colors">53 취소54 </button>55 <button class="px-4 py-2 text-sm font-semibold bg-blue-600 hover:bg-blue-70056 text-white rounded-lg transition-colors">57 확인58 </button>59 </div>60 </div>61 </div>62
63 <script>64 const modal = document.getElementById('modal')65 const openBtn = document.getElementById('open-modal')66 const closeBtn = document.getElementById('close-modal')67 const cancelBtn = document.getElementById('cancel-modal')68 const backdrop = document.getElementById('modal-backdrop')69
70 const openModal = () => modal.classList.remove('hidden')71 const closeModal = () => modal.classList.add('hidden')72
73 openBtn.addEventListener('click', openModal)74 closeBtn.addEventListener('click', closeModal)75 cancelBtn.addEventListener('click', closeModal)76 backdrop.addEventListener('click', closeModal)77
78 // ESC 키로도 닫기79 document.addEventListener('keydown', (e) => {80 if (e.key === 'Escape') closeModal()81 })82 </script>83
84</body>85</html>4-7. 드롭다운 메뉴
relative(부모) + absolute(패널) 조합으로 위치를 잡는다. 문서 아무 곳이나 클릭하면 닫히도록 처리한다.
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-7. 드롭다운</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8</head>9<body class="bg-gray-50 p-8">10
11
12 <div class="relative inline-block" id="dropdown-wrapper">13
14 <button id="dropdown-btn"15 class="inline-flex items-center gap-2 px-4 py-216 bg-white border border-gray-300 rounded-lg text-sm font-medium17 text-gray-700 hover:bg-gray-50 transition-colors18 focus:outline-none focus:ring-2 focus:ring-blue-500">19 메뉴20 <svg class="size-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">21 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>22 </svg>23 </button>24
25
26 <div id="dropdown-menu"27 class="hidden absolute right-0 mt-2 w-48 bg-white rounded-xl shadow-lg28 border border-gray-100 py-1 z-10"29 role="menu">30 <a href="#" class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50" role="menuitem">31 <svg class="size-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">32 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"33 d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>34 </svg>35 프로필36 </a>37 <a href="#" class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50" role="menuitem">38 <svg class="size-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">39 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"40 d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>41 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>42 </svg>43 설정44 </a>45 <hr class="my-1 border-gray-100" />46 <a href="#" class="flex items-center gap-2 px-4 py-2 text-sm text-red-600 hover:bg-red-50" role="menuitem">47 로그아웃48 </a>49 </div>50 </div>51
52 <script>53 const btn = document.getElementById('dropdown-btn')54 const menu = document.getElementById('dropdown-menu')55
56 // 버튼 클릭 → 토글 (e.stopPropagation으로 document 클릭과 분리)57 btn.addEventListener('click', (e) => {58 e.stopPropagation()59 menu.classList.toggle('hidden')60 })61
62 // 문서 아무 곳 클릭 → 닫기63 document.addEventListener('click', () => menu.classList.add('hidden'))64 </script>65
66</body>67</html>4-8. 알림(Toast / Alert)
색상 계열만 바꿔서 정보(blue), 성공(green), 경고(yellow), 오류(red) 4가지 상태를 표현한다.
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-8. 알림</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8</head>9<body class="bg-gray-50 p-8">10
11 <div class="max-w-lg space-y-4">12
13
14 <div class="flex items-start gap-3 bg-blue-50 border border-blue-200 text-blue-80015 rounded-xl px-4 py-3 text-sm" role="alert">16 <svg class="size-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">17 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"18 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>19 </svg>20 <p>새로운 업데이트가 있다. 확인해 보자.</p>21 </div>22
23
24 <div class="flex items-start gap-3 bg-green-50 border border-green-200 text-green-80025 rounded-xl px-4 py-3 text-sm" role="alert">26 <svg class="size-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">27 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>28 </svg>29 <p>저장이 완료됐다.</p>30 </div>31
32
33 <div class="flex items-start gap-3 bg-yellow-50 border border-yellow-200 text-yellow-80034 rounded-xl px-4 py-3 text-sm" role="alert">35 <svg class="size-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">36 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"37 d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>38 </svg>39 <p>저장공간이 80% 사용됐다.</p>40 </div>41
42
43 <div class="flex items-start gap-3 bg-red-50 border border-red-200 text-red-80044 rounded-xl px-4 py-3 text-sm" role="alert">45 <svg class="size-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">46 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"47 d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>48 </svg>49 <p>오류가 발생했다. 다시 시도해 보자.</p>50 </div>51
52 </div>53
54
55
56</body>57</html>4-9. 상태 변형 (State Variants)
hover / focus / active / disabled
각 의사 클래스의 발동 시점:
| 변형 | 발동 시점 |
|---|---|
hover: | 마우스 커서를 올렸을 때 |
focus: | Tab 키 또는 클릭으로 포커스를 받았을 때 |
active: | 클릭(마우스 누르는 중)하고 있을 때 |
disabled: | disabled 속성이 있을 때 |
1<!DOCTYPE html>2<html lang="ko">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>4-9. 상태 변형</title>7 <script src="https://unpkg.com/@tailwindcss/browser@4"></script>8</head>9<body class="bg-gray-50 p-8 space-y-12">10
11
12 <section>13 <h2 class="text-lg font-bold mb-4">hover / focus / active / disabled</h2>14 <div class="flex gap-4">15 <button class="16 bg-purple-60017 hover:bg-purple-70018 active:bg-purple-80019 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-220 disabled:opacity-40 disabled:cursor-not-allowed21 text-white font-semibold px-4 py-2 rounded-lg22 transition-all duration-150">23 활성 버튼24 </button>25
26 <button disabled class="27 bg-purple-60028 disabled:opacity-40 disabled:cursor-not-allowed29 text-white font-semibold px-4 py-2 rounded-lg">30 비활성 버튼31 </button>32 </div>33 </section>34
35
36 <section>37 <h2 class="text-lg font-bold mb-4">group-hover — 부모 hover 시 자식 변경</h2>38
39 <div class="group flex items-center gap-4 p-4 rounded-xl hover:bg-gray-100 cursor-pointer transition-colors max-w-sm border border-gray-200">40 <img src="https://picsum.photos/seed/avatar2/100/100" alt=""41 class="size-12 rounded-full" />42 <div class="flex-1 min-w-0">43 <p class="font-medium text-gray-900 group-hover:text-blue-600 transition-colors">사용자 이름</p>44
45 <p class="text-sm text-gray-500 truncate">user@example.com</p>46 </div>47
48 <svg class="size-5 text-gray-300 group-hover:text-blue-50049 opacity-0 group-hover:opacity-10050 transition-all duration-20051 -translate-x-1 group-hover:translate-x-0"52 fill="none" stroke="currentColor" viewBox="0 0 24 24">53 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>54 </svg>55 </div>56 </section>57
58
59 <section>60 <h2 class="text-lg font-bold mb-4">peer — 형제 요소 상태 연동 (플로팅 라벨)</h2>61
62 <div class="relative max-w-xs">63 <input id="floating-input" type="text" placeholder=" "64 class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm65 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />66
67 <label for="floating-input"68 class="absolute left-3 top-1.5 text-xs text-gray-40069 peer-placeholder-shown:top-3.5 peer-placeholder-shown:text-sm70 peer-focus:top-1.5 peer-focus:text-xs peer-focus:text-blue-60071 transition-all duration-150 pointer-events-none">72 플로팅 라벨73 </label>74 </div>75 </section>76
77</body>78</html>