alpha

Menu

A menu displays a list of actions or options that a user can choose.

installyarn add react-aria-components
version1.0.0-alpha.0
usageimport {MenuTrigger, Menu} from 'react-aria-components'

Example#


import {MenuTrigger, Button, Popover, Menu, Item} from 'react-aria-components';

<MenuTrigger>
  <Button aria-label="Menu"></Button>
  <Popover>
    <Menu onAction={alert}>
      <Item id="open">Open</Item>
      <Item id="rename">Rename…</Item>
      <Item id="duplicate">Duplicate</Item>
      <Item id="share">Share…</Item>
      <Item id="delete">Delete…</Item>
    </Menu>
  </Popover>
</MenuTrigger>
import {
  Button,
  Item,
  Menu,
  MenuTrigger,
  Popover
} from 'react-aria-components';

<MenuTrigger>
  <Button aria-label="Menu"></Button>
  <Popover>
    <Menu onAction={alert}>
      <Item id="open">Open</Item>
      <Item id="rename">Rename…</Item>
      <Item id="duplicate">Duplicate</Item>
      <Item id="share">Share…</Item>
      <Item id="delete">Delete…</Item>
    </Menu>
  </Popover>
</MenuTrigger>
import {
  Button,
  Item,
  Menu,
  MenuTrigger,
  Popover
} from 'react-aria-components';

<MenuTrigger>
  <Button aria-label="Menu"></Button>
  <Popover>
    <Menu
      onAction={alert}
    >
      <Item id="open">
        Open
      </Item>
      <Item id="rename">
        Rename…
      </Item>
      <Item id="duplicate">
        Duplicate
      </Item>
      <Item id="share">
        Share…
      </Item>
      <Item id="delete">
        Delete…
      </Item>
    </Menu>
  </Popover>
</MenuTrigger>
Show CSS
.react-aria-Button {
  background: var(--spectrum-global-color-gray-50);
  border: 1px solid var(--spectrum-global-color-gray-400);
  color: var(--spectrum-alias-text-color);
  border-radius: 4px;
  appearance: none;
  vertical-align: middle;
  font-size: 1.2rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 0.286rem 0.857rem;

  &[data-pressed] {
    box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
    background: var(--spectrum-global-color-gray-100);
    border-color: var(--spectrum-global-color-gray-500);
  }

  &[data-focus-visible] {
    border-color: slateblue;
    box-shadow: 0 0 0 1px slateblue;
  }
}

.react-aria-Menu {
  --highlight-background: slateblue;
  --highlight-foreground: white;
  --separator-color: var(--spectrum-global-color-gray-500);
  --text-color: var(--spectrum-alias-text-color);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);

  max-height: inherit;
  overflow: auto;
  padding: 2px;
  margin: 0;
  min-width: 150px;
  box-sizing: border-box;
  outline: none;

  .react-aria-Section:not(:first-child) {
    margin-top: 12px;
  }

  .react-aria-Header {
    font-size: 1.143rem;
    font-weight: bold;
    padding: 0 0.714rem;
  }

  [role=separator] {
    height: 1px;
    background: var(--separator-color);
    margin: 2px 4px;
  }

  .react-aria-Item {
    margin: 2px;
    padding: 0.286rem 0.571rem;
    border-radius: 6px;
    outline: none;
    cursor: default;
    color: var(--text-color);
    font-size: 1.072rem;
    position: relative;
    display: grid;
    grid-template-areas: "label kbd"
                        "desc  kbd";
    align-items: center;
    column-gap: 20px;

    &[data-focused] {
      background: var(--highlight-background);
      color: var(--highlight-foreground);
    }

    &[aria-disabled] {
      color: var(--text-color-disabled);
    }

    [slot=label] {
      font-weight: bold;
      grid-area: label;
    }

    [slot=description] {
      font-size: small;
      grid-area: desc;
    }

    & kbd {
      grid-area: kbd;
      font-family: monospace;
      text-align: end;
    }

    &[aria-checked] {
      padding-left: 24px;

      &[aria-checked=true]::before {
        content: '✓';
        content: '✓' / '';
        alt: ' ';
        position: absolute;
        left: 4px;
        font-weight: 600;
      }

      &[role=menuitemradio][aria-checked=true]::before {
        content: '●';
        content: '●' / '';
        transform: scale(0.7)
      }
    }
  }
}

.react-aria-Popover {
  --background-color: var(--page-background);
  --border-color: var(--spectrum-global-color-gray-400);

  border: 1px solid var(--border-color);
  box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
  border-radius: 6px;
  background: var(--background-color);
  outline: none;

  &[data-placement=top] {
    --origin: translateY(8px);
  }

  &[data-placement=bottom] {
    --origin: translateY(-8px);
  }

  &[data-entering] {
    animation: slide 200ms;
  }

  &[data-exiting] {
    animation: slide 200ms reverse ease-in;
  }
}

@keyframes slide {
  from {
    transform: var(--origin);
    opacity: 0;
  }

  to {
    transform: translateY(0);
    opacity: 1;
  }
}

@media (forced-colors: active) {
  .react-aria-Menu {
    forced-color-adjust: none;

    --highlight-background: Highlight;
    --highlight-foreground: HighlightText;
    --separator-color: ButtonBorder;
    --text-color: ButtonText;
    --text-color-disabled: GrayText;
  }
}
.react-aria-Button {
  background: var(--spectrum-global-color-gray-50);
  border: 1px solid var(--spectrum-global-color-gray-400);
  color: var(--spectrum-alias-text-color);
  border-radius: 4px;
  appearance: none;
  vertical-align: middle;
  font-size: 1.2rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 0.286rem 0.857rem;

  &[data-pressed] {
    box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
    background: var(--spectrum-global-color-gray-100);
    border-color: var(--spectrum-global-color-gray-500);
  }

  &[data-focus-visible] {
    border-color: slateblue;
    box-shadow: 0 0 0 1px slateblue;
  }
}

