dotUI
dotUI
beta
  1. Components
  2. Menus and selection
  3. Menu

Menu

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

<MenuRoot>
  <Button><MenuIcon /></Button>
  <Menu>
    <MenuItem>Account settings</MenuItem>
    <MenuItem>Create team</MenuItem>
    <MenuItem>Command menu</MenuItem>
    <MenuItem>Log out</MenuItem>
  </Menu>
</MenuRoot>

Installation

Install the following dependencies:

npm install react-aria-components

Copy and paste the following code into your project.

"use client";

import * as React from "react";
import {
  composeRenderProps,
  Menu as AriaMenu,
  MenuTrigger as AriaMenuTrigger,
  MenuItem as AriaMenuItem,
  SubmenuTrigger as AriaSubmenuTrigger,
  type MenuItemProps as AriaMenuItemProps,
  type MenuProps as AriaMenuProps,
  type MenuTriggerProps as AriaMenuTriggerProps,
} from "react-aria-components";
import { tv, type VariantProps } from "tailwind-variants";
import { CheckIcon, ChevronRightIcon } from "@/lib/icons";
import { Kbd } from "./kbd";
import { Overlay, type OverlayProps } from "./overlay";
import { Text } from "./text";

const menuStyles = tv({
  base: [
    "outline-none rounded-[inherit] max-h[inherit] p-1",
    "group-data-[type=drawer]/overlay:p-2",
    "[&_.separator]:-mx-1 [&_.separator]:my-1 [&_.separator]:w-auto",
  ],
});

const menuItemStyles = tv({
  base: [
    "flex cursor-pointer items-center rounded-sm px-3 py-1.5 text-sm outline-none transition-colors disabled:pointer-events-none focus:bg-bg-inverse/10 disabled:text-fg-disabled",
    "selection-single:pl-0 selection-multiple:pl-0",
    "group-data-[type=drawer]/overlay:text-md group-data-[type=drawer]/overlay:py-3",
    "group-data-[type=modal]/overlay:text-md group-data-[type=modal]/overlay:py-2",
    "[&_svg]:size-4",
  ],
  variants: {
    variant: {
      default: "text-fg",
      success: "text-fg-success",
      warning: "text-fg-warning",
      accent: "text-fg-accent",
      danger: "text-fg-danger",
    },
  },
  defaultVariants: {
    variant: "default",
  },
});

type MenuRootProps = AriaMenuTriggerProps;
const MenuRoot = (props: MenuRootProps) => {
  return <AriaMenuTrigger {...props} />;
};

type MenuProps<T> = MenuContentProps<T> & {
  type?: OverlayProps["type"];
  mobileType?: OverlayProps["mobileType"];
  mediaQuery?: OverlayProps["mediaQuery"];
  placement?: OverlayProps["placement"];
};
const Menu = <T extends object>({
  placement,
  type = "popover",
  mobileType = "drawer",
  mediaQuery,
  ...props
}: MenuProps<T>) => {
  return (
    <Overlay type={type} mobileType={mobileType} mediaQuery={mediaQuery} placement={placement}>
      <MenuContent {...props} />
    </Overlay>
  );
};

type MenuContentProps<T> = AriaMenuProps<T>;
const MenuContent = <T extends object>({ className, ...props }: MenuContentProps<T>) => {
  return <AriaMenu className={menuStyles({ className })} {...props} />;
};

const MenuSub = AriaSubmenuTrigger;

interface MenuItemProps<T>
  extends Omit<AriaMenuItemProps<T>, "className">,
    VariantProps<typeof menuItemStyles> {
  prefix?: React.ReactNode;
  suffix?: React.ReactNode;
  label?: string;
  description?: string;
  shortcut?: string;
  className?: string;
}
const MenuItem = <T extends object>({
  className,
  label,
  description,
  prefix,
  suffix,
  shortcut,
  variant,
  ...props
}: MenuItemProps<T>) => {
  return (
    <AriaMenuItem className={menuItemStyles({ className, variant })} {...props}>
      {composeRenderProps(props.children, (children, { selectionMode, isSelected, hasSubmenu }) => (
        <>
          {selectionMode !== "none" && (
            <span className="flex w-8 items-center justify-center">
              {isSelected && <CheckIcon aria-hidden className="size-4 text-fg-accent" />}
            </span>
          )}
          {prefix}
          <span className="flex items-center gap-2">
            <span className="flex flex-1 flex-col">
              {label && <Text slot="label">{label}</Text>}
              {description && <Text slot="description">{description}</Text>}
              {children}
            </span>
            {suffix}
            {shortcut && <Kbd>{shortcut}</Kbd>}
            {hasSubmenu && <ChevronRightIcon aria-hidden className="size-4" />}
          </span>
        </>
      ))}
    </AriaMenuItem>
  );
};

