Context Menu

Context Menu

마우스 오른쪽 클릭이나 길게 누르기로 트리거되는, 포인터 위치에 나타나는 메뉴 컴포넌트예요.

출처: 문서

본문

오른쪽 클릭(또는 터치 기기의 길게 누르기)으로 열리는 컨텍스트 메뉴예요. 서브메뉴, 아이템/라벨/그룹, 체크 가능한 아이템, 라디오 아이템, 모달/비모달 모드 등을 모두 지원하며 포커스 관리와 키보드 내비게이션이 완전히 처리돼요.

Features

  • 읽기 방향을 설정할 수 있는 서브메뉴 지원.
  • 아이템, 라벨, 아이템 그룹 지원.
  • 체크 가능한 아이템(단일/다중) + 선택적 indeterminate 상태 지원.
  • 모달/비모달 모드 지원.
  • side, alignment, offset, collision 처리 커스터마이즈.
  • 포커스 완전 관리.
  • 완전한 키보드 내비게이션.
  • Typeahead 지원.
  • 닫기/Dismiss 및 레이어링 동작의 높은 커스터마이즈 가능.
  • 터치 기기에서 길게 누르면 트리거.

Anatomy

모든 파트를 임포트해 조립해요.

import { ContextMenu } from "radix-ui";

export default () => (
  <ContextMenu.Root>
    <ContextMenu.Trigger />

    <ContextMenu.Portal>
      <ContextMenu.Content>
        <ContextMenu.Label />

        <ContextMenu.Item />

        <ContextMenu.Group>
          <ContextMenu.Item />
        </ContextMenu.Group>

        <ContextMenu.CheckboxItem>
          <ContextMenu.ItemIndicator />
        </ContextMenu.CheckboxItem>

        <ContextMenu.RadioGroup>
          <ContextMenu.RadioItem>
            <ContextMenu.ItemIndicator />
          </ContextMenu.RadioItem>
        </ContextMenu.RadioGroup>

        <ContextMenu.Sub>
          <ContextMenu.SubTrigger />

          <ContextMenu.Portal>
            <ContextMenu.SubContent />
          </ContextMenu.Portal>
        </ContextMenu.Sub>

        <ContextMenu.Separator />
      </ContextMenu.Content>
    </ContextMenu.Portal>
  </ContextMenu.Root>
);

API Reference

Menu WAI-ARIA 디자인 패턴을 준수하고, 메뉴 아이템 간 포커스 이동에 roving tabindex를 사용해요.

Root

컨텍스트 메뉴의 모든 파트를 담아요.

Prop Type Default
dir enum No default value
open boolean No default value
onOpenChange function No default value
modal boolean true

Trigger

컨텍스트 메뉴를 여는 영역이에요. 오른쪽 클릭(또는 관련 키보드 단축키)으로 메뉴를 열 대상 요소를 감싸 주세요.

Prop Type Default
asChild boolean false
disabled boolean false
Data attribute Values
[data-state] "open" | "closed"

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
alignOffset number 0
avoidCollisions boolean true
collisionBoundary Boundary []
collisionPadding number | Padding 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-context-menu-content-transform-origin The transform-origin computed from the content and arrow positions/offsets
--radix-context-menu-content-available-width The remaining width between the trigger and the boundary edge
--radix-context-menu-content-available-height The remaining height between the trigger and the boundary edge
--radix-context-menu-trigger-width The width of the trigger
--radix-context-menu-trigger-height The height of the trigger

Arrow

서브메뉴 옆에 렌더링할 수 있는 선택적 화살표 요소예요. 트리거 아이템과 ContextMenu.Content를 시각적으로 연결해 주는 데 도움을 줘요. ContextMenu.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-highlighted] Present when highlighted
[data-disabled] Present when disabled

Group

여러 ContextMenu.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

여러 ContextMenu.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

부모 ContextMenu.CheckboxItem 또는 ContextMenu.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

서브메뉴를 여는 아이템이에요. ContextMenu.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

SubContent

서브메뉴가 열렸을 때 튀어나오는 컴포넌트예요. ContextMenu.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"
CSS Variable Description
--radix-context-menu-content-transform-origin The transform-origin computed from the content and arrow positions/offsets
--radix-context-menu-content-available-width The remaining width between the trigger and the boundary edge
--radix-context-menu-content-available-height The remaining height between the trigger and the boundary edge
--radix-context-menu-trigger-width The width of the trigger
--radix-context-menu-trigger-height The height of the trigger

Examples

서브메뉴와 함께

ContextMenu.Sub과 그 파트들을 조합해 서브메뉴를 만들 수 있어요.

