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.
"use client";
import { toaster } from "@optiaxiom/react";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react/unstable";
import { useMemo } from "react";
const colors = ["Ocean", "Blue", "Purple", "Red", "Orange", "Yellow"];
export function App() {
return (
<Menu
options={useMemo(
() =>
colors.map<MenuOption>((color) => ({
execute: () => {
toaster.create(`Clicked "${color}"`);
},
label: color,
})),
[],
)}
>
<MenuTrigger>Select color</MenuTrigger>
<MenuContent />
</Menu>
);
}
#
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(color),
label: color,
selected: value === color,
})),
[value],
)}
>
<MenuTrigger>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>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 defaultInputVisible
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 defaultInputVisible 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.
App.tsx
"use client";
import {
Menu,
MenuContent,
type MenuOption,
MenuTrigger,
} from "@optiaxiom/react/unstable";
import { useMemo, useState } from "react";
import { useSet } from "./useSet";
const colors = ["Ocean", "Blue", "Purple", "Red", "Orange", "Yellow"];
export function App() {
const [items, setItems] = useState(colors);
const [value, { toggle }] = useSet<string>([]);
return (
<Menu
options={useMemo<MenuOption[]>(
() => [
...items.map<MenuOption>((color) => ({
execute: () => toggle(color),
label: color,
selected: value.includes(color),
})),
{
detail: ({ inputValue }) => `"${inputValue}"`,
execute: ({ inputValue }) => {
if (inputValue) {
setItems((items) => [...items, inputValue]);
}
},
label: "Create: ",
visible: ({ inputValue }) =>
inputValue
? !items.find(
(item) => item.toLowerCase() === inputValue.toLowerCase(),
)
: false,
},
],
[items, toggle, value],
)}
>
<MenuTrigger w="224">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 spinner 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
defaultInputVisible
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";
const colors = [
{ label: "Ocean" },
{ label: "Blue" },
{ label: "Purple" },
{ label: "Red" },
{ label: "Orange" },
{ label: "Yellow" },
];
export function App() {
return (
<Menu defaultInputVisible empty="No colors matched." options={colors}>
<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>
);
}
#
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>
);
}
#
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 |
---|
defaultInputVisible
|
defaultOpen The initial open state in uncontrolled mode.
Default: |
empty Custom empty state content.
|
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
|
#
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