Menu
Dropdown menu for displaying actions.
#
Documentation
#
#
Usage
#
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
import { useMemo, useState } from "react";
const priorities = ["No priority", "Urgent", "High", "Medium", "Low"];
export function App() {
const [value, setValue] = useState<string>("No priority");
return (
<Menu
options={useMemo(
() =>
priorities.map<MenuOption>((priority) => ({
execute: () => setValue(priority),
label: priority,
selected: priority === value,
})),
[value],
)}
>
<MenuTrigger w="224">
{value !== "No priority" ? value : "Set priority"}
</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Anatomy
#
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
export default () => (
<Menu>
<MenuTrigger />
<MenuContent />
</Menu>
);#
Structure
#
Menu works with lists of items provided via the options prop. The basic structure includes the main component provider, a trigger, and the content popover.
MenuMenuTriggerMenuContent
Items must be an array of objects of MenuOption type.
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
const colors: MenuOption[] = [
{ label: "Ocean" },
{ label: "Blue" },
{ label: "Purple" },
{ label: "Red" },
{ label: "Orange" },
{ label: "Yellow" },
];
export function App() {
return (
<Menu options={colors}>
<MenuTrigger>Select color</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Selection
#
Clicking the items will not do anything yet. For that we’ll have to add the execute property to each item.
Selected:
"use client";
import {
Flex,
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
Text,
} from "@optiaxiom/react";
import { useMemo, useState } from "react";
const colors = ["Ocean", "Blue", "Purple", "Red", "Orange", "Yellow"];
export function App() {
const [value, setValue] = useState("");
return (
<Flex>
<Menu
options={useMemo(
() =>
colors.map<MenuOption>((color) => ({
execute: () => setValue(color),
label: color,
})),
[],
)}
>
<MenuTrigger>Select color</MenuTrigger>
<MenuContent />
</Menu>
<Text fontSize="md">Selected: {value}</Text>
</Flex>
);
}#
Single-select
#
Now that we can perform actions on selection, we can also store that state and show the selected states for each item using the selected property.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
import { useMemo, useState } from "react";
const colors = ["Ocean", "Blue", "Purple", "Red", "Orange", "Yellow"];
export function App() {
const [value, setValue] = useState<(typeof colors)[number]>();
return (
<Menu
options={useMemo(
() =>
colors.map<MenuOption>((color) => ({
execute: () => setValue((value) => (value === color ? "" : color)),
label: color,
selected: value === color,
})),
[value],
)}
>
<MenuTrigger w="224">{value || "Select color"}</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Single-select (Switch)
#
We can optionally use switch toggles to show selection states by enabling the switch property.
"use client";
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
import { IconSparkles, IconUser } from "@tabler/icons-react";
import { useState } from "react";
export function App() {
const [enabled, setEnabled] = useState(false);
return (
<Menu
options={[
{
addon: <IconUser />,
label: "My Profile",
},
{
addon: <IconSparkles />,
execute: () => setEnabled(!enabled),
label: "New UI (Beta)",
selected: enabled,
switch: true,
},
]}
>
<MenuTrigger>Settings</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Multi-select
#
We can render multi-select items by enabling the multi property.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
import { useMemo, useState } from "react";
const colors = ["Ocean", "Blue", "Purple", "Red", "Orange", "Yellow"];
export function App() {
const [value, setValue] = useState<string[]>([]);
return (
<Menu
options={useMemo(
() =>
colors.map<MenuOption>((color) => ({
execute: () =>
setValue((value) =>
value.includes(color)
? value.filter((v) => v !== color)
: [...value, color],
),
label: color,
multi: true,
selected: value.includes(color),
})),
[value],
)}
>
<MenuTrigger w="224">
{value.length ? `${value.length} selected` : "Select color"}
</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Input
#
By default the input is only shown if there are any selectable items (items with a selected property) or when the user starts typing. But we can always show the input by setting the inputVisible prop to always.
"use client";
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
const colors = [
{ label: "Ocean" },
{ label: "Blue" },
{ label: "Purple" },
{ label: "Red" },
{ label: "Orange" },
{ label: "Yellow" },
];
export function App() {
return (
<Menu inputVisible="always" options={colors}>
<MenuTrigger>Select color</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Filtering
#
Menu automatically handles filtering using a built-in fuzzy filter based on the label and keywords properties of items.
Type “placeholder” into the menu and see how “Workflow” is highlighted:
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
export function App() {
return (
<Menu
options={[
{
keywords:
"general weekends working days publishing budgeting financial default language business days",
label: "Organization",
},
{
keywords: "create workflow step substep placeholder tasks",
label: "Workflows",
},
{
keywords: "custom fields",
label: "Fields",
},
{
keywords:
"form task campaign content model component types content types",
label: "Templates",
},
]}
>
<MenuTrigger>Profile</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Creatable example
#
We can customize the filter behavior using the visible property on items.
The following example shows how we can build creatable menus by allowing the user to add new entries on the fly.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
import { useMemo, useState } from "react";
const colors = ["Ocean", "Blue", "Purple", "Red", "Orange", "Yellow"];
export function App() {
const [items, setItems] = useState(colors);
const [value, setValue] = useState<string>();
return (
<Menu
options={useMemo<MenuOption[]>(
() => [
...items.map<MenuOption>((color) => ({
execute: () => setValue(color),
label: color,
selected: value === color,
})),
{
detail: ({ inputValue }) => `"${inputValue}"`,
execute: ({ inputValue }) => {
if (inputValue) {
setItems((items) => [...items, inputValue]);
setValue(inputValue);
}
},
label: "Create: ",
visible: ({ inputValue }) =>
inputValue
? !items.find(
(item) => item.toLowerCase() === inputValue.toLowerCase(),
)
: false,
},
],
[items, value],
)}
>
<MenuTrigger w="224">{value || "Select colors"}</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Async loading example
#
We can also manually control options in combination with the inputValue and onInputValueChange prop to load items as the user types.
And we can toggle the loading prop to show a loading state while the data is loading (an empty state will be shown otherwise).
App.tsx
"use client";
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
import { useQuery } from "./useQuery";
const colors = [
{
label: "Ocean",
visible: true,
},
{
label: "Blue",
visible: true,
},
{
label: "Purple",
visible: true,
},
{
label: "Red",
visible: true,
},
{
label: "Orange",
visible: true,
},
{
label: "Yellow",
visible: true,
},
];
export function App() {
const {
data = [],
isLoading,
refetch,
} = useQuery((inputValue: string) =>
inputValue
? colors.filter((color) =>
color.label.toLowerCase().startsWith(inputValue.toLowerCase()),
)
: colors,
);
return (
<Menu
inputVisible="always"
loading={isLoading && "spinner"}
onInputValueChange={(inputValue) => refetch(inputValue)}
options={data}
>
<MenuTrigger w="224">Select colors</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Empty
#
By default menu will display a generic empty content message if no results are found matching your query. We can customize this empty message by setting the empty prop.
"use client";
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
export function App() {
return (
<Menu empty="No colors available." inputVisible="always" options={[]}>
<MenuTrigger>Select color</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Trigger
#
#
Customize trigger
#
By default we use the Button component for the menu trigger which accepts all of the existing button props.
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
import { IconFilter, IconLogout, IconUser } from "@tabler/icons-react";
export function App() {
return (
<Menu
options={[
{
addon: <IconUser />,
group: {
label: "My Account",
},
label: "View Profile",
},
{
addon: <IconLogout />,
group: {
hidden: true,
label: "Logout",
separator: true,
},
label: "Logout",
},
]}
>
<MenuTrigger
appearance="subtle"
aria-label="Filters"
icon={<IconFilter />}
/>
<MenuContent />
</Menu>
);
}#
Ellipsis trigger
#
We can also use asChild to render a completely different component. We provide two built-in buttons:
AngleMenuButton(default)EllipsisMenuButton
import {
EllipsisMenuButton,
Menu,
MenuContent,
MenuTrigger,
} from "@optiaxiom/react";
import { IconLogout, IconUser } from "@tabler/icons-react";
export function App() {
return (
<Menu
options={[
{
addon: <IconUser />,
group: {
label: "My Account",
},
label: "View Profile",
},
{
addon: <IconLogout />,
group: {
hidden: true,
label: "Logout",
separator: true,
},
label: "Logout",
},
]}
>
<MenuTrigger asChild>
<EllipsisMenuButton appearance="subtle" aria-label="My Account" />
</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Addon
#
We can add the addon property to show additional content inside the items.
App.tsx
"use client";
import {
Box,
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
import { useMemo, useState } from "react";
import { type Color, colors } from "./data";
export function App() {
const [value, setValue] = useState<Color[]>([]);
return (
<Menu
options={useMemo(
() =>
colors.map<MenuOption>((color) => ({
addon: (
<Box
rounded="full"
size="10"
style={{ backgroundColor: color.color }}
/>
),
execute: () =>
setValue((value) =>
value.includes(color)
? value.filter((v) => v !== color)
: [...value, color],
),
label: color.label,
multi: true,
selected: () => value.includes(color),
})),
[value],
)}
>
<MenuTrigger w="224">Select colors</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Description
#
We can use the description property to show secondary text in addition to item labels.
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
export function App() {
return (
<Menu
options={[
{
description: "Create a new task",
label: "New task",
},
{
description: "Copy this task",
label: "Copy task",
},
]}
>
<MenuTrigger>Actions</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Detail
#
We can use the detail property to show secondary content in addition to item labels.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
toaster,
} from "@optiaxiom/react";
const priorities = [
{ priority: "No priority", tasks: 3 },
{ priority: "Urgent", tasks: 0 },
{ priority: "High", tasks: 1 },
{ priority: "Medium", tasks: 0 },
{ priority: "Low", tasks: 0 },
];
export function App() {
return (
<Menu
options={priorities.map<MenuOption>(({ priority, tasks }) => ({
detail: tasks ? `${tasks} issue${tasks === 1 ? "" : "s"}` : undefined,
execute: () => toaster.create(`Selected ${priority}`),
label: priority,
}))}
>
<MenuTrigger>Filter</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Disabled items
#
Use the disabledReason property to disable the menu items.
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
export function App() {
return (
<Menu
options={[
{ label: "Edit" },
{
disabledReason: "Public link will be available when published",
label: "Copy public link",
},
{ label: "Download" },
]}
>
<MenuTrigger>Actions</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Appearance
#
Use the intent prop to control the appearance of items.
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
export function App() {
return (
<Menu
options={[
{ description: "Create a new task", label: "New task" },
{ description: "Copy this task", label: "Copy task" },
{
group: { hidden: true, label: "Delete", separator: true },
intent: "danger",
label: "Delete task",
},
]}
>
<MenuTrigger>Actions</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Links
#
Add the href property to options to render them as links.
If the execute property is also present then the default link clicking behavior will be prevented (unless users use a modifier like CtrlEnter to open in a new tab).
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
import { useMemo, useState } from "react";
const users = [
{
href: "https://www.imdb.com/title/tt0086856/",
label: "Buckaroo Banzai",
},
{ label: "Emilio Lizardo" },
{ label: "Perfect Tommy" },
] satisfies MenuOption[];
export function App() {
const [value, setValue] = useState<(typeof users)[number]>();
return (
<Menu
options={useMemo(
() =>
users.map<MenuOption>((user) => ({
execute: () => setValue(user),
selected: value === user,
...user,
})),
[value],
)}
>
<MenuTrigger>Select assignee</MenuTrigger>
<MenuContent />
</Menu>
);
}If the execute property is omitted then clicking an item will simply follow the link and close the menu.
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
export function App() {
return (
<Menu
options={[
{
href: "../../",
label: "Home",
},
{
href: "../",
label: "Components",
},
]}
>
<MenuTrigger>Navigate to section</MenuTrigger>
<MenuContent />
</Menu>
);
}#
External
#
We can toggle the external property to show an external link icon and open links in a new tab by default.
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
import { IconHelp } from "@tabler/icons-react";
export function App() {
return (
<Menu
options={[
{
external: true,
href: "https://github.com/optimizely-axiom/optiaxiom/",
label: "GitHub",
},
]}
>
<MenuTrigger aria-label="Help menu" icon={<IconHelp />} />
<MenuContent />
</Menu>
);
}#
Groups
#
Use group property to group items.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
const groups = {
display: {
label: "Publishing Options",
},
} satisfies Record<string, MenuOption["group"]>;
export function App() {
return (
<Menu
options={[
{
group: groups.display,
label: "Save to Library",
selected: true,
},
{
group: groups.display,
label: "Overwrite Existing",
},
{
label: "Copy Link",
},
]}
>
<MenuTrigger>Settings</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Separator
#
Use group.separator to show a horizontal separator before/after the group.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
const groups = {
display: {
label: "Publishing Options",
separator: true,
},
} satisfies Record<string, MenuOption["group"]>;
export function App() {
return (
<Menu
options={[
{
group: groups.display,
label: "Save to Library",
selected: true,
},
{
group: groups.display,
label: "Overwrite Existing",
},
{
label: "Copy Link",
},
]}
>
<MenuTrigger>Settings</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Hidden label
#
Use group.hidden property to hide the group label. Useful when you want to separate groups but do not want show any labels.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
const groups = {
display: {
hidden: true,
label: "Publishing Options",
separator: true,
},
} satisfies Record<string, MenuOption["group"]>;
export function App() {
return (
<Menu
options={[
{
group: groups.display,
label: "Save to Library",
selected: true,
},
{
group: groups.display,
label: "Overwrite Existing",
},
{
label: "Copy Link",
},
]}
>
<MenuTrigger>Settings</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Priority
#
Use group.priority property to sort groups among each other.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
const groups = {
delete: {
hidden: true,
label: "Delete",
priority: -100,
separator: true,
},
edit: {
hidden: true,
label: "Edit",
separator: true,
},
} satisfies Record<string, MenuOption["group"]>;
export function App() {
return (
<Menu
options={[
{
group: groups.delete,
intent: "danger",
label: "Delete",
},
{
group: groups.edit,
label: "View",
},
{
group: groups.edit,
label: "Download",
},
]}
>
<MenuTrigger>Settings</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Submenus
#
Use subOptions property to render submenus.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
import {
IconBooks,
IconLink,
IconPlus,
IconSparkles,
IconUpload,
} from "@tabler/icons-react";
const options: MenuOption[] = [
{
addon: <IconUpload />,
label: "Select from…",
subOptions: [
{
addon: <IconUpload />,
label: "Your device",
},
{
addon: <IconBooks />,
label: "Library",
},
],
},
{
addon: <IconLink />,
label: "Add URL",
},
{
addon: <IconSparkles />,
label: "Generate",
},
];
export function App() {
return (
<Menu options={options}>
<MenuTrigger icon={<IconPlus />} iconPosition="start">
Add Content
</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Dialogs inside menus
#
Combine with dialogs to show modals or alerts when selecting an item.
Make sure to use AlertDialog or Dialog in controlled mode since there is no actual trigger.
"use client";
import {
AlertDialog,
AlertDialogAction,
AlertDialogBody,
AlertDialogCancel,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
Menu,
MenuContent,
MenuTrigger,
} from "@optiaxiom/react";
import { IconPencil, IconTrash } from "@tabler/icons-react";
import { useState } from "react";
export function App() {
const [open, setOpen] = useState(false);
return (
<Menu
options={[
{
addon: <IconPencil />,
label: "Edit",
},
{
addon: <IconTrash />,
execute: () => setOpen(true),
intent: "danger",
label: "Delete",
},
]}
>
<MenuTrigger>Open</MenuTrigger>
<MenuContent />
<AlertDialog onOpenChange={setOpen} open={open}>
<AlertDialogContent>
<AlertDialogHeader>Are you sure?</AlertDialogHeader>
<AlertDialogBody>
The task and all content will be deleted.
</AlertDialogBody>
<AlertDialogFooter>
<AlertDialogCancel />
<AlertDialogAction>Yes, delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Menu>
);
}#
Virtualized
#
Menu automatically uses virtualization to improve performance when rendering a large number of items.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react";
import { useMemo, useState } from "react";
const colors = Array.from({ length: 2000 }).map(
(_, index) => `Color ${index + 1}`,
);
export function App() {
const [value, setValue] = useState([colors[0]]);
return (
<Menu
options={useMemo(
() =>
colors.map<MenuOption>((color) => ({
execute: () =>
setValue((value) =>
value.includes(color)
? value.filter((v) => v !== color)
: [...value, color],
),
label: color,
multi: true,
selected: () => value.includes(color),
})),
[value],
)}
>
<MenuTrigger w="224">Select colors</MenuTrigger>
<MenuContent />
</Menu>
);
}#
Props
#
#
Menu
#
Doesn't render its own HTML element.
Prop |
|---|
defaultOpenThe initial open state in uncontrolled mode.
Default: |
emptyCustom empty state content.
|
inputValueThe input value in controlled mode.
|
inputVisibleWhether to always show the input or only if there are selectable options and/or when user starts to type.
|
loadingWhether to show loading spinner or skeleton items inside the menu.
|
onInputValueChangeHandler that is called when input value changes.
|
onOpenChangeHandler that is called when the open state changes.
|
openThe open state in controlled mode.
|
options*The items we want to render.
|
placeholderThe placeholder for the search input.
Default: |
sizeWhether to show a small popover or a dialog. Defaults to popover on large screens and dialog on mobile screens.
|
#
MenuTrigger
#
Supports all Button props in addition to its own. Renders a <button> element.
Prop |
|---|
addonAfterDisplay content inside the button after
|
addonBeforeDisplay content inside the button before
|
appearanceControl the appearance by selecting between the different button types.
|
asChildChange the default rendered element for the one passed as a child, merging their props and behavior. Read the Composition guide for more details.
|
className
|
disabledWhether the button is disabled.
|
iconDisplay an icon before or after the button content or omit
|
iconPositionControl whether to show the icon before or after the button content.
|
loadingWhether to show loading spinner inside the button.
|
sizeControl the size of the button.
|
squareWhether button should have square shape.
|
#
MenuContent
#
Supports all Box props in addition to its own. Renders a <div> element.
Prop |
|---|
align
|
alignOffset
|
asChildChange the default rendered element for the one passed as a child, merging their props and behavior. Read the Composition guide for more details.
|
className
|
minWWhether to set the min-width to the width of the trigger.
|
onCloseAutoFocusEvent handler called when auto-focusing on close. Can be prevented.
|
onEscapeKeyDownEvent handler called when the escape key is down. Can be prevented.
|
onFocusOutsideEvent handler called when the focus moves outside of the
|
onInteractOutsideEvent handler called when an interaction happens outside the
|
onOpenAutoFocusEvent handler called when auto-focusing on open. Can be prevented.
|
onPointerDownOutsideEvent handler called when the a
|
side
|
sideOffset
|
#
Accessibility
#
#
Keyboard interactions
#
Key | Description |
|---|---|
| Space | When focus is on MenuTrigger, opens the menu and focuses the first item (or selected item). When focus is on an item, activates the focused item. |
| Enter | When focus is on MenuTrigger, opens the menu and focuses the first item (or selected item). When focus is on an item, activates the focused item and closes the menu. |
| ArrowDown | When focus is on MenuTrigger, opens the menu and focuses the first item (or selected item). When focus is on an item, moves focus to the next item. |
| ArrowUp | When focus is on an item, moves focus to the previous item. |
PageUp PageDown | Moves focus up/down by 10 items at a time. |
Home End | Moves focus to the first/last item. |
ArrowRight ArrowLeft | When focus is on an item with a submenu, opens/closes the submenu. |
A - Z | When focus is on an item, filter and display matching items as you type. |
| Esc | Closes the menu and moves focus back to MenuTrigger. |
#
Changelog
#
#
1.6.0
#
-
Moved component out of Alpha.
// Before import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react/unstable"; // After import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react";
#
1.3.0
#
-
Renamed
Comboboxcomponent toMenu:// Before <Combobox> <ComboboxTrigger /> <ComboboxContent /> </Combobox> // After <Menu> <MenuTrigger /> <MenuContent /> </Menu> -
Renamed
itemsprop tooptions:// Before <Combobox items={[]} /> // After <Combobox options={[]} /> -
Removed
itemToLabelandisItemSelectedprops in favor of fixed properties on options:// Before <Combobox items={[ { id: "1", name: "Sample", }, ]} isItemSelected={(item) => value.includes(item)} itemToLabel={(item) => item.name} /> // After <Combobox options={[ { label: "Sample", selected: true, }, ]} />
#
0.4.0
#
- Added component