Skip to content

Menu

An accessible dropdown menu for the common dropdown menu button design pattern. Menu uses roving tabIndex for focus management.

Menu is based on The Chakra UI Menu and its sub components.

  • <Menu> The wrapper component provides context, state, and focus management.
    • <MenuButton> The trigger for the menu list. Must be a direct child of <Menu>.
    • <MenuIconButton> Custom trigger for the menu list based on IconButton. Must be a direct child of <Menu>.
    • <MenuBoxButton> Custom trigger for the menu list based on Box. Must be a direct child of <Menu>.
    • <MenuList> The wrapper for the menu items. Must be a direct child of <Menu>.
      • <MenuItem> List item within menu. Must be a direct child of a <MenuList>.
      • <MenuGroup> A parent MenuItem is used to group and menu items or nested menu groups. Supports three levels of nested groups. Must be a direct child of a <MenuList>.
      • <MenuDivider> A visual separator for menu items and groups.

The <Menu>-component is the outmost component of all, it does not render any DOM-node, but provides context to all modal subcomponents within. It should always contain a <MenuList> and <MenuButton>/<MenuBoxButton>/<MenuIconButton>.

<MenuButton> handles the opening and closing of the menu. Use <MenuIconButton> or <MenuBoxButton> if you don’t want the Button-component specifically to be the open/close element.

You can use the same properties on <MenuButton> as <Button>. The same goes for <MenuBoxButton> and <MenuIconButton> where you can use the <Box> and <IconButton> component properties, respectively.

import { Menu, MenuButton, MenuItem, MenuList } from "@caspeco/casper-ui-library";
export default function Demo() {
return (
<Menu onSelect={console.log}>
<MenuButton>Settings</MenuButton>
<MenuList>
<MenuItem value="profile">Profile</MenuItem>
<MenuItem value="preferences">Preferences</MenuItem>
</MenuList>
</Menu>
);
}

By default, the menu closes when an item is selected. You can override this behavior by setting the closeOnSelect-property to false.

import { Menu, MenuButton, MenuItem, MenuList } from "@caspeco/casper-ui-library";
export default function Demo() {
return (
<Menu closeOnSelect={false}>
<MenuButton>Settings</MenuButton>
<MenuList>
<MenuItem value="profile">Profile</MenuItem>
<MenuItem value="preferences">Preferences</MenuItem>
</MenuList>
</Menu>
);
}

To access the internal state of the Menu, use a function as children (render prop). You’ll get access to the internal isOpen-state.

import { Menu, MenuButton, MenuItem, MenuList } from "@caspeco/casper-ui-library";
export default function Demo() {
return (
<Menu>
{({ isOpen }) => (
<>
<MenuButton>{isOpen ? "Toggle (I am open!)" : "Toggle (I am closed!)"}</MenuButton>
<MenuList>
<MenuItem value="profile">Profile</MenuItem>
<MenuItem value="preferences">Preferences</MenuItem>
</MenuList>
</>
)}
</Menu>
);
}

The <MenuItem> is the component that renders the actual items in the menu. Depending on what you put within your <MenuItem>, it is important to set the itemType-property to the correct value.

The itemType-property is used to determine what element to render the item as, and is important to make the whole MenuItem “row” clickable/interractable as expected of that element. The default itemType is "button".

import {
Checkbox,
Menu,
MenuButton,
MenuItem,
MenuList,
Radio,
RadioGroup,
Switch,
} from "@caspeco/casper-ui-library";
export default function Demo() {
return (
<Menu closeOnSelect={false}>
<MenuButton>Item Types</MenuButton>
<MenuList>
<MenuItem
value="btn"
onClick={() => {
console.log("Clicked a button");
}}
>
Button type (default)
</MenuItem>
<MenuItem itemType="link" href="#" value="link">
Link type
</MenuItem>
<MenuItem itemType="none" value="none">
None type (non interactive item)
</MenuItem>
<MenuItem itemType="checkbox" value="checkbox">
Checkbox type
<Checkbox onCheckedChange={({ checked }) => console.log(checked)} />
</MenuItem>
<RadioGroup
gap="unset"
onValueChange={(val) => {
console.log(val);
}}
>
<MenuItem itemType="radio" value="radio1">
Radio type
<Radio value="1" />
</MenuItem>
<MenuItem itemType="radio" value="radio2">
Radio type
<Radio value="2" />
</MenuItem>
</RadioGroup>
<MenuItem itemType="switch" value="switch">
Switch type
<Switch onCheckedChange={({ checked }) => console.log(checked)} />
</MenuItem>
</MenuList>
</Menu>
);
}

