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
<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-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
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
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-medium
34
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-xl
14
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-sm
20
placeholder:text-gray-400
21
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
22
disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed
23
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-sm
31
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-sm
40
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-none
50
field-sizing-content min-h-24
51
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-sm
61
focus:outline-none focus:ring-2 focus:ring-blue-500
62
appearance-none bg-no-repeat bg-right"
63
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;">
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-semibold
74
py-2.5 rounded-lg transition-colors duration-150
75
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-600
17
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-full
48
transition-colors duration-200
49
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 shadow
53
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
MyApp
19
</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-semibold
33
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-semibold
14
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-100
33
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-100
52
rounded-lg transition-colors">
53
취소
54
</button>
55
<button class="px-4 py-2 text-sm font-semibold bg-blue-600 hover:bg-blue-700
56
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-2
16
bg-white border border-gray-300 rounded-lg text-sm font-medium
17
text-gray-700 hover:bg-gray-50 transition-colors
18
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-lg
28
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-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
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
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
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
</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-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
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-500
49
opacity-0 group-hover:opacity-100
50
transition-all duration-200
51
-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-sm
65
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-400
69
peer-placeholder-shown:top-3.5 peer-placeholder-shown:text-sm
70
peer-focus:top-1.5 peer-focus:text-xs peer-focus:text-blue-600
71
transition-all duration-150 pointer-events-none">
72
플로팅 라벨
73
</label>
74
</div>
75
</section>
76
77
</body>
78
</html>