export type { MenuRootProps, MenuProps };
export { MenuRoot, Menu, MenuItem, MenuContent, MenuSub };

Update the import paths to match your project setup.

Usage

Use Menu to display a list of actions or options that a user can choose.

Overlay type

Use the type prop to set the overlay type. The default is "popover". You can also set the mobile type using the mobileType prop ("drawer" by default).

<MenuRoot>
  <Button><MenuIcon /></Button>
  <Menu type={type} mobileType={mobileType}>
    <MenuItem>Account settings</MenuItem>
    <MenuItem>Create team</MenuItem>
    <MenuItem>Command menu</MenuItem>
    <MenuItem>Log out</MenuItem>
  </Menu>
</MenuRoot>

Position

Use the placement prop to set the position of the menu relative to the trigger button.

<MenuRoot>
  <Button>
    <MenuIcon />
  </Button>
  <Menu placement={placement}>
    <MenuItem>Account settings</MenuItem>
    <MenuItem>Create team</MenuItem>
    <MenuItem>Log out</MenuItem>
  </Menu>
</MenuRoot>

Selection mode

Single selection

Set the selectionMode prop to single for single selection.

const [selected, setSelected] = React.useState<Selection>(new Set(["center"]));
return (
  <MenuRoot>
    <Button>Align</Button>
    <Menu selectionMode="single" selectedKeys={selected} onSelectionChange={setSelected}>
      <MenuItem id="start">Start</MenuItem>
      <MenuItem id="center">Center</MenuItem>
      <MenuItem id="end">End</MenuItem>
    </Menu>
  </MenuRoot>
);

Multiple selection

Set the selectionMode prop to multiple for multiple selection.

<MenuRoot>
  <Button>Panels</Button>
  <Menu selectionMode="multiple" defaultSelectedKeys={["sidebar", "searchbar", "console"]}>
    <MenuItem id="sidebar">Sidebar</MenuItem>
    <MenuItem id="searchbar">Searchbar</MenuItem>
    <MenuItem id="tools">Tools</MenuItem>
    <MenuItem id="console">Console</MenuItem>
  </Menu>
</MenuRoot>

Variant

Use the variant prop to set the visual style of the menu item.

<MenuRoot>
  <Button><MenuIcon /></Button>
  <Menu>
    <MenuItem>Account settings</MenuItem>
    <MenuItem>Create team</MenuItem>
    <MenuItem>Command menu</MenuItem>
    <MenuItem variant="danger">Delete</MenuItem>
  </Menu>
</MenuRoot>

Label and description

MenuItems also support the "label" and "description" slots to separate primary and secondary content. You can also use the Text component to do the same behaviour.

<MenuRoot>
  <Button><MenuIcon /></Button>
  <Menu>
    <MenuItem label="New file" description="Create a new file" />
    <MenuItem label="Copy link" description="Copy the file link" />
    <MenuItem label="Edit file" description="Allows you to edit the file" />
  </Menu>
</MenuRoot>

Prefix and suffix

To add additional context for the menu item, such as icons, use the prefix and suffix props.

<MenuRoot>
  <Button><MenuIcon /></Button>
  <Menu>
    <MenuItem label="New file" description="Create a new file" prefix={<PlusSquareIcon />} />
    <MenuItem label="Copy link" description="Copy the file link" prefix={<CopyIcon />} />
    <MenuItem label="Edit file" description="Allows you to edit the file" prefix={<SquarePenIcon />} />
  </Menu>
</MenuRoot>

Keyboard shortcut

Use the shortcut prop to add a keyboard shortcut to a menu item, or use the Keyboard component.

