Menu
Multi-purpose combobox widget to allow selection from a dynamic set of options.
#
Documentation
#
#
Usage
#
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react/unstable";
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.
Menu
MenuTrigger
MenuContent
Items must be an array of objects of MenuOption
type.
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react/unstable";
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, Text } from "@optiaxiom/react";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react/unstable";
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/unstable";
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>
);
}
#
Multi-select
#
We can render multi-select items by enabling the multi
property.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react/unstable";
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). But we can always show the input by enabling the initialInputVisible
prop.
"use client";
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react/unstable";
const colors = [
{ label: "Ocean" },
{ label: "Blue" },
{ label: "Purple" },
{ label: "Red" },
{ label: "Orange" },
{ label: "Yellow" },
];
export function App() {
return (
<Menu initialInputVisible options={colors}>
<MenuTrigger>Select color</MenuTrigger>
<MenuContent />
</Menu>
);
}
#
Filtering
#
Menu automatically handles filtering using a built-in fuzzy filter based on the name
and keywords
properties of items.
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/unstable";
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
#
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/unstable";
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
initialInputVisible
loading={isLoading}
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/unstable";
export function App() {
return (
<Menu empty="No colors available." initialInputVisible options={[]}>
<MenuTrigger>Select color</MenuTrigger>
<MenuContent />
</Menu>
);
}
#
Trigger
#
By default we use the Button
component for the menu trigger which accepts all of the existing button props.
#
Addons
#
We can add the addon
and description
properties to show additional content inside the items.
App.tsx
"use client";
import { Box } from "@optiaxiom/react";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react/unstable";
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>
);
}
#
Appearance
#
Use the intent
prop to control the appearance of items.
import { Menu, MenuContent, MenuTrigger } from "@optiaxiom/react/unstable";
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 link
property to options to render them as links.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react/unstable";
import { useMemo, useState } from "react";
const users = [
{
label: "Buckaroo Banzai",
link: "https://www.imdb.com/title/tt0086856/",
},
{ 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>
);
}
#
Groups
#
Use group
property to group items.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react/unstable";
const groups = {
display: {
label: "Publishing Options",
},
divider: {
hidden: true,
label: "Blank",
separator: true,
},
} satisfies Record<string, MenuOption["group"]>;
const options: MenuOption[] = [
{
group: groups.display,
label: "Save to Library",
selected: true,
},
{
group: groups.display,
label: "Overwrite Existing",
},
{
group: groups.divider,
label: "Copy Link",
},
];
export function App() {
return (
<Menu options={options}>
<MenuTrigger>Settings</MenuTrigger>
<MenuContent />
</Menu>
);
}
#
Submenus
#
Use subOptions
property to render submenus.
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react/unstable";
import {
IconBooks,
IconLink,
IconPlus,
IconSparkles,
IconUpload,
} from "@tabler/icons-react";
const options: MenuOption[] = [
{
addon: <IconUpload size={16} />,
label: "Select from…",
subOptions: [
{
addon: <IconUpload size={16} />,
label: "Your device",
},
{
addon: <IconBooks size={16} />,
label: "Library",
},
],
},
{
addon: <IconLink size={16} />,
label: "Add URL",
},
{
addon: <IconSparkles size={16} />,
label: "Generate",
},
];
export function App() {
return (
<Menu options={options}>
<MenuTrigger icon={<IconPlus />} iconPosition="start">
Add Content
</MenuTrigger>
<MenuContent />
</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/unstable";
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>
);
}
#
Related
#
DropdownMenu
Display a dropdown menu.
Select
Single select combobox widget to allow selection from a fixed set of options.
#
Props
#
#
Menu
#
Doesn't render its own HTML element.
Prop |
---|
defaultOpen The initial open state in uncontrolled mode.
Default: |
empty Custom empty state content.
|
initialInputVisible
|
inputValue The input value in controlled mode.
|
loading Whether to show loading spinner inside the menu.
|
onHover Handler that is called when an item is hovered via mouse.
|
onInputValueChange Handler that is called when input value changes.
|
onOpenChange Handler that is called when the open state changes.
|
open The open state in controlled mode.
|
options* The items we want to render.
|
placeholder
Default: |
size
|
#
MenuTrigger
#
Supports all Button props in addition to its own. Renders a <button>
element.
Prop |
---|
addonAfter Display content inside the button after
|
addonBefore Display content inside the button before
|
appearance Control the appearance by selecting between the different button types.
|
asChild Change the default rendered element for the one passed as a child, merging their props and behavior. Read the Composition guide for more details.
|
className
|
disabled Whether the button is disabled.
|
hasCustomAnchor
|
icon Display an icon before or after the button content or omit
|
iconPosition Control whether to show the icon before or after the button content.
|
loading Whether to show loading spinner inside the button.
|
size Control the size of the button.
|
square Whether button should have square shape.
|
#
MenuContent
#
Supports all Box props in addition to its own. Renders a <div>
element.
Prop |
---|
align
|
alignOffset
|
asChild Change the default rendered element for the one passed as a child, merging their props and behavior. Read the Composition guide for more details.
|
className
|
onCloseAutoFocus Event handler called when auto-focusing on close. Can be prevented.
|
onEscapeKeyDown Event handler called when the escape key is down. Can be prevented.
|
onFocusOutside Event handler called when the focus moves outside of the
|
onInteractOutside Event handler called when an interaction happens outside the
|
onOpenAutoFocus Event handler called when auto-focusing on open. Can be prevented.
|
onPointerDownOutside Event 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.3.0
#
-
Renamed
Combobox
component toMenu
:// Before <Combobox> <ComboboxTrigger /> <ComboboxContent /> </Combobox> // After <Menu> <MenuTrigger /> <MenuContent /> </Menu>
-
Renamed
items
prop tooptions
:// Before <Combobox items={[]} /> // After <Combobox options={[]} />
-
Removed
itemToLabel
andisItemSelected
props 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