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 btn + btn-primary 처럼 조합해서 사용한다.61 btn은 공통 레이아웃, btn-primary는 색상 담당이다.62 -->63 <button class="btn btn-primary">저장</button>64 <button class="btn btn-secondary">취소</button>65 <button class="btn btn-danger">삭제</button>66 </div>67
68 <h2 class="text-lg font-bold mb-4">@utility 카드</h2>69 <!--70 card 클래스로 기본 카드 스타일을 적용하고,71 p-6 같은 유틸리티를 추가로 조합할 수 있다.72 -->73 <div class="card p-6 max-w-sm">74 <h3 class="font-bold text-gray-900">카드 제목</h3>75 <p class="mt-2 text-sm text-gray-600">카드 내용이 여기에 들어간다.</p>76 </div>77
78</body>79</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 <!-- disabled 상태 확인용 -->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 <!-- SVG 아이콘: + 모양 -->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 <!-- 소형: px-2.5 py-1.5 text-xs rounded-md -->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 <!-- 중형(기본): px-4 py-2 text-sm rounded-lg -->18 <button class="px-4 py-2 text-sm font-semibold bg-blue-600 text-white rounded-lg">19 중형20 </button>21
22 <!-- 대형: px-6 py-3 text-base rounded-xl -->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 <!-- 전체 너비 버튼: w-full -->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 picsum.photos: 무료 플레이스홀더 이미지 서비스15 object-cover: 이미지가 지정 영역을 꽉 채우되, 비율을 유지하고 넘치는 부분을 잘라냄16 -->17 <img src="https://picsum.photos/seed/card1/400/300" alt="상품 이미지"18 class="w-full h-48 object-cover" />19 <div class="p-5">20 <span class="text-xs font-medium text-blue-600 bg-blue-50 px-2 py-1 rounded-full">21 카테고리22 </span>23 <!-- line-clamp-2: 2줄 초과 시 말줄임(...) 처리 -->24 <h3 class="mt-2 text-lg font-bold text-gray-900 line-clamp-2">25 상품 또는 콘텐츠 제목이 여기에 들어간다26 </h3>27 <!-- line-clamp-3: 3줄 초과 시 말줄임 처리 -->28 <p class="mt-1 text-sm text-gray-500 line-clamp-3">29 설명 텍스트가 여기에 들어간다. 3줄이 넘으면 말줄임 처리된다.30 긴 텍스트를 넣어서 직접 확인해 보자.31 이렇게 세 줄을 넘기면 자동으로 잘린다.32 이 줄은 보이지 않을 것이다.33 </p>34 <div class="mt-4 flex items-center justify-between">35 <span class="text-xl font-bold text-gray-900">₩29,000</span>36 <button class="bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium37 px-3 py-1.5 rounded-lg transition-colors">38 구매39 </button>40 </div>41 </div>42 </div>43 </div>44
45</body>46</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 group: 이 요소가 hover 되면 내부의 group-hover:* 가 활성화된다.14 hover:shadow-xl: 마우스 올리면 그림자 커짐15 -->16 <div class="group bg-white rounded-2xl shadow hover:shadow-xl17 transition-all duration-300 overflow-hidden cursor-pointer">18 <div class="overflow-hidden h-48">19 <!--20 group-hover:scale-105: 부모 hover 시 이미지가 5% 확대됨21 overflow-hidden이 부모에 있으므로 확대된 이미지가 밖으로 넘치지 않는다22 -->23 <img src="https://picsum.photos/seed/hover1/400/300" alt=""24 class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />25 </div>26 <div class="p-5">27 <!-- group-hover:text-blue-600: 부모 hover 시 제목 색상 변경 -->28 <h3 class="font-bold text-gray-900 group-hover:text-blue-600 transition-colors">29 호버해 보세요30 </h3>31 <p class="mt-1 text-sm text-gray-500">이미지 확대 + 제목 색상 변경</p>32 </div>33 </div>34 </div>35
36</body>37</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 flex: 가로 배치14 flex-shrink-0: 이미지 영역이 줄어들지 않도록 고정15 -->16 <div class="flex bg-white rounded-2xl shadow-md overflow-hidden">17 <img src="https://picsum.photos/seed/hcard/200/200" alt=""18 class="w-32 sm:w-48 flex-shrink-0 object-cover" />19 <div class="flex flex-col justify-between p-4">20 <div>21 <h3 class="font-bold text-gray-900">가로형 카드 제목</h3>22 <p class="mt-1 text-sm text-gray-500 line-clamp-2">23 가로형 카드는 목록형 UI에 적합하다. 썸네일을 왼쪽에 고정하고 텍스트를 오른쪽에 배치한다.24 </p>25 </div>26 <div class="flex items-center gap-2 mt-3">27 <img src="https://picsum.photos/seed/avatar/100/100" alt=""28 class="size-6 rounded-full" />29 <span class="text-xs text-gray-500">작성자 · 2일 전</span>30 </div>31 </div>32 </div>33 </div>34
35</body>36</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 space-y-4: 자식 요소 사이에 1rem 세로 간격13 max-w-md: 최대 너비 28rem (폼이 너무 넓어지지 않게)14 -->15 <form class="space-y-4 max-w-md" onsubmit="event.preventDefault(); alert('제출됨!')">16
17 <!-- 텍스트 입력 -->18 <div>19 <label for="name" class="block text-sm font-medium text-gray-700 mb-1">이름</label>20 <!--21 focus:ring-2 focus:ring-blue-500: 포커스 시 파란색 테두리 링22 focus:border-transparent: 기본 border를 숨기고 ring만 보이게23 placeholder:text-gray-400: 플레이스홀더 색상 지정24 -->25 <input type="text" id="name" placeholder="홍길동"26 class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm27 placeholder:text-gray-40028 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent29 disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed30 transition-shadow duration-150" />31 </div>32
33 <!-- 이메일 입력 -->34 <div>35 <label for="email" class="block text-sm font-medium text-gray-700 mb-1">이메일</label>36 <input type="email" id="email" placeholder="example@email.com"37 class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm38 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />39 </div>40
41 <!-- 에러 상태 입력 -->42 <div>43 <label for="pw" class="block text-sm font-medium text-gray-700 mb-1">비밀번호</label>44 <!--45 에러 상태: border-red-400 + bg-red-50 + ring-red-50046 정상 상태와 색상 계열만 바꾸면 된다 (blue → red)47 -->48 <input type="password" id="pw"49 class="w-full rounded-lg border border-red-400 bg-red-50 px-3 py-2 text-sm50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent" />51 <p class="mt-1 text-xs text-red-600">비밀번호를 입력해 주세요.</p>52 </div>53
54 <!-- textarea — v4 field-sizing-content -->55 <div>56 <label for="msg" class="block text-sm font-medium text-gray-700 mb-1">메시지</label>57 <!--58 field-sizing-content: 내용이 늘어나면 textarea 높이도 자동 증가 (v4 신규)59 min-h-24: 미지원 브라우저를 위한 최소 높이 폴백60 resize-none: 수동 리사이즈 핸들 제거 (자동 크기조절과 충돌 방지)61 -->62 <textarea id="msg" rows="4"63 class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm resize-none64 field-sizing-content min-h-2465 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"66 placeholder="내용을 입력하세요 — 줄이 늘어나면 높이가 자동으로 커진다"></textarea>67 </div>68
69 <!-- 셀렉트 박스 -->70 <div>71 <label for="role" class="block text-sm font-medium text-gray-700 mb-1">역할</label>72 <!--73 appearance-none: 브라우저 기본 화살표 제거74 인라인 SVG로 커스텀 화살표를 bg-image로 넣는다75 -->76 <select id="role"77 class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm78 focus:outline-none focus:ring-2 focus:ring-blue-50079 appearance-none bg-no-repeat bg-right"80 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;">81 <option value="">선택하세요</option>82 <option value="admin">관리자</option>83 <option value="user">일반 사용자</option>84 <option value="viewer">뷰어</option>85 </select>86 </div>87
88 <!-- 제출 버튼 -->89 <button type="submit"90 class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold91 py-2.5 rounded-lg transition-colors duration-15092 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">93 제출94 </button>95 </form>96
97</body>98</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 <!-- group-hover: 라벨 위에 마우스를 올려도 텍스트 색상이 변한다 -->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 원리:43 1. sr-only 로 실제 checkbox를 시각적으로 숨긴다44 2. peer 클래스로 checkbox의 상태를 형제 요소에 전달한다45 3. peer-checked: 로 체크 시 배경색과 동그라미 위치를 변경한다46 -->47 <label class="inline-flex items-center cursor-pointer gap-3">48 <span class="text-sm font-medium text-gray-700">알림 수신</span>49 <div class="relative">50 <input type="checkbox" class="sr-only peer" />51 <!-- 트랙(배경) -->52 <div class="w-11 h-6 bg-gray-200 peer-checked:bg-blue-600 rounded-full53 transition-colors duration-20054 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 peer-focus:ring-offset-2">55 </div>56 <!-- 동그라미(핸들) — peer-checked 시 오른쪽으로 이동 -->57 <div class="absolute top-0.5 left-0.5 size-5 bg-white rounded-full shadow58 transition-transform duration-200 peer-checked:translate-x-5">59 </div>60 </div>61 </label>62 </div>63
64</body>65</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 sticky top-0 z-50: 스크롤해도 상단에 고정13 반응형: md:flex / md:hidden 으로 데스크톱/모바일 분기14 -->15 <nav class="bg-white border-b border-gray-200 sticky top-0 z-50">16 <div class="max-w-6xl mx-auto px-4 sm:px-6">17 <div class="flex items-center justify-between h-16">18 <!-- 로고 -->19 <a href="#" class="flex items-center gap-2 font-bold text-lg text-gray-900">20 <div class="size-8 bg-blue-600 rounded-lg"></div>21 MyApp22 </a>23
24 <!-- 데스크톱 메뉴 (768px 이상) -->25 <ul class="hidden md:flex items-center gap-1">26 <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>27 <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>28 <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>29 <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>30 </ul>31
32 <!-- CTA 버튼 (데스크톱) -->33 <div class="hidden md:flex items-center gap-2">34 <a href="#" class="text-sm font-medium text-gray-600 hover:text-gray-900 px-3 py-2">로그인</a>35 <a href="#" class="bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold36 px-4 py-2 rounded-lg transition-colors">시작하기</a>37 </div>38
39 <!-- 모바일 햄버거 버튼 (768px 미만) -->40 <button id="menu-btn" class="md:hidden p-2 rounded-lg text-gray-600 hover:bg-gray-100">41 <svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">42 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>43 </svg>44 </button>45 </div>46 </div>47
48 <!-- 모바일 메뉴 (기본 숨김, JS로 토글) -->49 <div id="mobile-menu" class="hidden md:hidden border-t border-gray-200 px-4 py-3 space-y-1">50 <a href="#" class="block px-3 py-2 rounded-lg text-sm font-medium text-gray-900 hover:bg-gray-100">홈</a>51 <a href="#" class="block px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-100">소개</a>52 <a href="#" class="block px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-100">서비스</a>53 <a href="#" class="block px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-100">문의</a>54 </div>55 </nav>56
57 <!-- 스크롤 확인용 더미 콘텐츠 -->58 <div class="max-w-6xl mx-auto p-8 space-y-4">59 <p class="text-gray-500">↓ 스크롤해서 sticky 네비게이션을 확인해 보자.</p>60 <div class="h-[200vh]"></div>61 </div>62
63 <script>64 // 모바일 메뉴 토글65 const menuBtn = document.getElementById('menu-btn')66 const mobileMenu = document.getElementById('mobile-menu')67
68 menuBtn.addEventListener('click', () => {69 mobileMenu.classList.toggle('hidden')70 })71 </script>72
73</body>74</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 모달 구조:20 ┌─ #modal (fixed inset-0) ─────────────────────┐21 │ ┌─ 배경 오버레이 (bg-black/60) ───────────┐ │22 │ └─────────────────────────────────────────┘ │23 │ ┌─ 모달 패널 (relative, bg-white) ────────┐ │24 │ │ 헤더 / 본문 / 푸터 │ │25 │ └─────────────────────────────────────────┘ │26 └───────────────────────────────────────────────┘27
28 fixed inset-0: 뷰포트 전체를 덮는다29 flex items-center justify-center: 모달을 정중앙에 배치30 -->31 <div id="modal"32 class="hidden fixed inset-0 z-50 flex items-center justify-center p-4"33 role="dialog" aria-modal="true" aria-labelledby="modal-title">34
35 <!-- 배경 오버레이 — 클릭하면 모달 닫힘 -->36 <div class="absolute inset-0 bg-black/60" id="modal-backdrop"></div>37
38 <!-- 모달 패널 — relative로 오버레이 위에 표시 -->39 <div class="relative bg-white rounded-2xl shadow-2xl w-full max-w-md">40 <!-- 헤더 -->41 <div class="flex items-center justify-between px-6 py-4 border-b border-gray-200">42 <h2 id="modal-title" class="text-lg font-bold text-gray-900">모달 제목</h2>43 <button id="close-modal"44 class="p-1 rounded-lg text-gray-400 hover:text-gray-600 hover:bg-gray-10045 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400">46 <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">47 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>48 </svg>49 <span class="sr-only">닫기</span>50 </button>51 </div>52
53 <!-- 본문 -->54 <div class="px-6 py-4">55 <p class="text-sm text-gray-600">56 모달 내용이 여기에 들어간다. 사용자에게 중요한 정보를 표시하거나 확인을 요청할 때 사용한다.57 </p>58 </div>59
60 <!-- 푸터 -->61 <div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200">62 <button id="cancel-modal"63 class="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-10064 rounded-lg transition-colors">65 취소66 </button>67 <button class="px-4 py-2 text-sm font-semibold bg-blue-600 hover:bg-blue-70068 text-white rounded-lg transition-colors">69 확인70 </button>71 </div>72 </div>73 </div>74
75 <script>76 const modal = document.getElementById('modal')77 const openBtn = document.getElementById('open-modal')78 const closeBtn = document.getElementById('close-modal')79 const cancelBtn = document.getElementById('cancel-modal')80 const backdrop = document.getElementById('modal-backdrop')81
82 const openModal = () => modal.classList.remove('hidden')83 const closeModal = () => modal.classList.add('hidden')84
85 openBtn.addEventListener('click', openModal)86 closeBtn.addEventListener('click', closeModal)87 cancelBtn.addEventListener('click', closeModal)88 backdrop.addEventListener('click', closeModal)89
90 // ESC 키로도 닫기91 document.addEventListener('keydown', (e) => {92 if (e.key === 'Escape') closeModal()93 })94 </script>95
96</body>97</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 relative: 자식의 absolute 기준점13 inline-block: 버튼 크기만큼만 차지14 -->15 <div class="relative inline-block" id="dropdown-wrapper">16 <!-- 트리거 버튼 -->17 <button id="dropdown-btn"18 class="inline-flex items-center gap-2 px-4 py-219 bg-white border border-gray-300 rounded-lg text-sm font-medium20 text-gray-700 hover:bg-gray-50 transition-colors21 focus:outline-none focus:ring-2 focus:ring-blue-500">22 메뉴23 <svg class="size-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">24 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>25 </svg>26 </button>27
28 <!--29 드롭다운 패널30 absolute right-0 mt-2: 버튼 아래 오른쪽 정렬31 z-10: 다른 요소 위에 표시32 -->33 <div id="dropdown-menu"34 class="hidden absolute right-0 mt-2 w-48 bg-white rounded-xl shadow-lg35 border border-gray-100 py-1 z-10"36 role="menu">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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>41 </svg>42 프로필43 </a>44 <a href="#" class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50" role="menuitem">45 <svg class="size-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">46 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"47 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"/>48 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>49 </svg>50 설정51 </a>52 <hr class="my-1 border-gray-100" />53 <a href="#" class="flex items-center gap-2 px-4 py-2 text-sm text-red-600 hover:bg-red-50" role="menuitem">54 로그아웃55 </a>56 </div>57 </div>58
59 <script>60 const btn = document.getElementById('dropdown-btn')61 const menu = document.getElementById('dropdown-menu')62
63 // 버튼 클릭 → 토글 (e.stopPropagation으로 document 클릭과 분리)64 btn.addEventListener('click', (e) => {65 e.stopPropagation()66 menu.classList.toggle('hidden')67 })68
69 // 문서 아무 곳 클릭 → 닫기70 document.addEventListener('click', () => menu.classList.add('hidden'))71 </script>72
73</body>74</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 <!-- 정보 (blue 계열) -->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 <!-- 성공 (green 계열) -->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 <!-- 경고 (yellow 계열) -->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 <!-- 오류 (red 계열) -->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 ┌──────┬──────────────┬────────────────┬───────────────┐57 │ 상태 │ 배경 │ 테두리 │ 텍스트 │58 ├──────┼──────────────┼────────────────┼───────────────┤59 │ 정보 │ bg-blue-50 │ border-blue-200 │ text-blue-800 │60 │ 성공 │ bg-green-50 │ border-green-200 │ text-green-800 │61 │ 경고 │ bg-yellow-50 │ border-yellow-200│ text-yellow-800│62 │ 오류 │ bg-red-50 │ border-red-200 │ text-red-800 │63 └──────┴──────────────┴────────────────┴───────────────┘64 -->65
66</body>67</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 <!-- ── hover / focus / active / disabled ── -->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 <!-- ── group-hover: 부모 hover → 자식 변경 ── -->36 <section>37 <h2 class="text-lg font-bold mb-4">group-hover — 부모 hover 시 자식 변경</h2>38 <!--39 group: "나를 hover하면 내 자식 중 group-hover:가 붙은 것들이 반응해라"40 실전에서 카드, 리스트 아이템에 자주 쓴다41 -->42 <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">43 <img src="https://picsum.photos/seed/avatar2/100/100" alt=""44 class="size-12 rounded-full" />45 <div class="flex-1 min-w-0">46 <p class="font-medium text-gray-900 group-hover:text-blue-600 transition-colors">사용자 이름</p>47 <!-- truncate: 한 줄 넘치면 말줄임 -->48 <p class="text-sm text-gray-500 truncate">user@example.com</p>49 </div>50 <!-- hover 시에만 나타나는 화살표 -->51 <svg class="size-5 text-gray-300 group-hover:text-blue-50052 opacity-0 group-hover:opacity-10053 transition-all duration-20054 -translate-x-1 group-hover:translate-x-0"55 fill="none" stroke="currentColor" viewBox="0 0 24 24">56 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>57 </svg>58 </div>59 </section>60
61 <!-- ── peer: 형제 요소 상태 연동 ── -->62 <section>63 <h2 class="text-lg font-bold mb-4">peer — 형제 요소 상태 연동 (플로팅 라벨)</h2>64 <!--65 peer 원리:66 1. input에 peer 클래스를 붙인다67 2. 형제(sibling) 요소에서 peer-*: 로 input의 상태를 참조한다68 ⚠️ peer 요소가 HTML 순서상 먼저 와야 한다 (CSS ~ 선택자 특성)69 -->70 <div class="relative max-w-xs">71 <input id="floating-input" type="text" placeholder=" "72 class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm73 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />74 <!--75 peer-placeholder-shown: placeholder가 보일 때 (= 입력값 없을 때) → 라벨이 아래로 내려감76 peer-focus: 포커스 시 → 라벨이 위로 올라가며 작아짐 + 색상 변경77 -->78 <label for="floating-input"79 class="absolute left-3 top-1.5 text-xs text-gray-40080 peer-placeholder-shown:top-3.5 peer-placeholder-shown:text-sm81 peer-focus:top-1.5 peer-focus:text-xs peer-focus:text-blue-60082 transition-all duration-150 pointer-events-none">83 플로팅 라벨84 </label>85 </div>86 </section>87
88</body>89</html>