You can add an icon to the left or right of the text in the <MenuItem>. The icon used is the Icon-component. Or create your own custom icon label using components within the <MenuItem>.

import {
Flex,
ForwardIcon,
Menu,
MenuButton,
MenuItem,
MenuList,
PinIcon,
RadioIcon,
ThemeColor,
ThemeIconSize,
ThemeSpace,
UserIcon,
} from "@caspeco/casper-ui-library";
export default function Demo() {
return (
<Menu>
<MenuButton>Menu with icons</MenuButton>
<MenuList>
<MenuItem leftIcon={UserIcon} itemType="none" value="left">
Left icon only
</MenuItem>
<MenuItem leftIcon={PinIcon} rightIcon={ForwardIcon} itemType="none" value="both">
Left and right icon
</MenuItem>
<MenuItem itemType="none" value="custom">
<Flex gap={ThemeSpace.XSmall}>
<Flex>
<RadioIcon color={ThemeColor.Warning} />
<UserIcon color={ThemeColor.Warning} size={ThemeIconSize.Medium} />
</Flex>
Custom label with icons
</Flex>
</MenuItem>
</MenuList>
</Menu>
);
}

Using the <MenuIconButton> or <MenuBoxButton> instead of <MenuButton> allows you to create a custom trigger for the menu.

import { Menu, MenuIconButton, MenuItem, MenuList, MoreVerticalIcon } from "@caspeco/casper-ui-library";
export default function Demo() {
return (
<Menu>
<MenuIconButton icon={MoreVerticalIcon} aria-label="Open menu" variant="ghost" />
<MenuList>
<MenuItem value="delete">Delete</MenuItem>
<MenuItem value="copy-id">Copy ID</MenuItem>
</MenuList>
</Menu>
);
}

You can cast the MenuItem as a Link-component from a routing library like react-router-dom or next.js, using as={Link}. Using as overrides the itemType, and should only be used when you want to use a Link-component.

import { Menu, MenuButton, MenuList, MenuItem } from "@caspeco/casper-ui-library";
import { Link } from "react-router-dom";
export default () => {
return (
<Menu>
<MenuButton>Links</MenuButton>
<MenuList>
<MenuItem as={Link} to="/cookie-policy">
Cookie Policy
</MenuItem>
<MenuItem as={Link} to="/privacy-policy">
Privacy Policy
</MenuItem>
</MenuList>
</Menu>
);
};

You can nest menu items using the <MenuGroup>-component.

MenuGroup shares all of the properties of MenuItem, but behaves differently in that the children are expected to be only MenuItems or MenuGroups. To set a text label for the group you use the text-property. And to set a component to the right of the text, use the rightComponent-property.

import {
Checkbox,
Menu,
MenuButton,
MenuGroup,
MenuItem,
MenuList,
SmileIcon,
} from "@caspeco/casper-ui-library";
export default function Demo() {
return (
<Menu closeOnSelect={false}>
<MenuButton>View nested settings</MenuButton>
<MenuList>
<MenuGroup text="Food" value="food">
<MenuGroup text="Hot Dishes" value="hot_dishes">
<MenuGroup text="Meat" value="meat">
<MenuItem value="beef">Beef</MenuItem>
<MenuItem value="pork">Pork</MenuItem>
<MenuItem value="chicken">Chicken</MenuItem>
</MenuGroup>
</MenuGroup>
</MenuGroup>
<MenuGroup
text="Drinks"
itemType="checkbox"
rightElement={<Checkbox />}
leftIcon={SmileIcon}
value="drinks"
>
<MenuItem itemType="checkbox" leftIcon={SmileIcon} value="beer">
Beer
<Checkbox />
</MenuItem>
<MenuItem itemType="checkbox" leftIcon={SmileIcon} value="wine">
Wine
<Checkbox />
</MenuItem>
<MenuItem itemType="checkbox" leftIcon={SmileIcon} value="whiskey">
Whiskey
<Checkbox />
</MenuItem>
</MenuGroup>
</MenuList>
</Menu>
);
}

The <Menu>-component can lazily mount the <MenuList> when the menu is open, by setting the isLazy-property to true. Additionally you can set the lazy behavior by setting lazyBehaviour to one of the following values:

  • "unmount" - will unmount the menu list when the menu is closed.
  • "keepMounted" - will keep the menu list mounted when the menu is closed.

Using isLazy can be useful and performance enhancing when the menu contains a lot of items or heavy components.

See more examples within the compositions, and for inspecting all properties available, see API-reference.