.react-aria-Menu {
  --highlight-background: slateblue;
  --highlight-foreground: white;
  --separator-color: var(--spectrum-global-color-gray-500);
  --text-color: var(--spectrum-alias-text-color);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);

  max-height: inherit;
  overflow: auto;
  padding: 2px;
  margin: 0;
  min-width: 150px;
  box-sizing: border-box;
  outline: none;

  .react-aria-Section:not(:first-child) {
    margin-top: 12px;
  }

  .react-aria-Header {
    font-size: 1.143rem;
    font-weight: bold;
    padding: 0 0.714rem;
  }

  [role=separator] {
    height: 1px;
    background: var(--separator-color);
    margin: 2px 4px;
  }

  .react-aria-Item {
    margin: 2px;
    padding: 0.286rem 0.571rem;
    border-radius: 6px;
    outline: none;
    cursor: default;
    color: var(--text-color);
    font-size: 1.072rem;
    position: relative;
    display: grid;
    grid-template-areas: "label kbd"
                        "desc  kbd";
    align-items: center;
    column-gap: 20px;

    &[data-focused] {
      background: var(--highlight-background);
      color: var(--highlight-foreground);
    }

    &[aria-disabled] {
      color: var(--text-color-disabled);
    }

    [slot=label] {
      font-weight: bold;
      grid-area: label;
    }

    [slot=description] {
      font-size: small;
      grid-area: desc;
    }

    & kbd {
      grid-area: kbd;
      font-family: monospace;
      text-align: end;
    }

    &[aria-checked] {
      padding-left: 24px;

      &[aria-checked=true]::before {
        content: '✓';
        content: '✓' / '';
        alt: ' ';
        position: absolute;
        left: 4px;
        font-weight: 600;
      }

      &[role=menuitemradio][aria-checked=true]::before {
        content: '●';
        content: '●' / '';
        transform: scale(0.7)
      }
    }
  }
}

.react-aria-Popover {
  --background-color: var(--page-background);
  --border-color: var(--spectrum-global-color-gray-400);

  border: 1px solid var(--border-color);
  box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
  border-radius: 6px;
  background: var(--background-color);
  outline: none;

  &[data-placement=top] {
    --origin: translateY(8px);
  }

  &[data-placement=bottom] {
    --origin: translateY(-8px);
  }

  &[data-entering] {
    animation: slide 200ms;
  }

  &[data-exiting] {
    animation: slide 200ms reverse ease-in;
  }
}

@keyframes slide {
  from {
    transform: var(--origin);
    opacity: 0;
  }

  to {
    transform: translateY(0);
    opacity: 1;
  }
}

@media (forced-colors: active) {
  .react-aria-Menu {
    forced-color-adjust: none;

    --highlight-background: Highlight;
    --highlight-foreground: HighlightText;
    --separator-color: ButtonBorder;
    --text-color: ButtonText;
    --text-color-disabled: GrayText;
  }
}
.react-aria-Button {
  background: var(--spectrum-global-color-gray-50);
  border: 1px solid var(--spectrum-global-color-gray-400);
  color: var(--spectrum-alias-text-color);
  border-radius: 4px;
  appearance: none;
  vertical-align: middle;
  font-size: 1.2rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 0.286rem 0.857rem;

  &[data-pressed] {
    box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
    background: var(--spectrum-global-color-gray-100);
    border-color: var(--spectrum-global-color-gray-500);
  }

  &[data-focus-visible] {
    border-color: slateblue;
    box-shadow: 0 0 0 1px slateblue;
  }
}

.react-aria-Menu {
  --highlight-background: slateblue;
  --highlight-foreground: white;
  --separator-color: var(--spectrum-global-color-gray-500);
  --text-color: var(--spectrum-alias-text-color);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);

  max-height: inherit;
  overflow: auto;
  padding: 2px;
  margin: 0;
  min-width: 150px;
  box-sizing: border-box;
  outline: none;

  .react-aria-Section:not(:first-child) {
    margin-top: 12px;
  }

  .react-aria-Header {
    font-size: 1.143rem;
    font-weight: bold;
    padding: 0 0.714rem;
  }

  [role=separator] {
    height: 1px;
    background: var(--separator-color);
    margin: 2px 4px;
  }

  .react-aria-Item {
    margin: 2px;
    padding: 0.286rem 0.571rem;
    border-radius: 6px;
    outline: none;
    cursor: default;
    color: var(--text-color);
    font-size: 1.072rem;
    position: relative;
    display: grid;
    grid-template-areas: "label kbd"
                        "desc  kbd";
    align-items: center;
    column-gap: 20px;

    &[data-focused] {
      background: var(--highlight-background);
      color: var(--highlight-foreground);
    }

    &[aria-disabled] {
      color: var(--text-color-disabled);
    }

    [slot=label] {
      font-weight: bold;
      grid-area: label;
    }

    [slot=description] {
      font-size: small;
      grid-area: desc;
    }

    & kbd {
      grid-area: kbd;
      font-family: monospace;
      text-align: end;
    }

    &[aria-checked] {
      padding-left: 24px;

      &[aria-checked=true]::before {
        content: '✓';
        content: '✓' / '';
        alt: ' ';
        position: absolute;
        left: 4px;
        font-weight: 600;
      }

      &[role=menuitemradio][aria-checked=true]::before {
        content: '●';
        content: '●' / '';
        transform: scale(0.7)
      }
    }
  }
}

.react-aria-Popover {
  --background-color: var(--page-background);
  --border-color: var(--spectrum-global-color-gray-400);

  border: 1px solid var(--border-color);
  box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
  border-radius: 6px;
  background: var(--background-color);
  outline: none;

  &[data-placement=top] {
    --origin: translateY(8px);
  }

  &[data-placement=bottom] {
    --origin: translateY(-8px);
  }

  &[data-entering] {
    animation: slide 200ms;
  }

  &[data-exiting] {
    animation: slide 200ms reverse ease-in;
  }
}

@keyframes slide {
  from {
    transform: var(--origin);
    opacity: 0;
  }

  to {
    transform: translateY(0);
    opacity: 1;
  }
}

@media (forced-colors: active) {
  .react-aria-Menu {
    forced-color-adjust: none;

    --highlight-background: Highlight;
    --highlight-foreground: HighlightText;
    --separator-color: ButtonBorder;
    --text-color: ButtonText;
    --text-color-disabled: GrayText;
  }
}

Features#


There is no native element to implement a menu in HTML that is widely supported. MenuTrigger and Menu help achieve accessible menu components that can be styled as needed.

  • Keyboard navigation – Menu items can be navigated using the arrow keys, along with page up/down, home/end, etc. Typeahead, auto scrolling, and disabled items are supported as well.
  • Item selection – Single or multiple selection can be optionally enabled.
  • Trigger interactions – Menus can be triggered by pressing with a mouse or touch, or optionally, with a long press interaction. The arrow keys also open the menu with a keyboard, automatically focusing the first or last item accordingly.
  • Accessible – Follows the ARIA menu pattern, with support for items and sections, and slots for label, description, and keyboard shortcut elements within each item for improved screen reader announcement.

Anatomy#


Option 1Option 2Menu itemMenu item keyboard shortcutMenu item labelDescriptionDescriptionOption 3DescriptionMenu item descriptionMenuSECTION TITLESection headingGroupKMenu item descriptionMore ActionsMenu Trigger