<MenuRoot>
  <Button><MenuIcon /></Button>
  <Menu>
    <MenuItem shortcut="⌘N">New file</MenuItem>
    <MenuItem shortcut="⌘C">Copy link</MenuItem>
    <MenuItem shortcut="⌘⇧E">Edit file</MenuItem>
  </Menu>
</MenuRoot>

Alternatively, items may be links to another page or website. This can be achieved by passing the href prop to the <MenuItem> component. Link items in a menu are not selectable.

Disabled

A MenuItem can be disabled with the isDisabled prop.

<MenuRoot>
  <Button><MenuIcon /></Button>
  <Menu>
    <MenuItem>Account settings</MenuItem>
    <MenuItem prefix={<PlusSquareIcon />} isDisabled>
      Create team
    </MenuItem>
    <MenuItem>Log out</MenuItem>
  </Menu>
</MenuRoot>

Long press

By default, Menu opens by pressing the trigger element or activating it via the Space or Enter keys. This behavior can be changed by providing "longPress" to the trigger prop.

<MenuRoot trigger="longPress">
  <Button><MenuIcon /></Button>
  <Menu>
    <MenuItem>Account settings</MenuItem>
    <MenuItem>Create team</MenuItem>
    <MenuItem>Log out</MenuItem>
  </Menu>
</MenuRoot>

Sections

Menu supports sections with headings in order to group items. Sections can be used by wrapping groups of MenuItems in a Section component.
A Header element may also be included to label the section or using the title prop on the Section component.

<MenuRoot>
  <Button><MenuIcon /></Button>
  <Menu>
    <Section title="Notifications">
      <MenuItem>Push notifications</MenuItem>
      <MenuItem>Badges</MenuItem>
    </Section>
    <Separator />
    <Section title="Panels">
      <MenuItem id="console">Console</MenuItem>
      <MenuItem>Search</MenuItem>
    </Section>
  </Menu>
</MenuRoot>

Separator

Separators may be added between menu items or sections in order to create non-labeled groupings.

<MenuRoot>
  <Button><MenuIcon /></Button>
  <Menu>
    <MenuItem>New...</MenuItem>
    <MenuItem>Badges</MenuItem>
    <Separator />
    <MenuItem>Save</MenuItem>
    <MenuItem>Save as...</MenuItem>
    <MenuItem>Rename...</MenuItem>
    <Separator />
    <MenuItem>Page setup…</MenuItem>
    <MenuItem>Print…</MenuItem>
  </Menu>
</MenuRoot>

Submenus can be created by wrapping a MenuItem and a Menu in a MenuSub.

<MenuRoot>
  <Button><MenuIcon /></Button>
  <Menu>
    <MenuItem>Account settings</MenuItem>
    <MenuSub>
      <MenuItem>Invite users</MenuItem>
      <Menu>
        <MenuItem>SMS</MenuItem>
        <MenuItem>Twitter</MenuItem>
        <MenuSub>
          <MenuItem>Email</MenuItem>
          <Menu>
            <MenuItem>Work</MenuItem>
            <MenuItem>Personal</MenuItem>
          </Menu>
        </MenuSub>
      </Menu>
    </MenuSub>
  </Menu>
</MenuRoot>

Controlled

The open state can be controlled via isOpen and onOpenChange props.

const [isOpen, setOpen] = React.useState(false);
return (
  <MenuRoot isOpen={isOpen} onOpenChange={setOpen}>
    <Button><MenuIcon /></Button>
    <Menu>
      <MenuItem>Account settings</MenuItem>
      <MenuItem>Create team</MenuItem>
      <MenuItem>Log out</MenuItem>
    </Menu>
  </MenuRoot>
)

Composition

If you need to customize things further, you can drop down to the composition level.

<MenuRoot>
  <Button>
    <MenuIcon />
  </Button>
  <Overlay type="popover" mobileType="drawer">
    <MenuContent>
      <MenuItem>Account settings</MenuItem>
      <MenuItem>Create team</MenuItem>
      <MenuItem>Command menu</MenuItem>
      <MenuItem>Log out</MenuItem>
    </MenuContent>
  </Overlay>
</MenuRoot>

API Reference

