Motile UI

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의 상태를 관리하는 최상위 컨텍스트 프로바이더

속성타입기본값설명
valuestring | number-선택된 값 (Controlled)
defaultValuestring | number-초기 선택 값 (Uncontrolled)
onValueChange(value: string | number) => void-값 변경 콜백
openboolean-열림/닫힘 상태 (Controlled)
onOpenChange(open: boolean) => void-열림/닫힘 상태 변경 콜백
disabledbooleanfalse비활성화 여부
zIndexnumber40z-index 값
hideCheckIconbooleanfalse체크 아이콘 숨김 여부
maxWidthstring | number768모바일/데스크톱 전환 breakpoint (px 이하: Drawer, 초과: Dropdown)
closeOnBackdropboolean | { escapeKey?: boolean, clickOutside?: boolean }true외부 클릭/ESC 키로 닫기 제어 (true, false, 또는 객체로 세밀하게 제어 가능)
colorstring-커스텀 색상 (Trigger, Content, Item 전체에 적용)
children*React.ReactNode-자식 컴포넌트

Select.Trigger

Select를 여는 트리거 버튼

속성타입기본값설명
asChildbooleanfalse자식 요소를 렌더링할지 여부 (Slot 패턴)
classNamestring-커스텀 CSS 클래스
children*React.ReactNode-자식 컴포넌트

기본 <code>button</code> HTML 속성을 모두 사용할 수 있습니다.

Select.Value

선택된 값을 표시하는 컴포넌트

속성타입기본값설명
placeholderstring"선택하세요"선택되지 않았을 때 표시되는 텍스트
classNamestring-커스텀 CSS 클래스

Select.Content

옵션 목록을 담는 드롭다운 컨테이너

속성타입기본값설명
classNamestring-커스텀 CSS 클래스
children*React.ReactNode-자식 컴포넌트

기본 <code>div</code> HTML 속성을 모두 사용할 수 있습니다.

Select.Item

선택 가능한 개별 옵션 아이템

속성타입기본값설명
valuestring | number-옵션 값 (선택적 - value 없이 onClick으로 완전히 제어 가능)
selectedboolean-선택 상태 (value 비교보다 우선, 객체 데이터 사용 시 유용)
disabledbooleanfalse옵션 비활성화 여부
classNamestring-커스텀 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}
Select | Motile UI