A menu trigger consists of a button or other trigger element combined with a popup menu, with a list of menu items or groups inside. Users can click, touch, or use the keyboard on the button to open the menu.

Concepts#

Menu makes use of the following concepts:

Composed components#

A Menu uses the following components, which may also be used standalone or reused in other components.

Button
A button allows a user to perform an action.
Popover
A popover displays content in context with a trigger element.

Props#


NameTypeDefaultDescription
childrenReactNode
triggerMenuTriggerType'press'How the menu is triggered.
isOpenbooleanWhether the overlay is open by default (controlled).
defaultOpenbooleanWhether the overlay is open by default (uncontrolled).
Events
NameTypeDefaultDescription
onOpenChange( (isOpen: boolean )) => voidHandler that is called when the overlay's open state changes.

Button#

A <Button> accepts its contents as children. Other props such as onPress and isDisabled will be set by the MenuTrigger.

Show props
NameTypeDefaultDescription
isDisabledbooleanWhether the button is disabled.
autoFocusbooleanWhether the element should receive focus on render.
type'button''submit''reset''button'The behavior of the button when used in an HTML form.
childrenReactNode( (values: ButtonRenderProps )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: ButtonRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: ButtonRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Events
NameTypeDefaultDescription
onPress( (e: PressEvent )) => voidHandler that is called when the press is released over the target.
onPressStart( (e: PressEvent )) => voidHandler that is called when a press interaction starts.
onPressEnd( (e: PressEvent )) => void

Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target.

onPressChange( (isPressed: boolean )) => voidHandler that is called when the press state changes.
onPressUp( (e: PressEvent )) => void

Handler that is called when a press is released over the target, regardless of whether it started on the target or not.

onFocus( (e: FocusEvent<Target> )) => voidHandler that is called when the element receives focus.
onBlur( (e: FocusEvent<Target> )) => voidHandler that is called when the element loses focus.
onFocusChange( (isFocused: boolean )) => voidHandler that is called when the element's focus status changes.
onKeyDown( (e: KeyboardEvent )) => voidHandler that is called when a key is pressed.
onKeyUp( (e: KeyboardEvent )) => voidHandler that is called when a key is released.
Layout
NameTypeDefaultDescription
slotstringA slot name for the component. Slots allow the component to receive props from a parent component.
Accessibility
NameTypeDefaultDescription
idstringThe element's unique identifier. See MDN.
excludeFromTabOrderboolean

Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available.

aria-expandedboolean'true''false'Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed.
aria-haspopupboolean'menu''listbox''tree''grid''dialog''true''false'Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element.
aria-controlsstringIdentifies the element (or elements) whose contents or presence are controlled by the current element.
aria-pressedboolean'true''false''mixed'Indicates the current "pressed" state of toggle buttons.
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

Popover#

A <Popover> is a container to hold the <Menu>. By default, it has a placement of bottom start within a <MenuTrigger>, but this and other positioning properties may be customized.

Show props
NameTypeDefaultDescription
triggerRefRefObject<Element>

The ref for the element which the popover positions itself with respect to.

When used within a trigger component such as DialogTrigger, MenuTrigger, Select, etc., this is set automatically. It is only required when used standalone.

placementPlacement'bottom'The placement of the element with respect to its anchor element.
containerPaddingnumber12

The placement padding that should be applied between the element and its surrounding container.

offsetnumber0

The additional offset applied along the main axis between the element and its anchor element.

crossOffsetnumber0

The additional offset applied along the cross axis between the element and its anchor element.

shouldFlipbooleantrue

Whether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely.

isNonModalboolean

Whether the popover is non-modal, i.e. elements outside the popover may be interacted with by assistive technologies.

Most popovers should not use this option as it may negatively impact the screen reader experience. Only use with components such as combobox, which are designed to handle this situation carefully.

isKeyboardDismissDisabledbooleanfalse

Whether pressing the escape key to close the popover should be disabled.

Most popovers should not use this option. When set to true, an alternative way to close the popover with a keyboard must be provided.

arrowSizenumber0Cross size of the overlay arrow in pixels.
boundaryElementElementdocument.bodyElement that that serves as the positioning boundary.
scrollRefRefObject<Element>overlayRefA ref for the scrollable region within the overlay.
shouldUpdatePositionbooleantrueWhether the overlay should update its position automatically.
arrowBoundaryOffsetnumber0The minimum distance the arrow's edge should be from the edge of the overlay element.
childrenReactNode( (values: PopoverRenderProps )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: PopoverRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: PopoverRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Layout
NameTypeDefaultDescription
slotstringA slot name for the component. Slots allow the component to receive props from a parent component.
Sizing
NameTypeDefaultDescription
maxHeightnumber

The maxHeight specified for the overlay element. By default, it will take all space up to the current viewport height.

NameTypeDefaultDescription
autoFocusbooleanFocusStrategyWhere the focus should be set.
shouldFocusWrapbooleanWhether keyboard navigation is circular.
itemsIterable<T>Item objects in the collection.
disabledKeysIterable<Key>The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
selectionModeSelectionModeThe type of selection that is allowed in the collection.
disallowEmptySelectionbooleanWhether the collection allows empty selection.
selectedKeys'all'Iterable<Key>The currently selected keys in the collection (controlled).
defaultSelectedKeys'all'Iterable<Key>The initial selected keys in the collection (uncontrolled).
childrenReactNode( (item: object )) => ReactElementThe contents of the collection.
classNamestringThe CSS className for the element.
styleCSSPropertiesThe inline style for the element.
Events
NameTypeDefaultDescription
onAction( (key: Key )) => voidHandler that is called when an item is selected.
onClose() => voidHandler that is called when the menu should close after selecting an item.
onSelectionChange( (keys: Selection )) => anyHandler that is called when the selection changes.
Layout
NameTypeDefaultDescription
slotstringA slot name for the component. Slots allow the component to receive props from a parent component.
Accessibility
NameTypeDefaultDescription
idstringThe element's unique identifier. See MDN.
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

Section#

A <Section> defines the child items for a section within a <Menu>. It may also contain an optional <Header> element. If there is no header, then an aria-label must be provided to identify the section to assistive technologies.

Show props
NameTypeDefaultDescription
valueobjectThe object value that this section represents. When using dynamic collections, this is set automatically.
childrenReactNode( (item: object )) => ReactElementStatic child items or a function to render children.
itemsIterable<T>Item objects in the section.
classNamestringThe CSS className for the element.
styleCSSPropertiesThe inline style for the element.
Accessibility
NameTypeDefaultDescription
idKeyThe unique id of the section.
aria-labelstringAn accessibility label for the section.

A <Header> defines the title for a <Section>. It accepts all DOM attributes.

Item#

An <Item> defines a single item within a <Menu>. If the children are not plain text, then the textValue prop must also be set to a plain text representation, which will be used for autocomplete in the Menu.

Show props
NameTypeDefaultDescription
valueobjectThe object value that this item represents. When using dynamic collections, this is set automatically.
titleReactNodeRendered contents of the item if children contains child items.
textValuestringA string representation of the item's contents, used for features like typeahead.
childItemsIterable<T>A list of child item objects. Used for dynamic collections.
hasChildItemsbooleanWhether this item has children, even if not loaded yet.
childrenReactNode( (values: ItemRenderProps )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: ItemRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: ItemRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Accessibility
NameTypeDefaultDescription
idKeyThe unique id of the item.
aria-labelstringAn accessibility label for this item.

Separator#

A <Separator> can be placed between menu items.

Show props
NameTypeDefaultDescription
orientationOrientation'horizontal'The orientation of the separator.
elementTypestringThe HTML element type that will be used to render the separator.
classNamestringThe CSS className for the element.
styleCSSPropertiesThe inline style for the element.
Layout
NameTypeDefaultDescription
slotstringA slot name for the component. Slots allow the component to receive props from a parent component.
Accessibility
NameTypeDefaultDescription
idstringThe element's unique identifier. See MDN.
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

Styling#


React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin className attribute which can be targeted using CSS selectors. These follow the react-aria-ComponentName naming convention.

.react-aria-Menu {
  /* ... */
}
.react-aria-Menu {
  /* ... */
}
.react-aria-Menu {
  /* ... */
}

A custom className can also be specified on any component. This overrides the default className provided by React Aria with your own.

<Menu className="my-menu">
  {/* ... */}
</Menu>
<Menu className="my-menu">
  {/* ... */}
</Menu>
<Menu className="my-menu">
  {/* ... */}
</Menu>

In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using DOM attributes, which you can target in CSS selectors. These are ARIA attributes wherever possible, or data attributes when a relevant ARIA attribute does not exist. For example:

.react-aria-Item[aria-checked=true] {
  /* ... */
}

.react-aria-Item[data-focused] {
  /* ... */
}
.react-aria-Item[aria-checked=true] {
  /* ... */
}

.react-aria-Item[data-focused] {
  /* ... */
}
.react-aria-Item[aria-checked=true] {
  /* ... */
}

.react-aria-Item[data-focused] {
  /* ... */
}

The className and style props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind.

<Item
  className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'}
>
  Item
</Item>
<Item
  className={({ isSelected }) =>
    isSelected ? 'bg-blue-400' : 'bg-gray-100'}
>
  Item
</Item>
<Item
  className={(
    { isSelected }
  ) =>
    isSelected
      ? 'bg-blue-400'
      : 'bg-gray-100'}
>
  Item
</Item>

Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render a checkmark icon when an item is selected.

<Item>
  {({isSelected}) => (
    <>
      {isSelected && <CheckmarkIcon />}
      Item
    </>
  )}
</Item>
<Item>
  {({isSelected}) => (
    <>
      {isSelected && <CheckmarkIcon />}
      Item
    </>
  )}
</Item>
<Item>
  {(
    { isSelected }
  ) => (
    <>
      {isSelected && (
        <CheckmarkIcon />
      )}
      Item
    </>
  )}
</Item>

The states and selectors for each component used in a Menu are documented below.

The MenuTrigger component does not render any DOM elements (it only passes through its children) so it does not support styling. If you need a wrapper element, add one yourself inside the <MenuTrigger>.

<MenuTrigger>
  <div className="my-menu-trigger">
    {/* ... */}
  </div>
</MenuTrigger>
<MenuTrigger>
  <div className="my-menu-trigger">
    {/* ... */}
  </div>
</MenuTrigger>
<MenuTrigger>
  <div className="my-menu-trigger">
    {/* ... */}
  </div>
</MenuTrigger>

Button#

A Button can be targeted with the .react-aria-Button CSS selector, or by overriding with a custom className. It supports the following states:

NameCSS SelectorDescription
isHovered[data-hovered]Whether the button is currently hovered with a mouse.
isPressed[data-pressed]Whether the button is currently in a pressed state.
isFocused:focusWhether the button is focused, either via a mouse or keyboard.
isFocusVisible[data-focus-visible]Whether the button is keyboard focused.
isDisabled:disabledWhether the button is disabled.

Popover#

The Popover component can be targeted with the .react-aria-Popover CSS selector, or by overriding with a custom className. Note that it renders in a React Portal, so it will not appear as a descendant of the MenuTrigger in the DOM. It supports the following states and render props:

NameCSS SelectorDescription
placement[data-placement="left | right | top | bottom"]The placement of the tooltip relative to the trigger.
isEntering[data-entering]Whether the popover is currently entering. Use this to apply animations.
isExiting[data-exiting]Whether the popover is currently exiting. Use this to apply animations.

A Menu can be targeted with the .react-aria-Menu CSS selector, or by overriding with a custom className.

Section#

A Section can be targeted with the .react-aria-Section CSS selector, or by overriding with a custom className. See sections for examples.

Header#

A Header within a Section can be targeted with the .react-aria-Header CSS selector, or by overriding with a custom className. See sections for examples.

Item#

An Item can be targeted with the .react-aria-Item CSS selector, or by overriding with a custom className. It supports the following states and render props:

NameCSS SelectorDescription
isSelected[aria-checked=true]Whether the item is currently selected.
isHovered[data-hovered]Whether the item is currently hovered with a mouse.
isPressed[data-pressed]Whether the item is currently in a pressed state.
isFocused[data-focused]Whether the item is currently focused.
isFocusVisible[data-focus-visible]Whether the item is currently keyboard focused.
isDisabled[aria-disabled]

Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on disabledKeys and disabledBehavior.

selectionModeThe type of selection that is allowed in the collection.
selectionBehaviorThe selection behavior for the collection.

Items also support two slots: a label, and a description. When provided using the <Text> element, the item will have aria-labelledby and aria-describedby attributes pointing to these slots, improving screen reader announcement. See complex items for an example.

Note that items may not contain interactive children such as buttons, as screen readers will not be able to access them.

Separator#

A Separator can be targeted with the .react-aria-Separator CSS selector, or by overriding with a custom className.

Reusable wrappers#


If you will use a Menu in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency.

This example wraps MenuTrigger and all of its children together into a single component which accepts a label prop and children, which are passed through to the right places. The Item component is also wrapped to apply class names based on the current state, as described above.

import type {ItemProps, MenuProps, MenuTriggerProps} from 'react-aria-components';

interface MyMenuButtonProps<T>
  extends MenuProps<T>, Omit<MenuTriggerProps, 'children'> {
  label?: string;
}

function MyMenuButton<T extends object>(
  { label, children, ...props }: MyMenuButtonProps<T>
) {
  return (
    <MenuTrigger {...props}>
      <Button>{label}</Button>
      <Popover>
        <Menu {...props}>
          {children}
        </Menu>
      </Popover>
    </MenuTrigger>
  );
}

function MyItem(props: ItemProps) {
  return (
    <Item
      {...props}
      className={({ isFocused, isSelected }) =>
        `my-item ${isFocused ? 'focused' : ''}`}
    />
  );
}

<MyMenuButton label="Edit">
  <MyItem>Cut</MyItem>
  <MyItem>Copy</MyItem>
  <MyItem>Paste</MyItem>
</MyMenuButton>
import type {
  ItemProps,
  MenuProps,
  MenuTriggerProps
} from 'react-aria-components';

interface MyMenuButtonProps<T>
  extends MenuProps<T>, Omit<MenuTriggerProps, 'children'> {
  label?: string;
}

function MyMenuButton<T extends object>(
  { label, children, ...props }: MyMenuButtonProps<T>
) {
  return (
    <MenuTrigger {...props}>
      <Button>{label}</Button>
      <Popover>
        <Menu {...props}>
          {children}
        </Menu>
      </Popover>
    </MenuTrigger>
  );
}

function MyItem(props: ItemProps) {
  return (
    <Item
      {...props}
      className={({ isFocused, isSelected }) =>
        `my-item ${isFocused ? 'focused' : ''}`}
    />
  );
}

<MyMenuButton label="Edit">
  <MyItem>Cut</MyItem>
  <MyItem>Copy</MyItem>
  <MyItem>Paste</MyItem>
</MyMenuButton>
import type {
  ItemProps,
  MenuProps,
  MenuTriggerProps
} from 'react-aria-components';

interface MyMenuButtonProps<
  T
> extends
  MenuProps<T>,
  Omit<
    MenuTriggerProps,
    'children'
  > {
  label?: string;
}

function MyMenuButton<
  T extends object
>(
  {
    label,
    children,
    ...props
  }: MyMenuButtonProps<T>
) {
  return (
    <MenuTrigger
      {...props}
    >
      <Button>
        {label}
      </Button>
      <Popover>
        <Menu {...props}>
          {children}
        </Menu>
      </Popover>
    </MenuTrigger>
  );
}

function MyItem(
  props: ItemProps
) {
  return (
    <Item
      {...props}
      className={(
        {
          isFocused,
          isSelected
        }
      ) =>
        `my-item ${
          isFocused
            ? 'focused'
            : ''
        }`}
    />
  );
}

<MyMenuButton label="Edit">
  <MyItem>Cut</MyItem>
  <MyItem>Copy</MyItem>
  <MyItem>
    Paste
  </MyItem>
</MyMenuButton>
Show CSS
.my-item {
  margin: 2px;
  padding: 0.286rem 0.571rem;
  border-radius: 6px;
  outline: none;
  cursor: default;
  color: var(--text-color);
  font-size: 1.072rem;
  position: relative;

  &.focused {
    background: #e70073;
    color: white;
  }
}

@media (forced-colors: active) {
  .my-item.focused {
    background: Highlight;
    color: HighlightText;
  }
}
.my-item {
  margin: 2px;
  padding: 0.286rem 0.571rem;
  border-radius: 6px;
  outline: none;
  cursor: default;
  color: var(--text-color);
  font-size: 1.072rem;
  position: relative;

  &.focused {
    background: #e70073;
    color: white;
  }
}

@media (forced-colors: active) {
  .my-item.focused {
    background: Highlight;
    color: HighlightText;
  }
}
.my-item {
  margin: 2px;
  padding: 0.286rem 0.571rem;
  border-radius: 6px;
  outline: none;
  cursor: default;
  color: var(--text-color);
  font-size: 1.072rem;
  position: relative;

  &.focused {
    background: #e70073;
    color: white;
  }
}

@media (forced-colors: active) {
  .my-item.focused {
    background: Highlight;
    color: HighlightText;
  }
}

Usage#


The following examples show how to use the MyMenuButton component created in the above example.

Dynamic collections#

Menu follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time.

As seen below, an iterable list of options is passed to the Menu using the items prop. Each item accepts an id prop, which is passed to the onSelectionChange handler to identify the selected item. Alternatively, if the item objects contain an id property, as shown in the example below, then this is used automatically and an id prop is not required.

function Example() {
  let items = [
    {id: 1, name: 'New'},
    {id: 2, name: 'Open'},
    {id: 3, name: 'Close'},
    {id: 4, name: 'Save'},
    {id: 5, name: 'Duplicate'},
    {id: 6, name: 'Rename'},
    {id: 7, name: 'Move'}
  ];

  return (
    <MyMenuButton label="Actions" items={items} onAction={alert}>
      {(item) => <Item>{item.name}</Item>}
    </MyMenuButton>
  );
}
function Example() {
  let items = [
    { id: 1, name: 'New' },
    { id: 2, name: 'Open' },
    { id: 3, name: 'Close' },
    { id: 4, name: 'Save' },
    { id: 5, name: 'Duplicate' },
    { id: 6, name: 'Rename' },
    { id: 7, name: 'Move' }
  ];

  return (
    <MyMenuButton
      label="Actions"
      items={items}
      onAction={alert}
    >
      {(item) => <Item>{item.name}</Item>}
    </MyMenuButton>
  );
}
function Example() {
  let items = [
    {
      id: 1,
      name: 'New'
    },
    {
      id: 2,
      name: 'Open'
    },
    {
      id: 3,
      name: 'Close'
    },
    {
      id: 4,
      name: 'Save'
    },
    {
      id: 5,
      name: 'Duplicate'
    },
    {
      id: 6,
      name: 'Rename'
    },
    {
      id: 7,
      name: 'Move'
    }
  ];

  return (
    <MyMenuButton
      label="Actions"
      items={items}
      onAction={alert}
    >
      {(item) => (
        <Item>
          {item.name}
        </Item>
      )}
    </MyMenuButton>
  );
}

Selection#

Menu supports multiple selection modes. By default, selection is disabled, however this can be changed using the selectionMode prop. Use defaultSelectedKeys to provide a default set of selected items (uncontrolled) and selectedKeys to set the selected items (controlled). The value of the selected keys must match the id prop of the items. See the react-stately Selection docs for more details.

Single#

import type {Selection} from 'react-aria-components';

function Example() {
  let [selected, setSelected] = React.useState<Selection>(new Set(['center']));

  return (
    <>
      <MyMenuButton
        label="Align"
        selectionMode="single"
        selectedKeys={selected}
        onSelectionChange={setSelected}
      >
        <Item id="left">Left</Item>
        <Item id="center">Center</Item>
        <Item id="right">Right</Item>
      </MyMenuButton>
      <p>Current selection (controlled): {[...selected].join(', ')}</p>
    </>
  );
}
import type {Selection} from 'react-aria-components';

function Example() {
  let [selected, setSelected] = React.useState<Selection>(
    new Set(['center'])
  );

  return (
    <>
      <MyMenuButton
        label="Align"
        selectionMode="single"
        selectedKeys={selected}
        onSelectionChange={setSelected}
      >
        <Item id="left">Left</Item>
        <Item id="center">Center</Item>
        <Item id="right">Right</Item>
      </MyMenuButton>
      <p>
        Current selection (controlled):{' '}
        {[...selected].join(', ')}
      </p>
    </>
  );
}
import type {Selection} from 'react-aria-components';

function Example() {
  let [
    selected,
    setSelected
  ] = React.useState<
    Selection
  >(new Set(['center']));

  return (
    <>
      <MyMenuButton
        label="Align"
        selectionMode="single"
        selectedKeys={selected}
        onSelectionChange={setSelected}
      >
        <Item id="left">
          Left
        </Item>
        <Item id="center">
          Center
        </Item>
        <Item id="right">
          Right
        </Item>
      </MyMenuButton>
      <p>
        Current selection
        (controlled):
        {' '}
        {[...selected]
          .join(', ')}
      </p>
    </>
  );
}

Multiple#

import type {Selection} from 'react-aria-components';

function Example() {
  let [selected, setSelected] = React.useState<Selection>(
    new Set(['sidebar', 'console'])
  );

  return (
    <>
      <MyMenuButton
        label="View"
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={setSelected}
      >
        <Item id="sidebar">Sidebar</Item>
        <Item id="searchbar">Searchbar</Item>
        <Item id="tools">Tools</Item>
        <Item id="console">Console</Item>
      </MyMenuButton>
      <p>
        Current selection (controlled):{' '}
        {selected === 'all' ? 'all' : [...selected].join(', ')}
      </p>
    </>
  );
}
import type {Selection} from 'react-aria-components';

function Example() {
  let [selected, setSelected] = React.useState<Selection>(
    new Set(['sidebar', 'console'])
  );

  return (
    <>
      <MyMenuButton
        label="View"
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={setSelected}
      >
        <Item id="sidebar">Sidebar</Item>
        <Item id="searchbar">Searchbar</Item>
        <Item id="tools">Tools</Item>
        <Item id="console">Console</Item>
      </MyMenuButton>
      <p>
        Current selection (controlled): {selected === 'all'
          ? 'all'
          : [...selected].join(', ')}
      </p>
    </>
  );
}
import type {Selection} from 'react-aria-components';

function Example() {
  let [
    selected,
    setSelected
  ] = React.useState<
    Selection
  >(
    new Set([
      'sidebar',
      'console'
    ])
  );

  return (
    <>
      <MyMenuButton
        label="View"
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={setSelected}
      >
        <Item id="sidebar">
          Sidebar
        </Item>
        <Item id="searchbar">
          Searchbar
        </Item>
        <Item id="tools">
          Tools
        </Item>
        <Item id="console">
          Console
        </Item>
      </MyMenuButton>
      <p>
        Current selection
        (controlled):
        {' '}
        {selected ===
            'all'
          ? 'all'
          : [...selected]
            .join(', ')}
      </p>
    </>
  );
}

Separators#

Separators may be added between menu items in order to create groupings. See Sections for more on creating labeled groupings.

import {Separator} from 'react-aria-components';

<MyMenuButton label="Actions" onAction={alert}>
  <Item id="new">New…</Item>
  <Item id="open">Open…</Item>
  <Separator />
  <Item id="save">Save</Item>
  <Item id="save-as">Save as…</Item>
  <Item id="rename">Rename…</Item>
  <Separator />
  <Item id="page-setup">Page setup…</Item>
  <Item id="print">Print…</Item>
</MyMenuButton>
import {Separator} from 'react-aria-components';

<MyMenuButton label="Actions" onAction={alert}>
  <Item id="new">New…</Item>
  <Item id="open">Open…</Item>
  <Separator />
  <Item id="save">Save</Item>
  <Item id="save-as">Save as…</Item>
  <Item id="rename">Rename…</Item>
  <Separator />
  <Item id="page-setup">Page setup…</Item>
  <Item id="print">Print…</Item>
</MyMenuButton>
import {Separator} from 'react-aria-components';

<MyMenuButton
  label="Actions"
  onAction={alert}
>
  <Item id="new">
    New…
  </Item>
  <Item id="open">
    Open…
  </Item>
  <Separator />
  <Item id="save">
    Save
  </Item>
  <Item id="save-as">
    Save as…
  </Item>
  <Item id="rename">
    Rename…
  </Item>
  <Separator />
  <Item id="page-setup">
    Page setup…
  </Item>
  <Item id="print">
    Print…
  </Item>
</MyMenuButton>

Sections#

Menu supports sections with headings in order to group items. Sections can be used by wrapping groups of Items in a Section component. A <Header> element may also be included to label the section.

Static items#

import {Section, Header} from 'react-aria-components';

<MyMenuButton label="Actions" onAction={alert}>
  <Section>
    <Header>Styles</Header>
    <Item id="bold">Bold</Item>
    <Item id="underline">Underline</Item>
  </Section>
  <Section>
    <Header>Align</Header>
    <Item id="left">Left</Item>
    <Item id="middle">Middle</Item>
    <Item id="right">Right</Item>
  </Section>
</MyMenuButton>
import {Section, Header} from 'react-aria-components';

<MyMenuButton label="Actions" onAction={alert}>
  <Section>
    <Header>Styles</Header>
    <Item id="bold">Bold</Item>
    <Item id="underline">Underline</Item>
  </Section>
  <Section>
    <Header>Align</Header>
    <Item id="left">Left</Item>
    <Item id="middle">Middle</Item>
    <Item id="right">Right</Item>
  </Section>
</MyMenuButton>
import {
  Header,
  Section
} from 'react-aria-components';

<MyMenuButton
  label="Actions"
  onAction={alert}
>
  <Section>
    <Header>
      Styles
    </Header>
    <Item id="bold">
      Bold
    </Item>
    <Item id="underline">
      Underline
    </Item>
  </Section>
  <Section>
    <Header>
      Align
    </Header>
    <Item id="left">
      Left
    </Item>
    <Item id="middle">
      Middle
    </Item>
    <Item id="right">
      Right
    </Item>
  </Section>
</MyMenuButton>

Dynamic items#

The above example shows sections with static items. Sections can also be populated from a heirarchical data structure. Similarly to the props on Menu, <Section> takes an array of data using the items prop. If the section also has a header, the Collection component can be used to render the child items.

import type {Selection} from 'react-aria-components';
import {Collection} from 'react-aria-components';

function Example() {
  let [selected, setSelected] = React.useState<Selection>(new Set([1,3]));
  let openWindows = [
    {
      name: 'Left Panel',
      id: 'left',
      children: [
        {id: 1, name: 'Final Copy (1)'}
      ]
    },
    {
      name: 'Right Panel',
      id: 'right',
      children: [
        {id: 2, name: 'index.ts'},
        {id: 3, name: 'package.json'},
        {id: 4, name: 'license.txt'}
      ]
    }
  ];

  return (
    <MyMenuButton
      label="Window"
      items={openWindows}
      selectionMode="multiple"
      selectedKeys={selected}
      onSelectionChange={setSelected}>
      {section => (
        <Section>
          <Header>{section.name}</Header>
          <Collection items={section.children}>
            {item => <Item>{item.name}</Item>}
          </Collection>
        </Section>
      )}
    </MyMenuButton>
  );
}
import type {Selection} from 'react-aria-components';
import {Collection} from 'react-aria-components';

function Example() {
  let [selected, setSelected] = React.useState<Selection>(
    new Set([1, 3])
  );
  let openWindows = [
    {
      name: 'Left Panel',
      id: 'left',
      children: [
        { id: 1, name: 'Final Copy (1)' }
      ]
    },
    {
      name: 'Right Panel',
      id: 'right',
      children: [
        { id: 2, name: 'index.ts' },
        { id: 3, name: 'package.json' },
        { id: 4, name: 'license.txt' }
      ]
    }
  ];

  return (
    <MyMenuButton
      label="Window"
      items={openWindows}
      selectionMode="multiple"
      selectedKeys={selected}
      onSelectionChange={setSelected}
    >
      {(section) => (
        <Section>
          <Header>{section.name}</Header>
          <Collection items={section.children}>
            {(item) => <Item>{item.name}</Item>}
          </Collection>
        </Section>
      )}
    </MyMenuButton>
  );
}
import type {Selection} from 'react-aria-components';
import {Collection} from 'react-aria-components';

function Example() {
  let [
    selected,
    setSelected
  ] = React.useState<
    Selection
  >(new Set([1, 3]));
  let openWindows = [
    {
      name: 'Left Panel',
      id: 'left',
      children: [
        {
          id: 1,
          name:
            'Final Copy (1)'
        }
      ]
    },
    {
      name:
        'Right Panel',
      id: 'right',
      children: [
        {
          id: 2,
          name:
            'index.ts'
        },
        {
          id: 3,
          name:
            'package.json'
        },
        {
          id: 4,
          name:
            'license.txt'
        }
      ]
    }
  ];

  return (
    <MyMenuButton
      label="Window"
      items={openWindows}
      selectionMode="multiple"
      selectedKeys={selected}
      onSelectionChange={setSelected}
    >
      {(section) => (
        <Section>
          <Header>
            {section
              .name}
          </Header>
          <Collection
            items={section
              .children}
          >
            {(item) => (
              <Item>
                {item
                  .name}
              </Item>
            )}
          </Collection>
        </Section>
      )}
    </MyMenuButton>
  );
}

Accessibility#

Sections without a title must provide an aria-label for accessibility.

Complex menu items#

By default, menu items that only contain text will be labeled by the contents of the item. The <Text> and <Keyboard> components may be used within a menu item to add a description and keyboard shortcut. These slots improve screen reader announcement, and may also be used for styling and layout purposes.

import {Text, Keyboard} from 'react-aria-components';

<MyMenuButton label="Actions">
  <Item textValue="Copy">
    <Text slot="label">Copy</Text>
    <Text slot="description">Copy the selected text</Text>
    <Keyboard>⌘C</Keyboard>
  </Item>
  <Item textValue="Cut">
    <Text slot="label">Cut</Text>
    <Text slot="description">Cut the selected text</Text>
    <Keyboard>⌘X</Keyboard>
  </Item>
  <Item textValue="Paste">
    <Text slot="label">Paste</Text>
    <Text slot="description">Paste the copied text</Text>
    <Keyboard>⌘V</Keyboard>
  </Item>
</MyMenuButton>
import {Text, Keyboard} from 'react-aria-components';

<MyMenuButton label="Actions">
  <Item textValue="Copy">
    <Text slot="label">Copy</Text>
    <Text slot="description">Copy the selected text</Text>
    <Keyboard>⌘C</Keyboard>
  </Item>
  <Item textValue="Cut">
    <Text slot="label">Cut</Text>
    <Text slot="description">Cut the selected text</Text>
    <Keyboard>⌘X</Keyboard>
  </Item>
  <Item textValue="Paste">
    <Text slot="label">Paste</Text>
    <Text slot="description">Paste the copied text</Text>
    <Keyboard>⌘V</Keyboard>
  </Item>
</MyMenuButton>
import {
  Keyboard,
  Text
} from 'react-aria-components';

<MyMenuButton label="Actions">
  <Item textValue="Copy">
    <Text slot="label">
      Copy
    </Text>
    <Text slot="description">
      Copy the selected
      text
    </Text>
    <Keyboard>
      ⌘C
    </Keyboard>
  </Item>
  <Item textValue="Cut">
    <Text slot="label">
      Cut
    </Text>
    <Text slot="description">
      Cut the selected
      text
    </Text>
    <Keyboard>
      ⌘X
    </Keyboard>
  </Item>
  <Item textValue="Paste">
    <Text slot="label">
      Paste
    </Text>
    <Text slot="description">
      Paste the copied
      text
    </Text>
    <Keyboard>
      ⌘V
    </Keyboard>
  </Item>
</MyMenuButton>

Disabled items#

Menu supports marking items as disabled using the disabledKeys prop. Each key in this list corresponds with the id prop passed to the Item component, or automatically derived from the values passed to the items prop. See Collections for more details.

Disabled items are not focusable or keyboard navigable, and do not trigger onAction or onSelectionChange.

<MyMenuButton label="Actions" onAction={alert} disabledKeys={['paste']}>
  <Item id="copy">Copy</Item>
  <Item id="cut">Cut</Item>
  <Item id="paste">Paste</Item>
</MyMenuButton>
<MyMenuButton
  label="Actions"
  onAction={alert}
  disabledKeys={['paste']}
>
  <Item id="copy">Copy</Item>
  <Item id="cut">Cut</Item>
  <Item id="paste">Paste</Item>
</MyMenuButton>
<MyMenuButton
  label="Actions"
  onAction={alert}
  disabledKeys={[
    'paste'
  ]}
>
  <Item id="copy">
    Copy
  </Item>
  <Item id="cut">
    Cut
  </Item>
  <Item id="paste">
    Paste
  </Item>
</MyMenuButton>

Controlled open state#

The open state of the menu can be controlled via the defaultOpen and isOpen props.

function Example() {
  let [open, setOpen] = React.useState(false);

  return (
    <>
      <p>Menu is {open ? 'open' : 'closed'}</p>
      <MyMenuButton
        label="View"
        isOpen={open}
        onOpenChange={setOpen}>
        <Item id="side">Side bar</Item>
        <Item id="options">Page options</Item>
        <Item id="edit">Edit Panel</Item>
      </MyMenuButton>
    </>
  );
}
function Example() {
  let [open, setOpen] = React.useState(false);

  return (
    <>
      <p>Menu is {open ? 'open' : 'closed'}</p>
      <MyMenuButton
        label="View"
        isOpen={open}
        onOpenChange={setOpen}>
        <Item id="side">Side bar</Item>
        <Item id="options">Page options</Item>
        <Item id="edit">Edit Panel</Item>
      </MyMenuButton>
    </>
  );
}
function Example() {
  let [open, setOpen] =
    React.useState(
      false
    );

  return (
    <>
      <p>
        Menu is {open
          ? 'open'
          : 'closed'}
      </p>
      <MyMenuButton
        label="View"
        isOpen={open}
        onOpenChange={setOpen}
      >
        <Item id="side">
          Side bar
        </Item>
        <Item id="options">
          Page options
        </Item>
        <Item id="edit">
          Edit Panel
        </Item>
      </MyMenuButton>
    </>
  );
}

Advanced customization#


Composition#

If you need to customize one of the components within a MenuTrigger, such as Button or Menu, in many cases you can create a wrapper component. This lets you customize the props passed to the component.

function MyMenu(props) {
  return <Menu {...props} className="my-menu" />
}
function MyMenu(props) {
  return <Menu {...props} className="my-menu" />
}
function MyMenu(props) {
  return (
    <Menu
      {...props}
      className="my-menu"
    />
  );
}

Hooks#

If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. MenuTrigger and Menu send props to their child elements via public React contexts for each component. You can use this context to implement replacements for any component, using hooks from react-aria. This allows you to replace only the components you need to customize, and keep using the others.

Menu uses the following hooks. See the linked documentation for more details.

To replace a component used within a Menu, create your own component and use the useContextProps hook to merge the local props and ref with the ones sent via context by Menu. This example shows how you could implement a custom Menu component that works with MenuTrigger.

import {MenuContext, useContextProps} from 'react-aria-components';
import {useMenu} from 'react-aria';
import {useTreeState} from 'react-stately';

function MyMenu(props) {
  // Merge local props and ref with props from context.
  let ref = React.useRef();
  [props, ref] = useContextProps(props, ref, MenuContext);

  let state = useTreeState(props);
  let {menuProps} = useMenu(props, state, ref);

  // Render stuff
  // ...
}
import {
  MenuContext,
  useContextProps
} from 'react-aria-components';
import {useMenu} from 'react-aria';
import {useTreeState} from 'react-stately';

function MyMenu(props) {
  // Merge local props and ref with props from context.
  let ref = React.useRef();
  [props, ref] = useContextProps(props, ref, MenuContext);

  let state = useTreeState(props);
  let { menuProps } = useMenu(props, state, ref);

  // Render stuff
  // ...
}
import {
  MenuContext,
  useContextProps
} from 'react-aria-components';
import {useMenu} from 'react-aria';
import {useTreeState} from 'react-stately';

function MyMenu(props) {
  // Merge local props and ref with props from context.
  let ref = React
    .useRef();
  [props, ref] =
    useContextProps(
      props,
      ref,
      MenuContext
    );

  let state =
    useTreeState(props);
  let { menuProps } =
    useMenu(
      props,
      state,
      ref
    );

  // Render stuff
  // ...
}

This also works the other way. If you need to customize MenuTrigger itself, but want to reuse the components it contains, you can do so by providing the necessary contexts. The Provider component is an easier way to send multiple contexts at once.

import {ButtonContext, MenuContext, PopoverContext, Provider} from 'react-aria-components';
import {useMenuTrigger} from 'react-aria';

function MyMenuTrigger(props) {
  // ...
  let ref = useRef(null);
  let {
    menuTriggerProps,
    menuProps
  } = useMenuTrigger({/* ... */});

  return (
    <Provider
      values={[
        [ButtonContext, { ...menuTriggerProps, ref }],
        [PopoverContext, { state, triggerRef: ref }],
        [MenuContext, menuProps]
      ]}
    >
      {props.children}
    </Provider>
  );
}
import {
  ButtonContext,
  MenuContext,
  PopoverContext,
  Provider
} from 'react-aria-components';
import {useMenuTrigger} from 'react-aria';

function MyMenuTrigger(props) {
  // ...
  let ref = useRef(null);
  let {
    menuTriggerProps,
    menuProps
  } = useMenuTrigger({/* ... */});

  return (
    <Provider
      values={[
        [ButtonContext, { ...menuTriggerProps, ref }],
        [PopoverContext, { state, triggerRef: ref }],
        [MenuContext, menuProps]
      ]}
    >
      {props.children}
    </Provider>
  );
}
import {
  ButtonContext,
  MenuContext,
  PopoverContext,
  Provider
} from 'react-aria-components';
import {useMenuTrigger} from 'react-aria';

function MyMenuTrigger(
  props
) {
  // ...
  let ref = useRef(null);
  let {
    menuTriggerProps,
    menuProps
  } = useMenuTrigger({
    /* ... */
  });

  return (
    <Provider
      values={[
        [ButtonContext, {
          ...menuTriggerProps,
          ref
        }],
        [
          PopoverContext,
          {
            state,
            triggerRef:
              ref
          }
        ],
        [
          MenuContext,
          menuProps
        ]
      ]}
    >
      {props.children}
    </Provider>
  );
}