Menu
An accessible dropdown menu for the common dropdown menu button design pattern. Menu uses roving tabIndex for focus management.
React
Hover Design System exports the following components for rendering menus.
import { Menu, MenuItem, MenuItemOption, MenuGroup, MenuOptionGroup, MenuDivider,} from '@hoverinc/design-system-react-web';
| Component | Description |
|---|---|
Menu | The wrapper component provides context, state, and focus management, requires a trigger prop. |
MenuItem | The trigger that handles menu selection. Must be a direct child of a MenuList. |
MenuGroup | A wrapper to group related menu items. |
MenuDivider | A visual separator for menu items and groups. |
MenuOptionGroup | A wrapper for checkable menu items (radio and checkbox). |
MenuItemOption | The checkable menu item, to be used with MenuOptionGroup. |
Usage
Trigger
The trigger prop accepts any system component. If you're passing a custom
component that composes a system component, it must accept a ref.
A custom component needs to accept a ref so that the menu list can be
positioned correctly. You can use forwardRef from
@hoverinc/design-system-react-web to supply the ref along with being able
to use system props. Without a ref, the menu list will render in an undefined
position.
Menu container styles
Use menuListProps to pass props to the div that contains MenuItems. This
container composes Box so you can pass all Box props to change its style.
Use with caution
Changes to the container should be approved by design and built into the theme styles for this component whenever possible. If there are changes / variants you need, please raise in the #design-system Slack channel first.
Accessing the internal state
To access the internal state of the Menu, use a function as children or
trigger (commonly known as a render prop). You'll get access to the internal
state isOpen and method onClose.
Letter navigation
When focus is on the trigger or within the list and you type a letter key, a
search begins. Focus will move to the first MenuItem that starts with the
letter you typed.
Try it out
Open the menu, try and type any letter, (say, S) to see the focus movement.
Example with images
Adding icons and commands
You can add icon to each MenuItem by passing the icon prop. To add a
commands (or hotkeys) to menu items, you can use the command prop.
Lazily mounting items
By default, the Menu component renders all children of MenuList to the DOM,
meaning that invisible menu items are still rendered but are hidden by styles.
If you want to defer rendering of each children of the menu list until that menu
is open, you can use the isLazy prop. This is useful if your Menu needs to
be extra performant, or make network calls on mount that should only happen when
the component is displayed.
Rendering the Menu in a Portal
To render menus in a Portal, pass the inPortal
prop.
Info
These live code blocks hide overflow, so we're setting inPortal on all the
examples
Groups
To group related MenuItems, use the MenuGroup component and pass it a
title for the group name.
Items as links
To render a MenuItem as a link, use the attributes as and href.
Option groups
You can compose a menu for table headers to help with sorting and filtering
options. Use the MenuOptionGroup and MenuItemOption components.
<Menu inPortal trigger={<Button iconBefore={iconFilter}>Filter</Button>}> <MenuOptionGroup defaultValue="asc" title="Order" type="radio"> <MenuItemOption value="asc">Ascending</MenuItemOption> <MenuItemOption value="desc">Descending</MenuItemOption> </MenuOptionGroup> <MenuDivider /> <MenuOptionGroup title="Country" type="checkbox"> <MenuItemOption value="email">Email</MenuItemOption> <MenuItemOption value="phone">Phone</MenuItemOption> <MenuItemOption value="country">Country</MenuItemOption> </MenuOptionGroup></Menu>
With arrow
Set hasArrow to display an arrow pointing from the menu to the trigger.
React Native
Menu is built with Tamagui and provides a dropdown interface with trigger, dropdown, items, and dividers for iOS and Android platforms.
Usage
import { iconChevronDown, iconTrash } from '@hoverinc/icons/native';import React, { useState } from 'react';import { Heading, Icon, Menu, XStack,} from '@hoverinc/design-system-react-native';const App = () => { const [open, setOpen] = useState(false); return ( <Menu onOpenChange={() => setOpen(!open)} open={open} placement="bottom-end" > <Menu.Trigger> <XStack alignItems="center" gap="$100" pressStyle={{ opacity: 0.5 }}> <Heading size="xs">Homeowner</Heading> <Icon icon={iconChevronDown} size="tiny" /> </XStack> </Menu.Trigger> <Menu.Dropdown> <Menu.Item isDisabled>Invited to scan</Menu.Item> <Menu.Divider /> <Menu.Item isSelectable isSelected> Selected Item </Menu.Item> <Menu.Item onPress={() => setOpen(false)}>Send reminder</Menu.Item> <Menu.Item onPress={() => setOpen(false)}>Copy link</Menu.Item> <Menu.Item color="$dangerSecondary" iconAfter={iconTrash} onPress={() => setOpen(false)} > Sign Out </Menu.Item> </Menu.Dropdown> </Menu> );};
Components
The Menu component exports the following sub-components:
Menuโ The main wrapper component that provides context and state managementMenu.Triggerโ The trigger element that opens the menu (supports scope prop)Menu.Dropdownโ The container for menu itemsMenu.Itemโ Individual menu items with support for selection (isSelectable,isSelected), disabled state, icons, and custom colorsMenu.Dividerโ Visual separator between menu items
Menu Item States
Menu items support several states and configurations:
// Basic menu item<Menu.Item>Basic Item</Menu.Item>// Selectable state (shows checkmark when selected)<Menu.Item isSelectable>Selectable Item</Menu.Item>// Selected state (requires isSelectable to be true)<Menu.Item isSelectable isSelected>Selected Item</Menu.Item>// Disabled state<Menu.Item isDisabled>Disabled Item</Menu.Item>// With icon after<Menu.Item iconAfter={iconChevronRight}>Item with Icon</Menu.Item>// With custom color<Menu.Item color="$dangerSecondary">Danger Item</Menu.Item>
Selection Behavior
Menu items support two related props for selection:
isSelectable: Makes the item selectable (shows checkmark icon when selected)isSelected: Marks the item as currently selected (requiresisSelectableto betrue)
When isSelectable is true, the item will:
- Show a checkmark icon when
isSelectedistrue - Have different padding to accommodate the checkmark
- Support selection state styling
// โ
Valid - selectable but not selected<Menu.Item isSelectable>Option 1</Menu.Item>// โ
Valid - selectable and selected<Menu.Item isSelectable isSelected>Selected Option</Menu.Item>// โ Invalid - selected but not selectable (TypeScript error)<Menu.Item isSelected>Selected Option</Menu.Item>
Placement
The menu can be positioned relative to its trigger using the placement prop:
<Menu placement="bottom-start">...</Menu><Menu placement="top-end">...</Menu><Menu placement="left">...</Menu><Menu placement="right">...</Menu>
Controlled vs Uncontrolled
The Menu component supports both controlled and uncontrolled usage:
// Controlledconst [open, setOpen] = useState(false);<Menu open={open} onOpenChange={setOpen}>...</Menu>// Uncontrolled<Menu defaultOpen={false}>...</Menu>
Scoped Menus
For complex applications with multiple menus, you can use scoped menus to isolate menu behavior:
<Menu scope="myScope"> <Menu.Trigger scope="myScope"> <XStack alignItems="center" gap="$100"> <Heading size="xs">Scoped Menu</Heading> <Icon icon={iconChevronDown} size="tiny" /> </XStack> </Menu.Trigger> <Menu.Dropdown> <Menu.Item>Scoped Item 1</Menu.Item> <Menu.Item>Scoped Item 2</Menu.Item> </Menu.Dropdown></Menu>
Accessibility
| Keyboard Interaction | Description |
|---|---|
Enter or Space | When MenuButton receives focus, opens the menu and places focus on the first menu item. |
ArrowDown | When MenuButton receives focus, opens the menu and moves focus to the first menu item. |
ArrowUp | When MenuButton receives focus, opens the menu and moves focus to the last menu item. |
Escape | When the menu is open, closes the menu and sets focus to the MenuButton. |
Tab | no effect |
Home | When the menu is open, moves focus to the first item. |
End | When the menu is open, moves focus to the last item. |
A-Z or a-z | When the menu is open, moves focus to the next menu item with a label that starts with the typed character if such an menu item exists. |
ARIA attributes
| Component | Aria Attribute | Default | Description |
|---|---|---|---|
| MenuButton | role | button | |
aria-haspopup | menu | ||
aria-expanded | true | true when the menu is displayed | |
aria-controls | Set to the id of the MenuList | ||
| MenuList | role | menu | |
aria-orientation | vertical | ||
| MenuItem | role | menuitem | menuitem, menuitemradio, or menuitemcheckbox |