Select
Select
사용자가 고를 수 있는 옵션 목록을 표시하는 컴포넌트로, 버튼으로 트리거돼요.
출처: 문서
본문
네이티브 <select>와 유사하지만 완전히 접근 가능하고 스타일링 자유도가 높은 셀렉트 컴포넌트예요. 제어/비제어 방식을 모두 지원하고 2가지 위치 지정 모드(item-aligned/popper)를 제공하며, 아이템·라벨·그룹을 지원하고 포커스 관리와 키보드 내비게이션이 완전히 처리돼요.
Features
- 제어(controlled)/비제어(uncontrolled) 방식 모두 지원.
- 2가지 위치 지정 모드 제공.
- 아이템, 라벨, 아이템 그룹 지원.
- 포커스 완전 관리.
- 완전한 키보드 내비게이션.
- 커스텀 플레이스홀더 지원.
- Typeahead 지원.
- 오른쪽에서 왼쪽(RTL) 방향 지원.
Anatomy
모든 파트를 임포트해 조립해요.
import { Select } from "radix-ui";
export default () => (
<Select.Root>
<Select.Trigger>
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Item>
<Select.ItemText />
<Select.ItemIndicator />
</Select.Item>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
<Select.ItemIndicator />
</Select.Item>
</Select.Group>
<Select.Separator />
</Select.Viewport>
<Select.ScrollDownButton />
<Select.Arrow />
</Select.Content>
</Select.Portal>
</Select.Root>
);
API Reference
Root
셀렉트의 모든 파트를 담아요.
| Prop | Type | Default |
|---|---|---|
defaultValue |
string |
No default value |
value |
string |
No default value |
onValueChange |
function |
No default value |
defaultOpen |
boolean |
No default value |
open |
boolean |
No default value |
onOpenChange |
function |
No default value |
dir |
enum |
No default value |
name |
string |
No default value |
disabled |
boolean |
No default value |
required |
boolean |
No default value |
Trigger
셀렉트를 토글하는 버튼이에요. Select.Content는 트리거 위에 정렬되어 위치하게 돼요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
| Data attribute | Values |
|---|---|
[data-state] |
"open" | "closed" |
[data-disabled] |
Present when disabled |
[data-placeholder] |
Present when has placeholder |
Value
선택된 값을 반영하는 파트예요. 기본적으로 선택된 아이템의 텍스트가 렌더링돼요. 더 제어가 필요하면 셀렉트를 제어하고 자신의 children을 전달할 수도 있어요. 올바른 위치 지정을 위해 스타일링하면 안 돼요. 셀렉트에 값이 없을 때 선택적 placeholder prop도 제공돼요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
placeholder |
ReactNode |
No default value |
Icon
값 옆에 자주 표시되는 작은 아이콘으로, 열릴 수 있다는 시각적 단서를 제공해요. 기본적으로 ▼을 렌더링하지만 asChild나 children으로 자신만의 아이콘을 사용할 수 있어요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
Portal
사용하면 content 파트를 body로 포털해요.
| Prop | Type | Default |
|---|---|---|
forceMount |
boolean |
No default value |
container |
HTMLElement |
document.body |
Content
셀렉트가 열렸을 때 튀어나오는 컴포넌트예요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
forceMount |
boolean |
No default value |
onCloseAutoFocus |
function |
No default value |
onEscapeKeyDown |
function |
No default value |
onPointerDownOutside |
function |
No default value |
position |
enum |
"item-aligned" |
side |
enum |
"bottom" |
sideOffset |
number |
0 |
align |
enum |
"start" |
alignOffset |
number |
0 |
avoidCollisions |
boolean |
true |
collisionBoundary |
Boundary |
[] |
collisionPadding |
number | Padding |
10 |
arrowPadding |
number |
0 |
sticky |
enum |
"partial" |
hideWhenDetached |
boolean |
false |
| Data attribute | Values |
|---|---|
[data-state] |
"open" | "closed" |
[data-side] |
"left" | "right" | "bottom" | "top" |
[data-align] |
"start" | "end" | "center" |
| CSS Variable | Description |
|---|---|
--radix-select-content-transform-origin |
The transform-origin computed from the content and arrow positions/offsets. Only present when position="popper". |
--radix-select-content-available-width |
The remaining width between the trigger and the boundary edge. Only present when position="popper". |
--radix-select-content-available-height |
The remaining height between the trigger and the boundary edge. Only present when position="popper". |
--radix-select-trigger-width |
The width of the trigger. Only present when position="popper". |
--radix-select-trigger-height |
The height of the trigger. Only present when position="popper". |
Viewport
모든 아이템을 담는 스크롤 뷰포트예요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
Item
셀렉트 아이템을 담는 컴포넌트예요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
value* |
string |
No default value |
disabled |
boolean |
No default value |
textValue |
string |
No default value |
| Data attribute | Values |
|---|---|
[data-state] |
"checked" | "unchecked" |
[data-highlighted] |
Present when highlighted |
[data-disabled] |
Present when disabled |
ItemText
아이템의 텍스트 파트예요. 해당 아이템이 선택되었을 때 트리거에 표시하고 싶은 텍스트만 담아야 해요. 올바른 위치 지정을 위해 스타일링하면 안 돼요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
ItemIndicator
아이템이 선택되었을 때 렌더링돼요. 이 요소를 직접 스타일하거나 아이콘을 넣는 래퍼로 쓰거나 둘 다 할 수 있어요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
ScrollUpButton
뷰포트 오버플로를 보여 주는 단서이자 위로 스크롤할 수 있게 해 주는 선택적 버튼이에요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
ScrollDownButton
뷰포트 오버플로를 보여 주는 단서이자 아래로 스크롤할 수 있게 해 주는 선택적 버튼이에요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
Group
여러 아이템을 그룹화하는 데 사용해요. Select.Label과 함께 사용하면 자동 라벨링으로 좋은 접근성을 확보할 수 있어요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
Label
그룹의 라벨을 렌더링하는 데 사용해요. 화살표 키로 포커스되지 않아요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
Separator
셀렉트에서 아이템을 시각적으로 분리하는 데 사용해요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
Arrow
콘텐츠 옆에 렌더링할 수 있는 선택적 화살표 요소예요. 트리거와 Select.Content를 시각적으로 연결해 주는 데 도움을 줘요. Select.Content 안에 렌더링해야 해요. position이 popper일 때만 사용할 수 있어요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
width |
number |
10 |
height |
number |
5 |
Examples
위치 지정 모드 바꾸기
기본적으로 Select는 네이티브 MacOS 메뉴처럼 활성 아이템을 기준으로 Select.Content를 위치시켜요. Popover나 DropdownMenu처럼 다른 위치 지정 방식을 원하면 position을 popper로 설정하고 side, sideOffset 등 추가 정렬 옵션을 사용할 수 있어요.
// index.jsx
import { Select } from "radix-ui";
export default () => (
<Select.Root>
<Select.Trigger>…</Select.Trigger>
<Select.Portal>
<Select.Content position="popper" sideOffset={5}>
…
</Select.Content>
</Select.Portal>
</Select.Root>
);
콘텐츠 크기 제한
Select.Content에 position="popper"를 쓸 때 콘텐츠의 너비를 트리거 너비에 맞추고 싶을 수 있어요. 높이를 뷰포트에 넘지 않게 제한할 수도 있어요.
--radix-select-trigger-width, --radix-select-content-available-height 같은 여러 CSS 커스텀 프로퍼티가 이를 지원해요. 콘텐츠 크기를 제한하는 데 활용하세요.
// index.jsx
import { Select } from "radix-ui";
import "./styles.css";
export default () => (
<Select.Root>
<Select.Trigger>…</Select.Trigger>
<Select.Portal>
<Select.Content className="SelectContent" position="popper" sideOffset={5}>
…
</Select.Content>
</Select.Portal>
</Select.Root>
);
/* styles.css */
.SelectContent {
width: var(--radix-select-trigger-width);
max-height: var(--radix-select-content-available-height);
}
비활성 아이템과 함께
data-disabled 속성으로 비활성 아이템에 특별한 스타일을 줄 수 있어요.
// index.jsx
import { Select } from "radix-ui";
import "./styles.css";
export default () => (
<Select.Root>
<Select.Trigger>…</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.Viewport>
<Select.Item className="SelectItem" disabled>
…
</Select.Item>
<Select.Item>…</Select.Item>
<Select.Item>…</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
);
/* styles.css */
.SelectItem[data-disabled] {
color: "gainsboro";
}
플레이스홀더와 함께
셀렉트에 값이 없을 때 Value의 placeholder prop을 사용할 수 있어요. 스타일링에 도움을 주는 data-placeholder 속성이 Trigger에도 있어요.
// index.jsx
import { Select } from "radix-ui";
import "./styles.css";
export default () => (
<Select.Root>
<Select.Trigger className="SelectTrigger">
<Select.Value placeholder="Pick an option" />
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Content>…</Select.Content>
</Select.Portal>
</Select.Root>
);
/* styles.css */
.SelectTrigger[data-placeholder] {
color: "gainsboro";
}
구분선과 함께
Separator 파트로 아이템 사이에 구분선을 넣을 수 있어요.
<Select.Root>
<Select.Trigger>…</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.Viewport>
<Select.Item>…</Select.Item>
<Select.Item>…</Select.Item>
<Select.Item>…</Select.Item>
<Select.Separator />
<Select.Item>…</Select.Item>
<Select.Item>…</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
그룹 아이템과 함께
Group과 Label 파트로 아이템을 섹션으로 묶을 수 있어요.
<Select.Root>
<Select.Trigger>…</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.Viewport>
<Select.Group>
<Select.Label>Label</Select.Label>
<Select.Item>…</Select.Item>
<Select.Item>…</Select.Item>
<Select.Item>…</Select.Item>
</Select.Group>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
복잡한 아이템과 함께
아이템에 커스텀 콘텐츠를 사용할 수 있어요.
import { Select } from "radix-ui";
export default () => (
<Select.Root>
<Select.Trigger>…</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.Viewport>
<Select.Item>
<Select.ItemText>
<img src="…" />
Adolfo Hess
</Select.ItemText>
<Select.ItemIndicator>…</Select.ItemIndicator>
</Select.Item>
<Select.Item>…</Select.Item>
<Select.Item>…</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
);
트리거에 표시되는 값 제어
기본적으로 트리거는 선택된 아이템 ItemText의 콘텐츠를 자동으로 표시해요. ItemText 파트 안/밖에 무엇을 둘지 선택해 표시되는 내용을 제어할 수 있어요.
더 유연하게 하려면 value/onValueChange props로 컴포넌트를 제어하고 SelectValue에 children을 전달할 수 있어요. 거기에 넣는 내용이 접근 가능하도록 확실히 하세요.
const countries = { france: "🇫🇷", "united-kingdom": "🇬🇧", spain: "🇪🇸" };
export default () => {
const [value, setValue] = React.useState("france");
return (
<Select.Root value={value} onValueChange={setValue}>
<Select.Trigger>
<Select.Value aria-label={value}>{countries[value]}</Select.Value>
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.Viewport>
<Select.Item value="france">
<Select.ItemText>France</Select.ItemText>
<Select.ItemIndicator>…</Select.ItemIndicator>
</Select.Item>
<Select.Item value="united-kingdom">
<Select.ItemText>United Kingdom</Select.ItemText>
<Select.ItemIndicator>…</Select.ItemIndicator>
</Select.Item>
<Select.Item value="spain">
<Select.ItemText>Spain</Select.ItemText>
<Select.ItemIndicator>…</Select.ItemIndicator>
</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
);
};
커스텀 스크롤바와 함께
최상의 UX를 위해 ScrollUpButton과 ScrollDownButton 파트 사용을 권장하므로 네이티브 스크롤바는 기본적으로 숨겨져 있어요. 이 파트를 쓰고 싶지 않다면 우리의 Scroll Area primitive와 셀렉트를 조합하세요.
// index.jsx
import { Select, ScrollArea } from "radix-ui";
import "./styles.css";
export default () => (
<Select.Root>
<Select.Trigger>…</Select.Trigger>
<Select.Portal>
<Select.Content>
<ScrollArea.Root className="ScrollAreaRoot" type="auto">
<Select.Viewport asChild>
<ScrollArea.Viewport className="ScrollAreaViewport">
<StyledItem>…</StyledItem>
<StyledItem>…</StyledItem>
<StyledItem>…</StyledItem>
</ScrollArea.Viewport>
</Select.Viewport>
<ScrollArea.Scrollbar className="ScrollAreaScrollbar" orientation="vertical">
<ScrollArea.Thumb className="ScrollAreaThumb" />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</Select.Content>
</Select.Portal>
</Select.Root>
);
/* styles.css */
.ScrollAreaRoot {
width: 100%;
height: 100%;
}
.ScrollAreaViewport {
width: 100%;
height: 100%;
}
.ScrollAreaScrollbar {
width: 4px;
padding: 5px 2px;
}
.ScrollAreaThumb {
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
숨겨진 input 분리하기
기본적으로 Select.Root는 폼 제출을 위해 시각적으로 숨겨진 select를 렌더링해요. 그 요소를 재구성하거나 이동하거나 제외하려면, 더 낮은 레벨의 파트들로 셀렉트를 조립할 수 있어요.
중요: 이 파트들은 불안정하며
unstable_접두사가 붙어 있어서 API가 향후 릴리스에서 바뀔 수 있어요.
Select.unstable_Provider는 셀렉트 상태를 제공하고 폼 관련·상태 props(name,value,defaultValue,onValueChange등)를 받아요.Select.unstable_BubbleInput은Select.Root가 기본으로 렌더링하는 시각적으로 숨겨진select예요. 폼 제출이 필요 없으면 생략해요.
import { Select } from "radix-ui";
export default () => (
<Select.unstable_Provider name="fruit">
<Select.Trigger>
<Select.Value placeholder="Pick an option" />
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Content>{/* Your content */}</Select.Content>
</Select.Portal>
<Select.unstable_BubbleInput />
</Select.unstable_Provider>
);
Accessibility
ListBox WAI-ARIA 디자인 패턴을 준수해요.
자세한 내용은 W3C Select-Only Combobox 예시를 참고하세요.
키보드 상호작용
| Key | Description |
|---|---|
Space |
When focus is on Select.Trigger, opens the select and focuses the selected item. When focus is on an item, selects the focused item. |
Enter |
When focus is on Select.Trigger, opens the select and focuses the first item. When focus is on an item, selects the focused item. |
ArrowDown |
When focus is on Select.Trigger, opens the select. When focus is on an item, moves focus to the next item. |
ArrowUp |
When focus is on Select.Trigger, opens the select. When focus is on an item, moves focus to the previous item. |
Esc |
Closes the select and moves focus to Select.Trigger. |
라벨링
셀렉트를 위한 시각적이고 접근 가능한 라벨을 제공하려면 Label 컴포넌트를 사용해요.
import { Select, Label } from "radix-ui";
export default () => (
<>
<Label>
Country
<Select.Root>…</Select.Root>
</Label>
{/* or */}
<Label htmlFor="country">Country</Label>
<Select.Root>
<Select.Trigger id="country">…</Select.Trigger>
<Select.Portal>
<Select.Content>…</Select.Content>
</Select.Portal>
</Select.Root>
</>
);
Custom APIs
primitive 파트를 자신의 컴포넌트로 추상화해 나만의 API를 만들 수 있어요.
Select와 SelectItem까지 추상화
이 예제는 대부분의 파트를 추상화해요.
사용법
import { Select, SelectItem } from "./your-select";
export default () => (
<Select defaultValue="2">
<SelectItem value="1">Item 1</SelectItem>
<SelectItem value="2">Item 2</SelectItem>
<SelectItem value="3">Item 3</SelectItem>
</Select>
);
구현
// your-select.jsx
import * as React from "react";
import { Select as SelectPrimitive } from "radix-ui";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons";
export const Select = React.forwardRef(({ children, ...props }, forwardedRef) => {
return (
<SelectPrimitive.Root {...props}>
<SelectPrimitive.Trigger ref={forwardedRef}>
<SelectPrimitive.Value />
<SelectPrimitive.Icon>
<ChevronDownIcon />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal>
<SelectPrimitive.Content>
<SelectPrimitive.ScrollUpButton>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport>{children}</SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
);
});
export const SelectItem = React.forwardRef(({ children, ...props }, forwardedRef) => {
return (
<SelectPrimitive.Item {...props} ref={forwardedRef}>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator>
<CheckIcon />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
);
});
더 알아보기 (Learn more)
- 기본 위치 지정은 활성 아이템 기준(item-aligned)이고,
position="popper"로 Popover/DropdownMenu 스타일의 위치 지정을 쓸 수 있어요. - 네이티브
<select>와 달리 완성된 접근성(라벨링·키보드)과 유연한 커스텀 스타일링이 가능해요.