<ContextMenu.Root>
  <ContextMenu.Trigger>…</ContextMenu.Trigger>

  <ContextMenu.Portal>
    <ContextMenu.Content>
      <ContextMenu.Item>…</ContextMenu.Item>

      <ContextMenu.Item>…</ContextMenu.Item>

      <ContextMenu.Separator />

      <ContextMenu.Sub>
        <ContextMenu.SubTrigger>Sub menu →</ContextMenu.SubTrigger>

        <ContextMenu.Portal>
          <ContextMenu.SubContent>
            <ContextMenu.Item>Sub menu item</ContextMenu.Item>

            <ContextMenu.Item>Sub menu item</ContextMenu.Item>

            <ContextMenu.Arrow />
          </ContextMenu.SubContent>
        </ContextMenu.Portal>
      </ContextMenu.Sub>

      <ContextMenu.Separator />

      <ContextMenu.Item>…</ContextMenu.Item>
    </ContextMenu.Content>
  </ContextMenu.Portal>
</ContextMenu.Root>

비활성 아이템과 함께

data-disabled 속성으로 비활성 아이템에 특별한 스타일을 줄 수 있어요.

// index.jsx

import { ContextMenu } from "radix-ui";

import "./styles.css";

export default () => (
  <ContextMenu.Root>
    <ContextMenu.Trigger>…</ContextMenu.Trigger>

    <ContextMenu.Portal>
      <ContextMenu.Content>
        <ContextMenu.Item className="ContextMenuItem" disabled>
          …
        </ContextMenu.Item>

        <ContextMenu.Item className="ContextMenuItem">…</ContextMenu.Item>
      </ContextMenu.Content>
    </ContextMenu.Portal>
  </ContextMenu.Root>
);
/* styles.css */

.ContextMenuItem[data-disabled] {
  color: gainsboro;
}

구분선과 함께

Separator 파트로 아이템 사이에 구분선을 넣을 수 있어요.

<ContextMenu.Root>
  <ContextMenu.Trigger>…</ContextMenu.Trigger>

  <ContextMenu.Portal>
    <ContextMenu.Content>
      <ContextMenu.Item>…</ContextMenu.Item>

      <ContextMenu.Separator />

      <ContextMenu.Item>…</ContextMenu.Item>

      <ContextMenu.Separator />

      <ContextMenu.Item>…</ContextMenu.Item>
    </ContextMenu.Content>
  </ContextMenu.Portal>
</ContextMenu.Root>

라벨과 함께

Label 파트로 섹션에 라벨을 붙일 수 있어요.

<ContextMenu.Root>
  <ContextMenu.Trigger>…</ContextMenu.Trigger>

  <ContextMenu.Portal>
    <ContextMenu.Content>
      <ContextMenu.Label>Label</ContextMenu.Label>

      <ContextMenu.Item>…</ContextMenu.Item>

      <ContextMenu.Item>…</ContextMenu.Item>

      <ContextMenu.Item>…</ContextMenu.Item>
    </ContextMenu.Content>
  </ContextMenu.Portal>
</ContextMenu.Root>

체크박스 아이템과 함께

CheckboxItem 파트로 체크할 수 있는 아이템을 추가해요.

import * as React from "react";

import { CheckIcon } from "@radix-ui/react-icons";

import { ContextMenu } from "radix-ui";

export default () => {
  const [checked, setChecked] = React.useState(true);

  return (
    <ContextMenu.Root>
      <ContextMenu.Trigger>…</ContextMenu.Trigger>

      <ContextMenu.Portal>
        <ContextMenu.Content>
          <ContextMenu.Item>…</ContextMenu.Item>

          <ContextMenu.Item>…</ContextMenu.Item>

          <ContextMenu.Separator />

          <ContextMenu.CheckboxItem checked={checked} onCheckedChange={setChecked}>
            <ContextMenu.ItemIndicator>
              <CheckIcon />
            </ContextMenu.ItemIndicator>
            Checkbox item
          </ContextMenu.CheckboxItem>
        </ContextMenu.Content>
      </ContextMenu.Portal>
    </ContextMenu.Root>
  );
};

라디오 아이템과 함께

RadioGroup과 RadioItem 파트로 여러 개 중 하나를 체크하는 아이템을 추가해요.

import * as React from "react";

import { CheckIcon } from "@radix-ui/react-icons";

import { ContextMenu } from "radix-ui";

