Dropdown Menu
Dropdown Menu
버튼으로 트리거되는, 일련의 동작이나 기능 같은 메뉴를 사용자에게 표시하는 컴포넌트예요.
출처: 문서
본문
버튼을 클릭하면 아래로 펼쳐지는 드롭다운 메뉴 컴포넌트예요. 서브메뉴, 아이템/라벨/그룹, 체크 가능한 아이템, 라디오 아이템, 모달/비모달 모드 등을 모두 지원하고 포커스 관리와 키보드 내비게이션이 완전히 처리돼요.
Features
- 제어(controlled)/비제어(uncontrolled) 방식 모두 지원.
- 읽기 방향을 설정할 수 있는 서브메뉴 지원.
- 아이템, 라벨, 아이템 그룹 지원.
- 체크 가능한 아이템(단일/다중) + 선택적 indeterminate 상태 지원.
- 모달/비모달 모드 지원.
- side, alignment, offset, collision 처리 커스터마이즈.
- 선택적으로 가리키는 화살표(arrow) 렌더링.
- 포커스 완전 관리.
- 완전한 키보드 내비게이션.
- Typeahead 지원.
- 닫기/Dismiss 및 레이어링 동작의 높은 커스터마이즈 가능.
Anatomy
모든 파트를 임포트해 조립해요.
import { DropdownMenu } from "radix-ui";
export default () => (
<DropdownMenu.Root>
<DropdownMenu.Trigger />
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Label />
<DropdownMenu.Item />
<DropdownMenu.Group>
<DropdownMenu.Item />
</DropdownMenu.Group>
<DropdownMenu.CheckboxItem>
<DropdownMenu.ItemIndicator />
</DropdownMenu.CheckboxItem>
<DropdownMenu.RadioGroup>
<DropdownMenu.RadioItem>
<DropdownMenu.ItemIndicator />
</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger />
<DropdownMenu.Portal>
<DropdownMenu.SubContent />
</DropdownMenu.Portal>
</DropdownMenu.Sub>
<DropdownMenu.Separator />
<DropdownMenu.Arrow />
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
API Reference
Root
드롭다운 메뉴의 모든 파트를 담아요.
| Prop | Type | Default |
|---|---|---|
defaultOpen |
boolean |
No default value |
open |
boolean |
No default value |
onOpenChange |
function |
No default value |
modal |
boolean |
true |
dir |
enum |
No default value |
Trigger
드롭다운 메뉴를 토글하는 버튼이에요. 기본적으로 DropdownMenu.Content는 트리거에 맞춰 위치하게 돼요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
| Data attribute | Values |
|---|---|
[data-state] |
"open" | "closed" |
[data-disabled] |
Present when disabled |
Portal
사용하면 content 파트를 body로 포털해요.
| Prop | Type | Default |
|---|---|---|
forceMount |
boolean |
No default value |
container |
HTMLElement |
document.body |
Content
드롭다운 메뉴가 열렸을 때 튀어나오는 컴포넌트예요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
loop |
boolean |
false |
onCloseAutoFocus |
function |
No default value |
onEscapeKeyDown |
function |
No default value |
onPointerDownOutside |
function |
No default value |
onFocusOutside |
function |
No default value |
onInteractOutside |
function |
No default value |
forceMount |
boolean |
No default value |
side |
enum |
"bottom" |
sideOffset |
number |
0 |
align |
enum |
"center" |
alignOffset |
number |
0 |
avoidCollisions |
boolean |
true |
collisionBoundary |
Boundary |
[] |
collisionPadding |
number | Padding |
0 |
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" |
[data-orientation] |
"vertical" | "horizontal" |
| CSS Variable | Description |
|---|---|
--radix-dropdown-menu-content-transform-origin |
The transform-origin computed from the content and arrow positions/offsets |
--radix-dropdown-menu-content-available-width |
The remaining width between the trigger and the boundary edge |
--radix-dropdown-menu-content-available-height |
The remaining height between the trigger and the boundary edge |
--radix-dropdown-menu-trigger-width |
The width of the trigger |
--radix-dropdown-menu-trigger-height |
The height of the trigger |
Arrow
드롭다운 메뉴 옆에 렌더링할 수 있는 선택적 화살표 요소예요. 트리거와 DropdownMenu.Content를 시각적으로 연결해 주는 데 도움을 줘요. DropdownMenu.Content 안에 렌더링해야 해요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
width |
number |
10 |
height |
number |
5 |
Item
드롭다운 메뉴 아이템을 담는 컴포넌트예요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
disabled |
boolean |
No default value |
onSelect |
function |
No default value |
textValue |
string |
No default value |
| Data attribute | Values |
|---|---|
[data-orientation] |
"vertical" | "horizontal" |
[data-highlighted] |
Present when highlighted |
[data-disabled] |
Present when disabled |
Group
여러 DropdownMenu.Item을 그룹화하는 데 사용해요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
Label
라벨을 렌더링하는 데 사용해요. 화살표 키로 포커스되지 않아요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
CheckboxItem
체크박스처럼 제어되고 렌더링되는 아이템이에요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
checked |
boolean | 'indeterminate' |
No default value |
onCheckedChange |
function |
No default value |
disabled |
boolean |
No default value |
onSelect |
function |
No default value |
textValue |
string |
No default value |
| Data attribute | Values |
|---|---|
[data-state] |
"checked" | "unchecked" | "indeterminate" |
[data-highlighted] |
Present when highlighted |
[data-disabled] |
Present when disabled |
RadioGroup
여러 DropdownMenu.RadioItem을 그룹화하는 데 사용해요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
value |
string |
No default value |
onValueChange |
function |
No default value |
RadioItem
라디오처럼 제어되고 렌더링되는 아이템이에요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
value* |
string |
No default value |
disabled |
boolean |
No default value |
onSelect |
function |
No default value |
textValue |
string |
No default value |
| Data attribute | Values |
|---|---|
[data-state] |
"checked" | "unchecked" | "indeterminate" |
[data-highlighted] |
Present when highlighted |
[data-disabled] |
Present when disabled |
ItemIndicator
부모 DropdownMenu.CheckboxItem 또는 DropdownMenu.RadioItem이 checked일 때 렌더링돼요. 이 요소를 직접 스타일하거나 아이콘을 넣는 래퍼로 쓰거나 둘 다 할 수 있어요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
forceMount |
boolean |
No default value |
| Data attribute | Values |
|---|---|
[data-state] |
"checked" | "unchecked" | "indeterminate" |
Separator
드롭다운 메뉴에서 아이템을 시각적으로 분리하는 데 사용해요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
Sub
서브메뉴의 모든 파트를 담아요.
| Prop | Type | Default |
|---|---|---|
defaultOpen |
boolean |
No default value |
open |
boolean |
No default value |
onOpenChange |
function |
No default value |
SubTrigger
서브메뉴를 여는 아이템이에요. DropdownMenu.Sub 안에 렌더링해야 해요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
disabled |
boolean |
No default value |
textValue |
string |
No default value |
| Data attribute | Values |
|---|---|
[data-state] |
"open" | "closed" |
[data-highlighted] |
Present when highlighted |
[data-disabled] |
Present when disabled |
| CSS Variable | Description |
|---|---|
--radix-dropdown-menu-content-transform-origin |
The transform-origin computed from the content and arrow positions/offsets |
--radix-dropdown-menu-content-available-width |
The remaining width between the trigger and the boundary edge |
--radix-dropdown-menu-content-available-height |
The remaining height between the trigger and the boundary edge |
--radix-dropdown-menu-trigger-width |
The width of the trigger |
--radix-dropdown-menu-trigger-height |
The height of the trigger |
SubContent
서브메뉴가 열렸을 때 튀어나오는 컴포넌트예요. DropdownMenu.Sub 안에 렌더링해야 해요.
| Prop | Type | Default |
|---|---|---|
asChild |
boolean |
false |
loop |
boolean |
false |
onEscapeKeyDown |
function |
No default value |
onPointerDownOutside |
function |
No default value |
onFocusOutside |
function |
No default value |
onInteractOutside |
function |
No default value |
forceMount |
boolean |
No default value |
sideOffset |
number |
0 |
align |
enum |
"start" |
alignOffset |
number |
0 |
avoidCollisions |
boolean |
true |
collisionBoundary |
Boundary |
[] |
collisionPadding |
number | Padding |
0 |
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" |
[data-orientation] |
"vertical" | "horizontal" |
Examples
서브메뉴와 함께
DropdownMenu.Sub과 그 파트들을 조합해 서브메뉴를 만들 수 있어요.
<DropdownMenu.Root>
<DropdownMenu.Trigger>…</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item>…</DropdownMenu.Item>
<DropdownMenu.Item>…</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>Sub menu →</DropdownMenu.SubTrigger>
<DropdownMenu.Portal>
<DropdownMenu.SubContent>
<DropdownMenu.Item>Sub menu item</DropdownMenu.Item>
<DropdownMenu.Item>Sub menu item</DropdownMenu.Item>
<DropdownMenu.Arrow />
</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>
<DropdownMenu.Separator />
<DropdownMenu.Item>…</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
비활성 아이템과 함께
data-disabled 속성으로 비활성 아이템에 특별한 스타일을 줄 수 있어요.
// index.jsx
import { DropdownMenu } from "radix-ui";
import "./styles.css";
export default () => (
<DropdownMenu.Root>
<DropdownMenu.Trigger>…</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item className="DropdownMenuItem" disabled>
…
</DropdownMenu.Item>
<DropdownMenu.Item className="DropdownMenuItem">…</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
/* styles.css */
.DropdownMenuItem[data-disabled] {
color: gainsboro;
}
구분선과 함께
Separator 파트로 아이템 사이에 구분선을 넣을 수 있어요.
<DropdownMenu.Root>
<DropdownMenu.Trigger>…</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item>…</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item>…</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item>…</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
라벨과 함께
Label 파트로 섹션에 라벨을 붙일 수 있어요.
<DropdownMenu.Root>
<DropdownMenu.Trigger>…</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Label>Label</DropdownMenu.Label>
<DropdownMenu.Item>…</DropdownMenu.Item>
<DropdownMenu.Item>…</DropdownMenu.Item>
<DropdownMenu.Item>…</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
체크박스 아이템과 함께
CheckboxItem 파트로 체크할 수 있는 아이템을 추가해요.
import * as React from "react";
import { CheckIcon } from "@radix-ui/react-icons";
import { DropdownMenu } from "radix-ui";
export default () => {
const [checked, setChecked] = React.useState(true);
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>…</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item>…</DropdownMenu.Item>
<DropdownMenu.Item>…</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.CheckboxItem checked={checked} onCheckedChange={setChecked}>
<DropdownMenu.ItemIndicator>
<CheckIcon />
</DropdownMenu.ItemIndicator>
Checkbox item
</DropdownMenu.CheckboxItem>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
};
라디오 아이템과 함께
RadioGroup과 RadioItem 파트로 여러 개 중 하나를 체크하는 아이템을 추가해요.
import * as React from "react";
import { CheckIcon } from "@radix-ui/react-icons";
import { DropdownMenu } from "radix-ui";
export default () => {
const [color, setColor] = React.useState("blue");
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>…</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.RadioGroup value={color} onValueChange={setColor}>
<DropdownMenu.RadioItem value="red">
<DropdownMenu.ItemIndicator>
<CheckIcon />
</DropdownMenu.ItemIndicator>
Red
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="blue">
<DropdownMenu.ItemIndicator>
<CheckIcon />
</DropdownMenu.ItemIndicator>
Blue
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="green">
<DropdownMenu.ItemIndicator>
<CheckIcon />
</DropdownMenu.ItemIndicator>
Green
</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
};
복잡한 아이템과 함께
Item 파트에 이미지 같은 추가 장식 요소를 넣을 수 있어요.
import { DropdownMenu } from "radix-ui";
export default () => (
<DropdownMenu.Root>
<DropdownMenu.Trigger>…</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item>
<img src="…" />
Adolfo Hess
</DropdownMenu.Item>
<DropdownMenu.Item>
<img src="…" />
Miyah Myles
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
콘텐츠/서브콘텐츠 크기 제한
콘텐츠(또는 서브콘텐츠)의 너비를 트리거(또는 서브트리거) 너비에 맞추고 싶을 수 있어요. 높이를 뷰포트에 넘지 않게 제한할 수도 있어요.
--radix-dropdown-menu-trigger-width, --radix-dropdown-menu-content-available-height 같은 여러 CSS 커스텀 프로퍼티가 이를 지원해요. 콘텐츠 크기를 제한하는 데 활용하세요.
// index.jsx
import { DropdownMenu } from "radix-ui";
import "./styles.css";
export default () => (
<DropdownMenu.Root>
<DropdownMenu.Trigger>…</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="DropdownMenuContent" sideOffset={5}>
…
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
/* styles.css */
.DropdownMenuContent {
width: var(--radix-dropdown-menu-trigger-width);
max-height: var(--radix-dropdown-menu-content-available-height);
}
Origin 인지 애니메이션
--radix-dropdown-menu-content-transform-origin CSS 커스텀 프로퍼티를 노출해요. side, sideOffset, align, alignOffset과 충돌을 기반으로 계산된 origin에서 콘텐츠를 애니메이션하는 데 사용하세요.
// index.jsx
import { DropdownMenu } from "radix-ui";
import "./styles.css";
export default () => (
<DropdownMenu.Root>
<DropdownMenu.Trigger>…</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="DropdownMenuContent">…</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
/* styles.css */
.DropdownMenuContent {
transform-origin: var(--radix-dropdown-menu-content-transform-origin);
animation: scaleIn 0.5s ease-out;
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
Collision 인지 애니메이션
data-side와 data-align 속성을 노출해요. 이 값들은 충돌을 반영해 런타임에 바뀌어요. 충돌과 방향에 인지된 애니메이션을 만드는 데 사용하세요.
// index.jsx
import { DropdownMenu } from "radix-ui";
import "./styles.css";
export default () => (
<DropdownMenu.Root>
<DropdownMenu.Trigger>…</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="DropdownMenuContent">…</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
/* styles.css */
.DropdownMenuContent {
animation-duration: 0.6s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
.DropdownMenuContent[data-side="top"] {
animation-name: slideUp;
}
.DropdownMenuContent[data-side="bottom"] {
animation-name: slideDown;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
Accessibility
Menu Button WAI-ARIA 디자인 패턴을 준수하고, 메뉴 아이템 간 포커스 이동을 roving tabindex로 관리해요.
키보드 상호작용
| Key | Description |
|---|---|
Space |
When focus is on DropdownMenu.Trigger, opens the dropdown menu and focuses the first item. When focus is on an item, activates the focused item. |
Enter |
When focus is on DropdownMenu.Trigger, opens the dropdown menu and focuses the first item. When focus is on an item, activates the focused item. |
ArrowDown |
When focus is on DropdownMenu.Trigger, opens the dropdown menu. When focus is on an item, moves focus to the next item. |
ArrowUp |
When focus is on an item, moves focus to the previous item. |
ArrowRight``ArrowLeft |
When focus is on DropdownMenu.SubTrigger, opens or closes the submenu depending on reading direction. |
Esc |
Closes the dropdown menu and moves focus to DropdownMenu.Trigger. |
Custom APIs
primitive 파트를 자신의 컴포넌트로 추상화해 나만의 API를 만들 수 있어요.
화살표와 아이템 인디케이터 추상화
이 예제는 DropdownMenu.Arrow와 DropdownMenu.ItemIndicator 파트를 추상화하고, CheckboxItem/RadioItem의 구현 세부사항도 감싸요.
사용법
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuGroup,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
} from "./your-dropdown-menu";
export default () => (
<DropdownMenu>
<DropdownMenuTrigger>DropdownMenu trigger</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Item</DropdownMenuItem>
<DropdownMenuLabel>Label</DropdownMenuLabel>
<DropdownMenuGroup>Group</DropdownMenuGroup>
<DropdownMenuCheckboxItem>CheckboxItem</DropdownMenuCheckboxItem>
<DropdownMenuSeparator>Separator</DropdownMenuSeparator>
<DropdownMenuRadioGroup>
<DropdownMenuRadioItem>RadioItem</DropdownMenuRadioItem>
<DropdownMenuRadioItem>RadioItem</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
구현
// your-dropdown-menu.jsx
import * as React from "react";
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
import { CheckIcon, DividerHorizontalIcon } from "@radix-ui/react-icons";
export const DropdownMenu = DropdownMenuPrimitive.Root;
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
export const DropdownMenuContent = React.forwardRef(({ children, ...props }, forwardedRef) => {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content {...props} ref={forwardedRef}>
{children}
<DropdownMenuPrimitive.Arrow />
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>
);
});
export const DropdownMenuLabel = DropdownMenuPrimitive.Label;
export const DropdownMenuItem = DropdownMenuPrimitive.Item;
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
export const DropdownMenuCheckboxItem = React.forwardRef(({ children, ...props }, forwardedRef) => {
return (
<DropdownMenuPrimitive.CheckboxItem {...props} ref={forwardedRef}>
{children}
<DropdownMenuPrimitive.ItemIndicator>
{props.checked === "indeterminate" && <DividerHorizontalIcon />}
{props.checked === true && <CheckIcon />}
</DropdownMenuPrimitive.ItemIndicator>
</DropdownMenuPrimitive.CheckboxItem>
);
});
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
export const DropdownMenuRadioItem = React.forwardRef(({ children, ...props }, forwardedRef) => {
return (
<DropdownMenuPrimitive.RadioItem {...props} ref={forwardedRef}>
{children}
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon />
</DropdownMenuPrimitive.ItemIndicator>
</DropdownMenuPrimitive.RadioItem>
);
});
export const DropdownMenuSeparator = DropdownMenuPrimitive.Separator;
더 알아보기 (Learn more)
- Dropdown Menu의 대부분 구조는 Context Menu와 공유되지만, 버튼 클릭(트리거)으로 열리는 점이 달라요.
DropdownMenu.Arrow를 Content 안에 넣으면 트리거를 가리키는 화살표를 렌더링할 수 있어요.