Menu
A menu displays a list of choices on a temporary surface. They appear when users interact with a button, action, or other control.
Simple Example
A menu can be created by using the DropdownMenu
and MenuItem
components. The
DropdownMenu
requires the buttonChildren
prop to render text, icons, or
other content in a button and the available MenuItem
s as children.
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { type ReactElement } from "react";
export default function SimpleExample(): ReactElement {
return (
<DropdownMenu buttonChildren="Dropdown">
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
);
}
Menu Item Actions
MenuItem
s should normally provide an onClick
event handler
The last clicked item is: none
"use client";
import { Box } from "@react-md/core/box/Box";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { Typography } from "@react-md/core/typography/Typography";
import { type ReactElement, useState } from "react";
export default function MenuitemActionExample(): ReactElement {
const [lastClicked, setLastClicked] = useState("");
return (
<Box stacked align="start">
<DropdownMenu buttonChildren="Dropdown">
<MenuItem
onClick={() => {
setLastClicked("Item 1");
}}
>
Item 1
</MenuItem>
<MenuItem
onClick={() => {
setLastClicked("Item 2");
}}
>
Item 2
</MenuItem>
<MenuItem
onClick={() => {
setLastClicked("Item 3");
}}
>
Item 3
</MenuItem>
</DropdownMenu>
<Typography>
The last clicked item is: <code>{lastClicked || "none"}</code>
</Typography>
</Box>
);
}
Horizontal Menu
A menu can be rendered horizontally instead of vertically by enabling the
horizontal
prop.
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { type ReactElement } from "react";
export default function HorizontalMenuExample(): ReactElement {
return (
<DropdownMenu buttonChildren="Dropdown" horizontal>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
);
}
Button Styling
Most of the Button props are available as top-level props
on the DropdownMenu
.
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import MoreVertOutlinedIcon from "@react-md/material-icons/MoreVertOutlinedIcon";
import { type ReactElement } from "react";
export default function ButtonStyling(): ReactElement {
return (
<>
<DropdownMenu
theme="primary"
themeType="outline"
buttonChildren="Options..."
>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
<DropdownMenu
theme="secondary"
themeType="contained"
iconSize="small"
buttonType="icon"
buttonChildren={<MoreVertOutlinedIcon />}
>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
</>
);
}
Icon Button
An icon button can be rendered by setting the buttonType
to "icon"
. This
will also remove the default dropdown icon within the button.
Remember to provide an aria-label
or accessible text for screen
readers when using icon buttons.
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import MoreVertOutlinedIcon from "@react-md/material-icons/MoreVertOutlinedIcon";
import { type ReactElement } from "react";
export default function IconButtonExample(): ReactElement {
return (
<DropdownMenu
aria-label="Options"
buttonType="icon"
buttonChildren={<MoreVertOutlinedIcon />}
>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
);
}
Floating Action Button
A menu can be rendered in a floating action button by providing the floating
prop. Just like the Button
component, the button will default to an icon
button if the floating prop is defined instead of a text button.
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import MoreVertOutlinedIcon from "@react-md/material-icons/MoreVertOutlinedIcon";
import { type ReactElement } from "react";
export default function FloatingActionButtonExample(): ReactElement {
return (
<>
<DropdownMenu
floating="top-left"
buttonChildren={<MoreVertOutlinedIcon />}
>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
<DropdownMenu
aria-label="Options"
floating="top-right"
buttonType="text"
buttonChildren="Options"
>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
</>
);
}
Menu Item Addons
A MenuItem
can render addons just like the ListItem.
import { Avatar } from "@react-md/core/avatar/Avatar";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import FavoriteIcon from "@react-md/material-icons/FavoriteIcon";
import { type ReactElement } from "react";
export default function MenuItemAddonsExample(): ReactElement {
return (
<DropdownMenu buttonChildren="Dropdown">
<MenuItem leftAddon={<FavoriteIcon />}>Item 1</MenuItem>
<MenuItem rightAddon={<Avatar>I</Avatar>} rightAddonType="avatar">
Item 1
</MenuItem>
</DropdownMenu>
);
}
Configuring Menu Behavior
The Menu
uses the useFixedPositioning
hook to position itself
within the viewport. Check out the
useFixedPositioning documentation page for
available options.
Controlling Menu Visibility
If the menu's visibility must manually be controlled, provide the visible
and
setVisible
props.
import { Button } from "@react-md/core/button/Button";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { type ReactElement, useState } from "react";
export default function ControllingMenuVisibility(): ReactElement {
const [visible, setVisible] = useState(false);
return (
<>
<Button
onClick={() => {
setVisible(true);
}}
>
Open
</Button>
<DropdownMenu
buttonChildren="Options..."
visible={visible}
setVisible={setVisible}
>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
</>
);
}
Prevent Scroll Example
The default behavior is to reposition the Menu
while the user scrolls the page
until the menu is no longer in the viewport. Another approach is to prevent the
user from scrolling the page while the menu is open. To enable this behavior,
enable the preventScroll
prop.
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { type ReactElement } from "react";
export default function PreventScrollExample(): ReactElement {
return (
<DropdownMenu buttonChildren="Options..." preventScroll>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
);
}
Close On Resize Example
The Menu
will automatically reposition itself if the user resizes the browser.
Another approach is to close the menu instead. Enable the closeOnResize
prop
to opt into this behavior.
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { type ReactElement } from "react";
export default function CloseOnResizeExample(): ReactElement {
return (
<DropdownMenu buttonChildren="Options..." closeOnResize>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
);
}
Disable Portal Example
The Menu
will use the Portal by default to help with
positioning issues within the viewport and temporary elements like
Dialogs. The portal behavior can be disabled by enabling
the disablePortal
prop which will make the Menu
render as the next element
sibling of the menu toggle button.
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { type ReactElement } from "react";
export default function DisablePortalExample(): ReactElement {
return (
<DropdownMenu buttonChildren="Options..." disablePortal>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
);
}
Disable Conditional Rendering Example
The Menu
will only be mounted while visible
which means any internal state
is reset each time the Menu
is opened. Set the temporary
prop to false
if
the Menu
should remain mounted while not visible
and be hidden using CSS
instead.
"use client";
import { ListItem } from "@react-md/core/list/ListItem";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { type ReactElement, useState } from "react";
export default function DisableConditionalRenderingExample(): ReactElement {
return (
<DropdownMenu buttonChildren="Options..." temporary={false}>
<ExampleChildren />
</DropdownMenu>
);
}
function ExampleChildren(): ReactElement {
const [clicked, setClicked] = useState(-1);
return (
<>
<ListItem presentational>{`Last clicked: ${clicked}`}</ListItem>
<MenuItem
onClick={() => {
setClicked(0);
}}
>
Item 1
</MenuItem>
<MenuItem
onClick={() => {
setClicked(1);
}}
>
Item 2
</MenuItem>
<MenuItem
onClick={() => {
setClicked(2);
}}
>
Item 3
</MenuItem>
</>
);
}
Context Menu
To override the default right click behavior and display a custom context menu,
use the useContextMenu
hook and the Menu
component. The useContextMenu
will provide the required props for the Menu
component and automatically
position it relative to the mouse.
The context menu will also update the menu to have the following default props:
anchor
-BELOW_INNER_LEFT_ANCHOR
menuLabel
-"Context Menu"
preventScroll
-true
"use client";
import { TextArea } from "@react-md/core/form/TextArea";
import {
// BELOW_INNER_LEFT_ANCHOR,
Menu,
} from "@react-md/core/menu/Menu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { useContextMenu } from "@react-md/core/menu/useContextMenu";
import ContentCopyOutlinedIcon from "@react-md/material-icons/ContentCopyOutlinedIcon";
import ContentCutOutlinedIcon from "@react-md/material-icons/ContentCutOutlinedIcon";
import ContentPasteOutlinedIcon from "@react-md/material-icons/ContentPasteOutlinedIcon";
import { type ReactElement } from "react";
export default function ContextMenuExample(): ReactElement {
const { menuProps, onContextMenu } = useContextMenu();
// const { menuProps, onContextMenu } = useContextMenu({
// // these are the default values
// anchor: BELOW_INNER_LEFT_ANCHOR,
// menuLabel: "Context Menu",
// preventScroll: true,
//
// // do something custom with the context menu event
// onContextMenu(event) {
// },
// });
return (
<>
<TextArea onContextMenu={onContextMenu} placeholder="Right click me!" />
<Menu {...menuProps}>
<MenuItem leftAddon={<ContentCutOutlinedIcon />}>Cut</MenuItem>
<MenuItem leftAddon={<ContentCopyOutlinedIcon />}>Copy</MenuItem>
<MenuItem leftAddon={<ContentPasteOutlinedIcon />}>Paste</MenuItem>
</Menu>
</>
);
}
Nested Menus
Nested dropdown menus can be created by adding a DropdownMenu
as a child of
another DropdownMenu
. The DropdownMenu
will render as a MenuItemButton
instead of as a MenuButton
and allow for all the correct keyboard behavior.
import { Box } from "@react-md/core/box/Box";
import { Switch } from "@react-md/core/form/Switch";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { type ReactElement, useState } from "react";
export default function NestedMenusExample(): ReactElement {
const [horizontal, setHorizontal] = useState(false);
return (
<Box stacked>
<Switch
label="Horizontal?"
checked={horizontal}
onChange={(event) => {
setHorizontal(event.currentTarget.checked);
}}
/>
<DropdownMenu buttonChildren="Dropdown" horizontal={horizontal}>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<DropdownMenu buttonChildren="Item 3">
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
<MenuItem>Item 4</MenuItem>
<MenuItem>Item 5</MenuItem>
</DropdownMenu>
</Box>
);
}
Rendering in a Sheet
It can sometimes be useful to render a Menu
within a Sheet
instead of a
temporary popup element when on smaller viewports or dealing with large menu
items.
This can be configured on an app-level using the MenuConfigurationProvider
or
a specific DropdownMenu
/Menu
by providing a renderAsSheet
prop. This can
be set to "phone"
to only render menus within sheets on the phone app size or
supports true
/false
values if custom logic is required to render as a sheet
instead.
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuConfigurationProvider } from "@react-md/core/menu/MenuConfigurationProvider";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { type ReactElement } from "react";
export default function RenderingInASheet(): ReactElement {
return (
<>
<MenuConfigurationProvider
renderAsSheet
// renderAsSheet={false}
// renderAsSheet="phone"
>
<DropdownMenu buttonChildren="Dropdown">
<MenuItem disabled>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
<MenuItem>Item 4</MenuItem>
<MenuItem>Item 5</MenuItem>
</DropdownMenu>
</MenuConfigurationProvider>
</>
);
}
Sheet Options
The Sheet
can be configured further to support an optional header, an optional
footer, the position within the viewport, and the vertical size. The optional
header or footer components can be updated to control the visibility of the menu
as well by using the useMenuVisibility
hook which provides the visible
state
and setVisible
function.
Check out the Sheet for more sheet behavior and styling.
import { AppBar } from "@react-md/core/app-bar/AppBar";
import { AppBarTitle } from "@react-md/core/app-bar/AppBarTitle";
import { Button } from "@react-md/core/button/Button";
import { DialogFooter } from "@react-md/core/dialog/DialogFooter";
import { Divider } from "@react-md/core/divider/Divider";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuConfigurationProvider } from "@react-md/core/menu/MenuConfigurationProvider";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { useMenuVisibility } from "@react-md/core/menu/MenuVisibilityProvider";
import CloseIcon from "@react-md/material-icons/CloseIcon";
import { type ReactElement } from "react";
export default function SheetOptionsExample(): ReactElement {
const renderAsSheet = true;
return (
<>
<MenuConfigurationProvider
sheetHeader={<Header />}
sheetFooter={<Footer />}
renderAsSheet={renderAsSheet}
// these are the defaults
sheetPosition="bottom"
sheetVerticalSize="touch"
>
<DropdownMenu
buttonChildren="Dropdown"
// additional styles can be passed to the sheet from the DropdownMenu
// sheetStyle={{ margin: "0 2rem" }}
// sheetClassName="my-custom-class-name"
//
// any other props available on the Sheet component
// sheetProps={{
// horizontalSize: "touch"
// }}
>
<MenuItem disabled>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<DropdownMenu buttonChildren="Child Dropdown">
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
<MenuItem>Item 4</MenuItem>
<MenuItem>Item 5</MenuItem>
</DropdownMenu>
<MenuItem>Item 3</MenuItem>
<MenuItem>Item 4</MenuItem>
<MenuItem>Item 5</MenuItem>
</DropdownMenu>
</MenuConfigurationProvider>
</>
);
}
function Header(): ReactElement {
const { setVisible } = useMenuVisibility();
return (
<AppBar theme="clear">
<AppBarTitle>Custom</AppBarTitle>
<Button
onClick={() => {
setVisible(false);
}}
buttonType="icon"
>
<CloseIcon />
</Button>
</AppBar>
);
}
function Footer(): ReactElement {
const { setVisible } = useMenuVisibility();
return (
<>
<Divider />
<DialogFooter>
<Button
onClick={() => {
setVisible(false);
}}
>
Cancel
</Button>
</DialogFooter>
</>
);
}
Hoverable Menu
The DropdownMenu
can be updated to become visible after a specific hover
timeout by wrapping a group of dropdown menus within the MenuBar
component to
and providing a hoverTimeout
prop. When the hoverTimeout
is set to 0
, the
DropdownMenu
will open immediately otherwise the DropdownMenu
must be
hovered for the duration in milliseconds.
The MenuBar
also provides special keyboard movement behavior so the
children should normally only be DropdownMenu
components. Check out the
MenuBar accessibility section for more info around the keyboard
movement behavior.
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuBar } from "@react-md/core/menu/MenuBar";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import type { ReactElement, ReactNode } from "react";
export default function HoverableMenuExample(): ReactElement {
return (
<MenuBar hoverTimeout={0}>
<DropdownMenu buttonChildren="Item 1">
<MenuItem>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
<MenuItem>Menu Item 4</MenuItem>
</DropdownMenu>
<DropdownMenu buttonChildren="Item 2">
<MenuItem>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
<MenuItem>Menu Item 4</MenuItem>
</DropdownMenu>
<DropdownMenu buttonChildren="Item 3">
<MenuItem>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<InfiniteDropdownMenu buttonChildren="Menu Item 3" depth={0} />
<MenuItem>Menu Item 4</MenuItem>
</DropdownMenu>
<DropdownMenu buttonChildren="Item 4">
<MenuItem>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
<MenuItem>Menu Item 4</MenuItem>
</DropdownMenu>
</MenuBar>
);
}
interface InfiniteDropdownMenuProps {
depth: number;
buttonChildren: ReactNode;
}
function InfiniteDropdownMenu(props: InfiniteDropdownMenuProps): ReactElement {
const { depth, buttonChildren } = props;
return (
<DropdownMenu buttonChildren={buttonChildren}>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
{Array.from({ length: 4 }, (_, i) => (
<InfiniteDropdownMenu
key={i}
depth={depth + 1}
buttonChildren={`Item ${i + 1}`}
/>
))}
<MenuItem>Item 7</MenuItem>
</DropdownMenu>
);
}
Click-first Hoverable Menu
The DropdownMenu
can also behave like the browser bookmarks toolbar where
once a menu has been opened, all other menus within the group will immediately
open once hovered. Omit the hoverTimeout
prop or set it to undefined
to use
this functionality.
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuBar } from "@react-md/core/menu/MenuBar";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import type { ReactElement, ReactNode } from "react";
export default function ClickFirstHoverableMenu(): ReactElement {
return (
<MenuBar>
<DropdownMenu buttonChildren="Item 1">
<MenuItem>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
<MenuItem>Menu Item 4</MenuItem>
</DropdownMenu>
<DropdownMenu buttonChildren="Item 2">
<MenuItem>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
<MenuItem>Menu Item 4</MenuItem>
</DropdownMenu>
<DropdownMenu buttonChildren="Item 3">
<MenuItem>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<InfiniteDropdownMenu buttonChildren="Menu Item 3" depth={0} />
<MenuItem>Menu Item 4</MenuItem>
</DropdownMenu>
<DropdownMenu buttonChildren="Item 4">
<MenuItem>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
<MenuItem>Menu Item 4</MenuItem>
</DropdownMenu>
</MenuBar>
);
}
interface InfiniteDropdownMenuProps {
depth: number;
buttonChildren: ReactNode;
}
function InfiniteDropdownMenu(props: InfiniteDropdownMenuProps): ReactElement {
const { depth, buttonChildren } = props;
return (
<DropdownMenu buttonChildren={buttonChildren}>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
{Array.from({ length: 4 }, (_, i) => (
<InfiniteDropdownMenu
key={i}
depth={depth + 1}
buttonChildren={`Item ${i + 1}`}
/>
))}
<MenuItem>Item 7</MenuItem>
</DropdownMenu>
);
}
Additional MenuItem Components
The following components have been provided to merge the functionality and
accessibility into a MenuItem
.
MenuItemCheckbox
MenuItemRadio
MenuItemSwitch
MenuItemTextField
MenuItemFileInput
MenuItemSeparator
MenuItemGroup
These components will ensure the correct accessibility and keyboard movement
within a DropdownMenu
.
Enable the preventMenuHideOnClick
if the menu should not close after
clicking the menu item.
MenuItemCheckbox Example
The MenuItemCheckbox
component can be used to render a checkbox within a
MenuItem
and is a controlled component requiring the checked
and
onCheckedChange
props. It is recommended to use the
useCheckboxGroup hook when multiple checkboxes are
involved since it also supports indeterminate checkboxes.
"use client";
import { useCheckboxGroup } from "@react-md/core/form/useCheckboxGroup";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItemCheckbox } from "@react-md/core/menu/MenuItemCheckbox";
import { MenuItemSeparator } from "@react-md/core/menu/MenuItemSeparator";
import { type ReactElement, useState } from "react";
const values = ["a", "b", "c", "d"] as const;
const labels = {
a: "Label 1",
b: "Label 2",
c: "Label 3",
d: "Label 4",
} as const;
export default function MenuItemCheckboxExample(): ReactElement {
const [bold, setBold] = useState(false);
const [italic, setItalic] = useState(false);
const { getCheckboxProps, getIndeterminateProps } = useCheckboxGroup({
values,
menu: true,
});
return (
<DropdownMenu buttonChildren="Checkboxes" themeType="outline">
<MenuItemCheckbox {...getIndeterminateProps()} preventMenuHideOnClick>
Toggle All
</MenuItemCheckbox>
{values.map((value) => (
<MenuItemCheckbox
key={value}
{...getCheckboxProps(value)}
preventMenuHideOnClick
>
{labels[value]}
</MenuItemCheckbox>
))}
<MenuItemSeparator />
<MenuItemCheckbox disabled checked={false} onCheckedChange={() => {}}>
Disabled
</MenuItemCheckbox>
<MenuItemCheckbox
checked={bold}
onCheckedChange={(checked) => {
setBold(checked);
}}
>
Bold
</MenuItemCheckbox>
<MenuItemCheckbox
checked={italic}
onCheckedChange={(checked) => {
setItalic(checked);
}}
>
Italic
</MenuItemCheckbox>
</DropdownMenu>
);
}
MenuItemRadio Example
The MenuItemRadio
component can be used to render a radio within a MenuItem
and is a controlled component requiring the checked
and onCheckedChange
props. It is recommended to use the useRadioGroup hook
to control the group of radios.
"use client";
import { type TextDecoration } from "@react-md/core/cssUtils";
import { useRadioGroup } from "@react-md/core/form/useRadioGroup";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItemRadio } from "@react-md/core/menu/MenuItemRadio";
import { Typography } from "@react-md/core/typography/Typography";
import { type ReactElement } from "react";
const decorations = [
"none",
"underline",
"overline",
"strike-through",
] as const;
type Decoration = (typeof decorations)[number];
const getDecoration = (decoration: Decoration): TextDecoration | undefined => {
if (decoration === "none") {
return undefined;
}
return decoration === "strike-through" ? "line-through" : decoration;
};
export default function MenuItemRadioExample(): ReactElement {
const { getRadioProps } = useRadioGroup<Decoration>({
menu: true,
defaultValue: "none",
});
return (
<DropdownMenu buttonChildren="Radio" themeType="outline">
{decorations.map((decoration) => (
<MenuItemRadio
key={decoration}
{...getRadioProps(decoration)}
// preventMenuHideOnClick
>
<Typography textDecoration={getDecoration(decoration)} as="span">
{decoration}
</Typography>
</MenuItemRadio>
))}
<MenuItemRadio disabled checked={false} onCheckedChange={() => {}}>
Disabled
</MenuItemRadio>
</DropdownMenu>
);
}
MenuItemSwitch Example
The MenuItemSwitch
component can be used to render a Switch
within a
MenuItem
and is a controlled component requiring the checked
and
onCheckedChange
props. The state can be controlled by the
useCheckboxGroup if desired.
"use client";
import { useCheckboxGroup } from "@react-md/core/form/useCheckboxGroup";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItemSwitch } from "@react-md/core/menu/MenuItemSwitch";
import { type ReactElement, useState } from "react";
export default function MenuItemSwitchExample(): ReactElement {
const { getCheckboxProps } = useCheckboxGroup({ menu: true });
const [checked, setChecked] = useState(false);
return (
<DropdownMenu buttonChildren="Dropdown" themeType="outline">
<MenuItemSwitch
checked={checked}
onCheckedChange={(checked) => {
setChecked(checked);
}}
preventMenuHideOnClick
>
Label
</MenuItemSwitch>
<MenuItemSwitch {...getCheckboxProps("0")}>Label</MenuItemSwitch>
<MenuItemSwitch iconAfter {...getCheckboxProps("1")}>
Label
</MenuItemSwitch>
</DropdownMenu>
);
}
MenuItemTextField Example
The MenuItemTextField
can be used to render a TextField
within a MenuItem
.
This component will update the menu keyboard behavior so that the menu keyboard
movement only occurs when there is no value within the text field.
Like the normal TextField
, this component defaults to uncontrolled but can be
controlled by providing the value
and onChange
props.
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { MenuItemSeparator } from "@react-md/core/menu/MenuItemSeparator";
import { MenuItemTextField } from "@react-md/core/menu/MenuItemTextField";
import SearchIcon from "@react-md/material-icons/SearchIcon";
import { type ReactElement } from "react";
export default function MenuItemTextFieldExample(): ReactElement {
return (
<DropdownMenu buttonChildren="Dropdown" themeType="outline">
<MenuItemTextField
aria-label="Search"
placeholder="Search"
rightAddon={<SearchIcon />}
/>
<MenuItemSeparator />
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
<MenuItem>Item 4</MenuItem>
</DropdownMenu>
);
}
MenuItemFileInput Example
The MenuItemFileInput
can be used to render FileInput
components within a
MenuItem
. The main difference is that the upload icon will be rendered as a
MenuItem
addon and a label should be provided within the MenuItemFileInput
children.
This component works with the useFileUpload hook and should be used for more complex file selection.
import { useFileUpload } from "@react-md/core/files/useFileUpload";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItemFileInput } from "@react-md/core/menu/MenuItemFileInput";
import { type ReactElement, useState } from "react";
const extensions = [
"svg",
"jpeg",
"jpg",
"png",
"apng",
"mkv",
"mp4",
"mpeg",
"mpg",
"webm",
"mov",
] as const;
const FOUR_HUNDRED_MB = 400 * 1024 * 1024;
const maxFiles = 10;
export default function MenuItemFileInputExample(): ReactElement {
const [file1, setFile1] = useState("");
const [file2, setFile2] = useState("");
// checkout the useFileUpload documentation for a real example with this hook
const { accept, onChange } = useFileUpload({
maxFiles,
maxFileSize: FOUR_HUNDRED_MB,
extensions,
});
return (
<DropdownMenu buttonChildren="Options">
<MenuItemFileInput
onChange={(event) => {
setFile1(event.currentTarget.value);
}}
secondaryText={`Selected file: ${file1 || "none"}`}
>
Upload
</MenuItemFileInput>
<MenuItemFileInput
onChange={(event) => {
setFile2(event.currentTarget.value);
}}
secondaryText={`Selected file: ${file2 || "none"}`}
preventMenuHideOnClick
>
Upload
</MenuItemFileInput>
<MenuItemFileInput disabled onChange={() => {}}>
Disabled
</MenuItemFileInput>
<MenuItemFileInput accept={accept} onChange={onChange} multiple>
Upload Multiple
</MenuItemFileInput>
</DropdownMenu>
);
}
MenuItemSeparator and MenuItemGroup
Use the MenuItemGroup
and MenuItemSeparator
when rendering the
MenuItemRadio
with other groups of MenuItem
s.
See more at https://www.w3.org/TR/wai-aria-1.1/#menuitemradio
import { useRadioGroup } from "@react-md/core/form/useRadioGroup";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { MenuItemGroup } from "@react-md/core/menu/MenuItemGroup";
import { MenuItemRadio } from "@react-md/core/menu/MenuItemRadio";
import { MenuItemSeparator } from "@react-md/core/menu/MenuItemSeparator";
import { type ReactElement } from "react";
export default function MenuItemSeparatorAndGroupExample(): ReactElement {
const { getRadioProps } = useRadioGroup({
menu: true,
});
return (
<DropdownMenu buttonChildren="Dropdown" themeType="outline">
<MenuItemGroup aria-label="Example Radio Group">
<MenuItemRadio {...getRadioProps("a")}>Radio 1</MenuItemRadio>
<MenuItemRadio {...getRadioProps("b")}>Radio 2</MenuItemRadio>
<MenuItemRadio {...getRadioProps("c")}>Radio 3</MenuItemRadio>
</MenuItemGroup>
<MenuItemSeparator />
<MenuItem>Menu Item 1</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
<MenuItem>Menu Item 3</MenuItem>
</DropdownMenu>
);
}
Form MenuItem with Addons
The form menu item components can also render an addon like the MenuItem
using
addon
, addonType
, addonPosition
props.
In addition, if the icon checkbox, radio, or switch should be rendered after the
text instead of before, the iconAfter
prop can be enabled.
import { Avatar } from "@react-md/core/avatar/Avatar";
import { useCheckboxGroup } from "@react-md/core/form/useCheckboxGroup";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItemCheckbox } from "@react-md/core/menu/MenuItemCheckbox";
import { MenuItemRadio } from "@react-md/core/menu/MenuItemRadio";
import { MenuItemSwitch } from "@react-md/core/menu/MenuItemSwitch";
import FavoriteIcon from "@react-md/material-icons/FavoriteIcon";
import { type ReactElement } from "react";
export default function FormMenuItemWithAddonsExample(): ReactElement {
const { getCheckboxProps } = useCheckboxGroup({ menu: true });
return (
<DropdownMenu buttonChildren="Dropdown">
<MenuItemCheckbox {...getCheckboxProps("a")} addon={<FavoriteIcon />}>
With Icon
</MenuItemCheckbox>
<MenuItemRadio
{...getCheckboxProps("b")}
addon={<Avatar>A</Avatar>}
addonType="avatar"
>
With Avatar
</MenuItemRadio>
<MenuItemSwitch
{...getCheckboxProps("c")}
addon={<img src="https://picsum.photos/56?image=700" alt="" />}
addonType="media"
>
With Media
</MenuItemSwitch>
<MenuItemCheckbox
{...getCheckboxProps("d")}
addon={<img src="https://picsum.photos/100/56?image=800" alt="" />}
addonType="large-media"
iconAfter
>
With Large Media
</MenuItemCheckbox>
</DropdownMenu>
);
}
Menu within a Dialog
Since menus use the Portal to render in a different part of the DOM and position within the viewport using useFixedPositioning, menus can be rendered in dialogs without causing any issues like scrollbars appearing or not being able to view all the menu items.
"use client";
import { Button } from "@react-md/core/button/Button";
import { Dialog } from "@react-md/core/dialog/Dialog";
import { DialogContent } from "@react-md/core/dialog/DialogContent";
import { DropdownMenu } from "@react-md/core/menu/DropdownMenu";
import { MenuItem } from "@react-md/core/menu/MenuItem";
import { Typography } from "@react-md/core/typography/Typography";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement } from "react";
export default function MenuWithinADialog(): ReactElement {
const { toggled, enable, disable } = useToggle();
return (
<>
<Button onClick={enable}>Show Dialog</Button>
<Dialog aria-label="Example" visible={toggled} onRequestClose={disable}>
<DialogContent>
<Typography>
{
"Here is a paragraph of text. Even though it's really only two sentences."
}
</Typography>
<DropdownMenu buttonChildren="Dropdown">
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
<MenuItem>Item 3</MenuItem>
</DropdownMenu>
</DialogContent>
</Dialog>
</>
);
}
Accessibility
The menu components implement the menu and menu button specifications.
Keyboard Movement
The following keyboard movement has been implemented:
Menu Button
- ArrowUp - opens the menu and focuses the last menu item
- ArrowDown/Space/Enter - opens the menu and focuses the first menu item
Menu Item Button
- ArrowRight - opens the nested menu and moves focus to the first menu item in
the newly opened menu
- ArrowDown is used for horizontal menus
- ArrowLeft - closes a nested dropdown menu (if exists) and moves focus back
into the parent menu
- ArrowUp is used for horizontal menus
Menu
- ArrowUp/ArrowDown - moves the focus to the previous/next menu item and
loops
- ArrowLeft/ArrowRight will be used for horizontal menus
- Home/End - moves focus to the first and last menu item
- Enter/Space - clicks the current
MenuItem
- Typing a letter will find the focus the first menu item that starts with the same letter. Pressing the same letter will continue to loop through matches.
- Typing an uppercase letter will always focus the first menu item that starts with the same letter.
- Escape - Closes the current menu
MenuBar
- While no dropdown menus are visible:
- ArrowLeft/ArrowRight will move focus to the previous/next dropdown menu items
- Home/End - moves focus to the first and last dropdown menu item
- Typing a letter will find the focus the first menu item that starts with the same letter. Pressing the same letter will continue to loop through matches.
- Typing an uppercase letter will always focus the first menu item that starts with the same letter.
- ArrowUp - opens the menu and focuses the last menu item
- ArrowDown/Space/Enter - opens the menu and focuses the first menu item
- While a dropdown menu is visible:
- ArrowLeft/ArrowRight will close the current dropdown menu, open the
previous/next dropdown menu, and move focus into the new dropdown menu on
the first item
- If a nested dropdown menu is focused, that behavior will be used instead
- ArrowLeft/ArrowRight will close the current dropdown menu, open the
previous/next dropdown menu, and move focus into the new dropdown menu on
the first item