Type something to search...

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-800
15
text-white font-semibold
16
px-4 py-2 rounded-lg
17
transition-colors duration-150
18
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
19
disabled:opacity-50 disabled:cursor-not-allowed">
20
기본 버튼
21
</button>
22
<!-- disabled 상태 확인용 -->
23
<button disabled class="bg-blue-600 text-white font-semibold
24
px-4 py-2 rounded-lg ml-2
25
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-600
34
hover:bg-blue-50 active:bg-blue-100
35
font-semibold px-4 py-2 rounded-lg
36
transition-colors duration-150
37
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-200
46
font-medium px-4 py-2 rounded-lg
47
transition-colors duration-150
48
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-2
57
bg-green-600 hover:bg-green-700
58
text-white font-semibold px-4 py-2 rounded-lg
59
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-medium
37
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-xl
17
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-sm
27
placeholder:text-gray-400
28
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
29
disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed
30
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-sm
38
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-500
46
정상 상태와 색상 계열만 바꾸면 된다 (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-sm
50
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-none
64
field-sizing-content min-h-24
65
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-sm
78
focus:outline-none focus:ring-2 focus:ring-blue-500
79
appearance-none bg-no-repeat bg-right"
80
style="background-image: url(&quot;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&quot;); 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-semibold
91
py-2.5 rounded-lg transition-colors duration-150
92
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-600
17
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-full
53
transition-colors duration-200
54
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 shadow
58
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
MyApp
22
</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-semibold
36
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-semibold
14
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-100
45
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-100
64
rounded-lg transition-colors">
65
취소
66
</button>
67
<button class="px-4 py-2 text-sm font-semibold bg-blue-600 hover:bg-blue-700
68
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-2
19
bg-white border border-gray-300 rounded-lg text-sm font-medium
20
text-gray-700 hover:bg-gray-50 transition-colors
21
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-lg
35
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-800
15
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-800
25
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-800
34
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-800
44
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-600
17
hover:bg-purple-700
18
active:bg-purple-800
19
focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2
20
disabled:opacity-40 disabled:cursor-not-allowed
21
text-white font-semibold px-4 py-2 rounded-lg
22
transition-all duration-150">
23
활성 버튼
24
</button>
25
26
<button disabled class="
27
bg-purple-600
28
disabled:opacity-40 disabled:cursor-not-allowed
29
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-500
52
opacity-0 group-hover:opacity-100
53
transition-all duration-200
54
-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-sm
73
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-400
80
peer-placeholder-shown:top-3.5 peer-placeholder-shown:text-sm
81
peer-focus:top-1.5 peer-focus:text-xs peer-focus:text-blue-600
82
transition-all duration-150 pointer-events-none">
83
플로팅 라벨
84
</label>
85
</div>
86
</section>
87
88
</body>
89
</html>