export default () => {
  const [color, setColor] = React.useState("blue");

  return (
    <ContextMenu.Root>
      <ContextMenu.Trigger>…</ContextMenu.Trigger>

      <ContextMenu.Portal>
        <ContextMenu.Content>
          <ContextMenu.RadioGroup value={color} onValueChange={setColor}>
            <ContextMenu.RadioItem value="red">
              <ContextMenu.ItemIndicator>
                <CheckIcon />
              </ContextMenu.ItemIndicator>
              Red
            </ContextMenu.RadioItem>

            <ContextMenu.RadioItem value="blue">
              <ContextMenu.ItemIndicator>
                <CheckIcon />
              </ContextMenu.ItemIndicator>
              Blue
            </ContextMenu.RadioItem>

            <ContextMenu.RadioItem value="green">
              <ContextMenu.ItemIndicator>
                <CheckIcon />
              </ContextMenu.ItemIndicator>
              Green
            </ContextMenu.RadioItem>
          </ContextMenu.RadioGroup>
        </ContextMenu.Content>
      </ContextMenu.Portal>
    </ContextMenu.Root>
  );
};

복잡한 아이템과 함께

Item 파트에 이미지 같은 추가 장식 요소를 넣을 수 있어요.

import { ContextMenu } from "radix-ui";

export default () => (
  <ContextMenu.Root>
    <ContextMenu.Trigger>…</ContextMenu.Trigger>

    <ContextMenu.Portal>
      <ContextMenu.Content>
        <ContextMenu.Item>
          <img src="…" />
          Adolfo Hess
        </ContextMenu.Item>

        <ContextMenu.Item>
          <img src="…" />
          Miyah Myles
        </ContextMenu.Item>
      </ContextMenu.Content>
    </ContextMenu.Portal>
  </ContextMenu.Root>
);

콘텐츠/서브콘텐츠 크기 제한

콘텐츠(또는 서브콘텐츠)의 너비를 트리거(또는 서브트리거) 너비에 맞추고 싶을 수 있어요. 높이를 뷰포트에 넘지 않게 제한할 수도 있어요.

--radix-context-menu-trigger-width, --radix-context-menu-content-available-height 같은 여러 CSS 커스텀 프로퍼티가 이를 지원해요. 콘텐츠 크기를 제한하는 데 활용하세요.

// index.jsx

import { ContextMenu } from "radix-ui";

import "./styles.css";

export default () => (
  <ContextMenu.Root>
    <ContextMenu.Trigger>…</ContextMenu.Trigger>

    <ContextMenu.Portal>
      <ContextMenu.Content className="ContextMenuContent">…</ContextMenu.Content>
    </ContextMenu.Portal>
  </ContextMenu.Root>
);
/* styles.css */

.ContextMenuContent {
  width: var(--radix-context-menu-trigger-width);

  max-height: var(--radix-context-menu-content-available-height);
}

Origin 인지 애니메이션

--radix-context-menu-content-transform-origin CSS 커스텀 프로퍼티를 노출해요. side, sideOffset, align, alignOffset과 충돌을 기반으로 계산된 origin에서 콘텐츠를 애니메이션하는 데 사용하세요.

// index.jsx

import { ContextMenu } from "radix-ui";

import "./styles.css";

export default () => (
  <ContextMenu.Root>
    <ContextMenu.Trigger>…</ContextMenu.Trigger>

    <ContextMenu.Portal>
      <ContextMenu.Content className="ContextMenuContent">…</ContextMenu.Content>
    </ContextMenu.Portal>
  </ContextMenu.Root>
);
/* styles.css */

.ContextMenuContent {
  transform-origin: var(--radix-context-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 { ContextMenu } from "radix-ui";

import "./styles.css";

export default () => (
  <ContextMenu.Root>
    <ContextMenu.Trigger>…</ContextMenu.Trigger>

    <ContextMenu.Portal>
      <ContextMenu.Content className="ContextMenuContent">…</ContextMenu.Content>
    </ContextMenu.Portal>
  </ContextMenu.Root>
);
/* styles.css */

.ContextMenuContent {
  animation-duration: 0.6s;

  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}

.ContextMenuContent[data-side="top"] {
  animation-name: slideUp;
}

.ContextMenuContent[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

메뉴 아이템 간 포커스 이동을 roving tabindex로 관리해요.

키보드 상호작용

Key Description
Space Activates the focused item.
Enter Activates the focused item.
ArrowDown Moves focus to the next item.
ArrowUp Moves focus to the previous item.
ArrowRight``ArrowLeft When focus is on ContextMenu.SubTrigger, opens or closes the submenu depending on reading direction.
Esc Closes the context menu

더 알아보기 (Learn more)

  • Context Menu는 Dropdown Menu와 구조가 거의 같지만 오른쪽 클릭/길게 누르기로 트리거된다는 점이 달라요.
  • checkable/radio 아이템은 선택 상태를 시각화하는 ContextMenu.ItemIndicator와 함께 쓰여요.