PropTypeDefaultDescription
children*
React.ReactNode
-
trigger
'press' | 'longPress'
'press'
How the menu is triggered.
isOpen
boolean
-
Whether the overlay is open by default (controlled).
defaultOpen
boolean
-
Whether the overlay is open by default (uncontrolled).
EventTypeDescription
onOpenChange
(isOpen: boolean) => void
Handler that is called when the overlay's open state changes.
PropTypeDefaultDescription
type
"popover" | "modal" | "drawer"
-
The overlay type.
mobileType
"popover" | "modal" | "drawer"
-
The overlay type for mobile screens.
mediaQuery
string
"(max-width: 640px)"
The media query that determines when the mobile type is used.
placement
Placement
-
The position of the menu relative to the trigger button.
autoFocus
boolean | 'first' | 'last'
-
Where the focus should be set.
shouldFocusWrap
boolean
-
Whether keyboard navigation is circular.
items
Iterable<T>
-
Item objects in the collection.
disabledKeys
Iterable<Key>
-
The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
selectionMode
'none' | 'single' | 'multiple'
'none'
The type of selection that is allowed in the collection.
disallowEmptySelection
boolean
-
Whether 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).
dependencies
any[]
-
Values that should invalidate the item cache when using dynamic collections.
children
ReactNode | (item: object) => ReactNode
-
The contents of the collection.
className
string
-
The CSS className for the element.
style
CSSProperties
-
The inline style for the element.
EventTypeDescription
onAction
(key: Key) => void
Handler that is called when an item is selected.
onClose
() => void
Handler that is called when the menu should close after selecting an item.
onSelectionChange
(keys: Selection) => void
Handler that is called when the selection changes.
onScroll
(e: UIEvent<Element>) => void
Handler that is called when a user scrolls.
PropTypeDefaultDescription
id
Key
-
The unique id of the item.
variant
'default' | 'success' | 'warning' | 'danger' | 'accent'
"default"
The visual style of the menu item.
value
object
-
The object value that this item represents. When using dynamic collections, this is set automatically.
textValue
string
-
A string representation of the item's contents, used for features like typeahead.
isDisabled
boolean
-
Whether the item is disabled.
children
ReactNode | (values: MenuItemRenderProps & {defaultChildren: ReactNode | undefined}) => ReactNode
-
The children of the component. A function may be provided to alter the children based on component state.
className
string
-
The CSS className for the element.
style
CSSProperties | (values: MenuItemRenderProps & {defaultStyle: CSSProperties}) => CSSProperties
-
The inline style for the element. A function may be provided to compute the style based on component state.
href
Href
-
A URL to link to.
hrefLang
string
-
Hints at the human language of the linked URL.
target
HTMLAttributeAnchorTarget
-
The target window for the link.
rel
string
-
The relationship between the linked resource and the current page.
download
boolean | string
-
Causes the browser to download the linked URL. A string may be provided to suggest a file name.
ping
string
-
A space-separated list of URLs to ping when the link is followed.
referrerPolicy
HTMLAttributeReferrerPolicy
-
How much of the referrer to send when following the link.
routerOptions
RouterOptions
-
Options for the configured client side router.
EventTypeDescription
onAction
() => void
Handler that is called when the item is selected.
onHoverStart
(e: HoverEvent) => void
Handler that is called when a hover interaction starts.
onHoverEnd
(e: HoverEvent) => void
Handler that is called when a hover interaction ends.
onHoverChange
(isHovering: boolean) => void
Handler that is called when the hover state changes.
PropTypeDefaultDescription
children*
React.Element[]
-
The contents of the SubmenuTrigger. The first child should be an Item (the trigger) and the second child should be the Popover (for the submenu).
delay
number
200
The delay time in milliseconds for the submenu to appear after hovering over the trigger.

Accessibility

Keyboard interactions

KeyDescription
Space Enter
When focus is on the Trigger, opens the menu and focuses the first item. When focus is on an item, activates the focused item.
ArrowDown
When focus is on the trigger, opens the menu. When focus is on an item, moves focus to the next item.
ArrowUp
When focus is on the trigger, opens the menu. When focus is on an item, moves focus to the previous item.
ArrowRight
When focus is on submenu trigger, opens the submenu.
ArrowLeft
When focus is on submenu trigger, closes the submenu.
Esc
When menu is open, closes the menu and moves focus to the Trigger button.

Built by mehdibha. The source code is available on GitHub.