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>와 달리 완성된 접근성(라벨링·키보드)과 유연한 커스텀 스타일링이 가능해요.