Select
선택 옵션을 위한 Select 컴포넌트입니다. Root, Trigger, Value, Content, Item으로 구성된 Compound 패턴으로 유연한 커스터마이징이 가능합니다. 반응형 디자인으로 모바일에서는 Drawer, 데스크톱에서는 Dropdown으로 자동 전환됩니다. ARIA 속성을 통한 접근성 지원, ESC 키 및 외부 클릭으로 닫기 제어, asChild 패턴을 통한 완전한 커스터마이징, 다크 모드 및 Safe Area 대응을 제공합니다.
미리보기
사용법
TSX
1"use client";
2
3import { Select } from "motile-ui";
4
5export default function PreviewExample() {
6 return (
7 <Select.Root zIndex={40}>
8 <Select.Trigger>
9 <Select.Value placeholder="과일을 선택하세요" />
10 </Select.Trigger>
11 <Select.Content>
12 <Select.Item value="apple">사과</Select.Item>
13 <Select.Item value="banana">바나나</Select.Item>
14 <Select.Item value="orange">오렌지</Select.Item>
15 <Select.Item value="grape">포도</Select.Item>
16 </Select.Content>
17 </Select.Root>
18 );
19}API 레퍼런스
Select.Root
Select의 상태를 관리하는 최상위 컨텍스트 프로바이더
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | string | number | - | 선택된 값 (Controlled) |
defaultValue | string | number | - | 초기 선택 값 (Uncontrolled) |
onValueChange | (value: string | number) => void | - | 값 변경 콜백 |
open | boolean | - | 열림/닫힘 상태 (Controlled) |
onOpenChange | (open: boolean) => void | - | 열림/닫힘 상태 변경 콜백 |
disabled | boolean | false | 비활성화 여부 |
zIndex | number | 40 | z-index 값 |
hideCheckIcon | boolean | false | 체크 아이콘 숨김 여부 |
maxWidth | string | number | 768 | 모바일/데스크톱 전환 breakpoint (px 이하: Drawer, 초과: Dropdown) |
closeOnBackdrop | boolean | { escapeKey?: boolean, clickOutside?: boolean } | true | 외부 클릭/ESC 키로 닫기 제어 (true, false, 또는 객체로 세밀하게 제어 가능) |
color | string | - | 커스텀 색상 (Trigger, Content, Item 전체에 적용) |
children* | React.ReactNode | - | 자식 컴포넌트 |
Select.Trigger
Select를 여는 트리거 버튼
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
asChild | boolean | false | 자식 요소를 렌더링할지 여부 (Slot 패턴) |
className | string | - | 커스텀 CSS 클래스 |
children* | React.ReactNode | - | 자식 컴포넌트 |
기본 <code>button</code> HTML 속성을 모두 사용할 수 있습니다.
Select.Value
선택된 값을 표시하는 컴포넌트
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
placeholder | string | "선택하세요" | 선택되지 않았을 때 표시되는 텍스트 |
className | string | - | 커스텀 CSS 클래스 |
Select.Content
옵션 목록을 담는 드롭다운 컨테이너
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
className | string | - | 커스텀 CSS 클래스 |
children* | React.ReactNode | - | 자식 컴포넌트 |
기본 <code>div</code> HTML 속성을 모두 사용할 수 있습니다.
Select.Item
선택 가능한 개별 옵션 아이템
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | string | number | - | 옵션 값 (선택적 - value 없이 onClick으로 완전히 제어 가능) |
selected | boolean | - | 선택 상태 (value 비교보다 우선, 객체 데이터 사용 시 유용) |
disabled | boolean | false | 옵션 비활성화 여부 |
className | string | - | 커스텀 CSS 클래스 |
children* | React.ReactNode | - | 자식 컴포넌트 |
기본 <code>div</code> HTML 속성을 모두 사용할 수 있습니다.
예제
기본값 설정
TSX
1"use client";
2
3import { Select } from "motile-ui";
4
5export default function WithDefaultValueExample() {
6 return (
7 <Select.Root defaultValue="banana">
8 <Select.Trigger>
9 <Select.Value placeholder="과일을 선택하세요" />
10 </Select.Trigger>
11 <Select.Content>
12 <Select.Item value="apple">사과</Select.Item>
13 <Select.Item value="banana">바나나</Select.Item>
14 <Select.Item value="orange">오렌지</Select.Item>
15 </Select.Content>
16 </Select.Root>
17 );
18}Controlled 컴포넌트
선택된 값: apple상태: 닫힘
TSX
1"use client";
2
3import { Button, Select } from "motile-ui";
4import { useState } from "react";
5
6export default function ControlledExample() {
7 const [open, setOpen] = useState(false);
8 const [value, setValue] = useState<string | undefined>("apple");
9
10 const handleMouseDown = (e: React.MouseEvent) => {
11 // useClickOutside의 mousedown 이벤트 전파 차단
12 e.stopPropagation();
13 };
14
15 const handleValueChange = (newValue: string | number) => {
16 setValue(String(newValue));
17 };
18
19 return (
20 <div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
21 <div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
22 <Button
23 onClick={() => setOpen(!open)}
24 onMouseDownCapture={handleMouseDown}
25 size="small"
26 >
27 {open ? "닫기" : "열기"}
28 </Button>
29 <Button onClick={() => setValue("banana")} size="small">
30 바나나로 변경
31 </Button>
32 <Button onClick={() => setValue(undefined)} size="small">
33 초기화
34 </Button>
35 </div>
36
37 <Select.Root
38 open={open}
39 onOpenChange={setOpen}
40 value={value}
41 onValueChange={handleValueChange}
42 >
43 <Select.Trigger>
44 <Select.Value placeholder="과일을 선택하세요" />
45 </Select.Trigger>
46 <Select.Content>
47 <Select.Item value="apple">사과</Select.Item>
48 <Select.Item value="banana">바나나</Select.Item>
49 <Select.Item value="orange">오렌지</Select.Item>
50 <Select.Item value="grape">포도</Select.Item>
51 </Select.Content>
52 </Select.Root>
53
54 <div style={{ fontSize: "14px", color: "#6b7280" }}>
55 선택된 값: {value || "(없음)"}
56 <span style={{ marginLeft: "16px" }}>
57 상태: {open ? "열림" : "닫힘"}
58 </span>
59 </div>
60 </div>
61 );
62}비활성화
TSX
1"use client";
2
3import { Select } from "motile-ui";
4
5export default function DisabledExample() {
6 return (
7 <Select.Root disabled>
8 <Select.Trigger>
9 <Select.Value placeholder="선택 불가" />
10 </Select.Trigger>
11 <Select.Content>
12 <Select.Item value="apple">사과</Select.Item>
13 <Select.Item value="banana">바나나</Select.Item>
14 </Select.Content>
15 </Select.Root>
16 );
17}일부 옵션 비활성화
TSX
1"use client";
2
3import { Select } from "motile-ui";
4
5export default function DisabledItemsExample() {
6 return (
7 <Select.Root>
8 <Select.Trigger>
9 <Select.Value placeholder="과일을 선택하세요" />
10 </Select.Trigger>
11 <Select.Content>
12 <Select.Item value="apple">사과</Select.Item>
13 <Select.Item value="banana" disabled>
14 바나나 (품절)
15 </Select.Item>
16 <Select.Item value="orange">오렌지</Select.Item>
17 <Select.Item value="grape" disabled>
18 포도 (품절)
19 </Select.Item>
20 <Select.Item value="melon">멜론</Select.Item>
21 </Select.Content>
22 </Select.Root>
23 );
24}onClick 이벤트
TSX
1"use client";
2
3import { Select } from "motile-ui";
4
5export default function OnClickExample() {
6 return (
7 <Select.Root>
8 <Select.Trigger>
9 <Select.Value placeholder="과일을 선택하세요" />
10 </Select.Trigger>
11 <Select.Content>
12 <Select.Item
13 value="apple"
14 onClick={() => alert("사과를 클릭했습니다!")}
15 >
16 사과
17 </Select.Item>
18 <Select.Item
19 value="banana"
20 onClick={() => alert("바나나를 클릭했습니다!")}
21 >
22 바나나
23 </Select.Item>
24 <Select.Item
25 value="orange"
26 onClick={() => alert("오렌지를 클릭했습니다!")}
27 >
28 오렌지
29 </Select.Item>
30 </Select.Content>
31 </Select.Root>
32 );
33}객체 데이터 사용
TSX
1"use client";
2
3import { Select } from "motile-ui";
4import { useState } from "react";
5
6interface TeamMember {
7 id: number;
8 name: string;
9 role: string;
10 email: string;
11 avatar: string;
12 color: string;
13}
14
15export default function ProductionExample() {
16 const [selectedMember, setSelectedMember] = useState<TeamMember | null>(null);
17
18 const teamMembers: TeamMember[] = [
19 {
20 id: 1,
21 name: "John Smith",
22 role: "Lead Designer",
23 email: "john@example.com",
24 avatar: "👨💼",
25 color: "#3b82f6",
26 },
27 {
28 id: 2,
29 name: "Sarah Johnson",
30 role: "Frontend Developer",
31 email: "sarah@example.com",
32 avatar: "👩💻",
33 color: "#10b981",
34 },
35 {
36 id: 3,
37 name: "Mike Chen",
38 role: "Product Manager",
39 email: "mike@example.com",
40 avatar: "👨💼",
41 color: "#8b5cf6",
42 },
43 {
44 id: 4,
45 name: "Emma Wilson",
46 role: "UX Researcher",
47 email: "emma@example.com",
48 avatar: "👩🎨",
49 color: "#f59e0b",
50 },
51 {
52 id: 5,
53 name: "Alex Rodriguez",
54 role: "Backend Engineer",
55 email: "alex@example.com",
56 avatar: "👨🔧",
57 color: "#ef4444",
58 },
59 ];
60
61 return (
62 <Select.Root>
63 <Select.Trigger
64 style={{
65 padding: "12px 16px",
66 border: "1px solid #e5e7eb",
67 borderRadius: "12px",
68 backgroundColor: "#ffffff",
69 minWidth: "280px",
70 }}
71 >
72 <div
73 style={{
74 display: "flex",
75 alignItems: "center",
76 gap: "12px",
77 minHeight: "32px",
78 }}
79 >
80 {selectedMember ? (
81 <>
82 <span
83 style={{
84 display: "flex",
85 alignItems: "center",
86 justifyContent: "center",
87 width: "32px",
88 height: "32px",
89 borderRadius: "8px",
90 backgroundColor: `${selectedMember.color}15`,
91 fontSize: "18px",
92 }}
93 >
94 {selectedMember.avatar}
95 </span>
96 <div
97 style={{ display: "flex", flexDirection: "column", gap: "2px" }}
98 >
99 <span
100 style={{
101 fontSize: "14px",
102 fontWeight: "500",
103 color: "#111827",
104 }}
105 >
106 {selectedMember.name}
107 </span>
108 <span style={{ fontSize: "12px", color: selectedMember.color }}>
109 {selectedMember.role}
110 </span>
111 </div>
112 </>
113 ) : (
114 <span style={{ color: "#9ca3af" }}>팀원을 선택하세요</span>
115 )}
116 </div>
117 </Select.Trigger>
118 <Select.Content>
119 {teamMembers.map((member) => (
120 <Select.Item
121 key={member.id}
122 selected={selectedMember?.id === member.id}
123 onClick={() => setSelectedMember(member)}
124 >
125 <div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
126 <span
127 style={{
128 display: "flex",
129 alignItems: "center",
130 justifyContent: "center",
131 width: "32px",
132 height: "32px",
133 borderRadius: "8px",
134 backgroundColor: `${member.color}15`,
135 fontSize: "18px",
136 }}
137 >
138 {member.avatar}
139 </span>
140 <div
141 style={{
142 display: "flex",
143 flexDirection: "column",
144 gap: "2px",
145 flex: 1,
146 }}
147 >
148 <span style={{ fontSize: "14px", fontWeight: "500" }}>
149 {member.name}
150 </span>
151 <span style={{ fontSize: "12px", color: member.color }}>
152 {member.role}
153 </span>
154 </div>
155 </div>
156 </Select.Item>
157 ))}
158 </Select.Content>
159 </Select.Root>
160 );
161}