Menu
Overview
The Menu component provides Bulma's vertical navigation menu: a simple, accessible sidebar or section menu for your Bulma React UI.
It supports labels, nested menu lists, active states, custom links, sections that open and close, and all Bulma/utility helper props. Use it for dashboards, sidebars, admin panels, or any grouped navigation.
Menu supports unlimited nesting and lets you use any link or router component via the as prop on Menu.Item.
Import
import { Menu } from '@allxsmith/bestax-bulma';
Usage
Basic Menu with Nested Lists
To create a sidebar or section menu with multiple levels, use the Menu component with nested Menu.Label and Menu.List subcomponents. You can nest Menu.List inside a Menu.Item to create hierarchical navigation. Use the active prop on a Menu.Item to highlight the current selection. This pattern is ideal for dashboards, admin panels, or any grouped navigation area, and you can apply Bulma helper props for layout and color customization. Give each item an href, a router link through as, or as="button": Menu.Item renders an <a> by default, and an <a> with no href isn't a link, so Tab skips it and assistive tech doesn't present it as one.
<Menu style={{ maxWidth: 300 }}> <Menu.Label>General</Menu.Label> <Menu.List> <Menu.Item href="#dashboard">Dashboard</Menu.Item> <Menu.Item href="#customers">Customers</Menu.Item> </Menu.List> <Menu.Label>Administration</Menu.Label> <Menu.List> <Menu.Item href="#team-settings">Team Settings</Menu.Item> <Menu.Item active href="#manage-your-team"> Manage Your Team <Menu.List> <Menu.Item href="#members">Members</Menu.Item> <Menu.Item href="#plugins">Plugins</Menu.Item> <Menu.Item href="#add-a-member">Add a member</Menu.Item> </Menu.List> </Menu.Item> <Menu.Item href="#invitations">Invitations</Menu.Item> <Menu.Item href="#cloud-storage"> Cloud Storage Environment Settings </Menu.Item> <Menu.Item href="#authentication">Authentication</Menu.Item> </Menu.List> <Menu.Label>Transactions</Menu.Label> <Menu.List> <Menu.Item href="#payments">Payments</Menu.Item> <Menu.Item href="#transfers">Transfers</Menu.Item> <Menu.Item href="#balance">Balance</Menu.Item> </Menu.List> </Menu>
Custom Link Component
Use the as prop on Menu.Item to render a custom link component, such as a router link. Its props follow as, so props like to or href are checked against whatever you render and are forwarded to it.
// import { Link } from 'react-router-dom'; function example() { const Link = (props: { to: string; children?: React.ReactNode }) => ( <a href={props.to}>{props.children}</a> ); return ( <Menu> <Menu.Label>App</Menu.Label> <Menu.List> <Menu.Item as={Link} to="/dashboard"> Dashboard </Menu.Item> <Menu.Item as={Link} to="/settings"> Settings </Menu.Item> </Menu.List> </Menu> ); }
Active Menu Item
Highlight a menu entry by setting the active prop on a Menu.Item. This visually indicates the user's current location within the navigation structure, making it easier to orient within the app or section. It also sets aria-current="page" on the item's link, so assistive tech knows which one is current too.
<Menu> <Menu.Label>Active Example</Menu.Label> <Menu.List> <Menu.Item href="#overview">Overview</Menu.Item> <Menu.Item active href="#current-page"> Current Page </Menu.Item> <Menu.Item href="#other">Other</Menu.Item> </Menu.List> </Menu>
Deeply Nested Menu
Create multi-level navigation by nesting Menu.List components inside Menu.Item. This is ideal for complex sidebars or admin panels with grouped and deeply nested navigation links.
<Menu> <Menu.Label>Levels</Menu.Label> <Menu.List> <Menu.Item href="#level-1"> Level 1 <Menu.List> <Menu.Item href="#level-2"> Level 2 <Menu.List> <Menu.Item href="#level-3">Level 3</Menu.Item> </Menu.List> </Menu.Item> </Menu.List> </Menu.Item> </Menu.List> </Menu>
Sections that open and close
A docs or app sidebar often has groups of links that open and close. Put each group's
Menu.List in a Menu.Section, with the group's name in label. The heading is a button drawn
like Menu.Label, with a chevron that points down while the section is open, and the links under
it are the same Menu.Items as anywhere else, with their href, active and aria-current.
Give defaultOpen to the section that holds the current page, so the reader sees where they are.
A section goes directly in Menu, beside any Menu.Labels and Menu.Lists, and holds a
Menu.List. Don't put one inside a Menu.Item, where it would render inside the item's link, or
inside a Menu.List, where it wouldn't be a list item.
<nav aria-label="Documentation"> <Menu> <Menu.Section label="Getting started"> <Menu.List> <Menu.Item href="#installation">Installation</Menu.Item> <Menu.Item href="#quick-start">Quick start</Menu.Item> </Menu.List> </Menu.Section> <Menu.Section label="Components" defaultOpen> <Menu.List> <Menu.Item href="#button">Button</Menu.Item> <Menu.Item href="#card" active> Card </Menu.Item> <Menu.Item href="#modal">Modal</Menu.Item> </Menu.List> </Menu.Section> <Menu.Section label="Form"> <Menu.List> <Menu.Item href="#input">Input</Menu.Item> <Menu.Item href="#select">Select</Menu.Item> </Menu.List> </Menu.Section> </Menu> </nav>
This is navigation, not a tree: each link is a tab stop of its own, and there are no arrow keys to
learn. For data you browse and pick from, like a file explorer, use TreeView
instead.
Bulma's menu has no section that opens and closes, so Menu.Section is bestax's own. Its styles
ship in bestax.css and extras.css, not in Bulma's stylesheet.
Controlled sections
A section holds its own open state unless you pass open. Then it's yours, and the button only
reports the state it asks for through onOpenChange, the way Collapse works.
That lets the app open the section for the current route, or open and close them all at once.
function example() { const groups = { Components: ['Button', 'Card', 'Modal'], Form: ['Input', 'Select'], }; const [open, setOpen] = React.useState(['Components']); return ( <> <Buttons mb="3"> <Button size="small" onClick={() => setOpen(Object.keys(groups))}> Open all </Button> <Button size="small" onClick={() => setOpen([])}> Close all </Button> </Buttons> <Menu> {Object.entries(groups).map(([label, links]) => ( <Menu.Section key={label} label={label} open={open.includes(label)} onOpenChange={isOpen => setOpen(current => isOpen ? [...current, label] : current.filter(l => l !== label) ) } > <Menu.List> {links.map(link => ( <Menu.Item key={link} href={`#${link.toLowerCase()}`}> {link} </Menu.Item> ))} </Menu.List> </Menu.Section> ))} </Menu> </> ); }
Forwarded ref
Menu.Item forwards a ref to the inner element — the one as names — not to the wrapping <li>. That is the element you want for programmatic focus.
function example() { const itemRef = React.useRef<HTMLAnchorElement>(null); return ( <> <Button mb="3" onClick={() => itemRef.current?.focus()}> Focus Customers </Button> <Menu> <Menu.Label>General</Menu.Label> <Menu.List> <Menu.Item href="#dashboard">Dashboard</Menu.Item> <Menu.Item href="#customers" ref={itemRef}> Customers </Menu.Item> </Menu.List> </Menu> </> ); }
Compound (dot-notation) usage
MenuLabel, MenuList, MenuItem, and MenuSection are also available as Menu.Label, Menu.List, Menu.Item, and Menu.Section, so the whole menu can be composed from the single Menu import.
<Menu> <Menu.Label>General</Menu.Label> <Menu.List> <Menu.Item active href="#dashboard"> Dashboard </Menu.Item> <Menu.Item href="#customers">Customers</Menu.Item> </Menu.List> <Menu.Section label="Transactions"> <Menu.List> <Menu.Item href="#payments">Payments</Menu.Item> <Menu.Item href="#transfers">Transfers</Menu.Item> </Menu.List> </Menu.Section> </Menu>
Accessibility
- The root
Menurenders as<aside class="menu">. A menu that's navigation is better inside a<nav>with anaria-label, which gives assistive tech a navigation landmark to jump to. - Every
Menu.Listhasrole="list", nested ones included. That's the role a<ul>already has, set again for Safari, whose WebKit stops exposing a<ul>as a list once its markers are hidden, as Bulma's reset hides them, unless the role is written out. Menu.Item activesetsaria-current="page"on the elementasrenders, the link by default, so the current item isn't only a color. Anaria-currentyou pass wins: a menu of sections on one page can say"location", andaria-current={false}turns it off.- Use semantic nesting (
Menu.ListinsideMenu.Item) for submenus. - Give each item an
href, a router link throughas, oras="button", so the keyboard can reach it. Menu.Item as="button"defaults totype="button", so a menu item inside a form doesn't submit it. Passtype="submit"ortype="reset"and yours is used instead; any other value renderstype="button".- In forced colors (Windows' contrast themes), the browser replaces Bulma's fill and text color
for the active item along with every other color, so bestax's stylesheet marks an
is-activeoris-selecteditem with a bar in the system's highlight color along its start edge there instead. The item's text, and an icon or a tag inside it, keep the colors your contrast theme gives them. That rule ships inbestax.cssandextras.css, not in Bulma's own stylesheet.
For Menu.Section, a section that opens and closes:
- It follows APG's
Disclosure Navigation Menu example.
The heading is a
<button type="button">witharia-expanded, andaria-controlsnaming the wrapper around its list. Enter and Space open and close it, as they do any button, and it doesn't submit a form it sits in. - The links under it are ordinary links. Each is its own tab stop, and there's no roving focus or
arrow-key handling, which is a tree's model rather than navigation's. They keep their
href, so a link still opens in a new tab, andactivestill setsaria-current="page". - A closed section's list is
hiddenandinert, so it isn't drawn, Tab skips its links, and assistive tech and find-in-page don't reach them.inertkeeps the links out of reach even if a stylesheet setsdisplayon the list and shows it. The list stays in the page, soaria-controlsalways names an element that exists, and a section that starts closed is already closed in the server's HTML, before any script runs. - Open the section that holds the current page, with
defaultOpenoropen, so the link markedaria-currentcan be seen and reached. - When the app closes a section while focus is on one of its links, as a sidebar that keeps only the current route's section open does after a link is followed, focus moves to that section's heading. Without that, the browser drops focus to the page, and the next Tab starts from the top.
labelis the button's content and its accessible name. Keep links and other buttons out of it.- In forced colors the chevron stays visible, since it's drawn as a border, and the focus ring takes the system's text color. In a right-to-left page the chevron sits at the left end of the heading and points left while closed. It turns with a short transition, which stops when the reader asks for reduced motion.
For router integrations, pass the correct as and to/href props to Menu.Item.
Related Components
Sidebar: For slide-out navigation panels using Menu.Panel: For alternate sidebar/navigation layouts.TreeView: Looks like a menu, but it's for data you browse and pick from, like a file explorer, not for navigation. Its items aren't links. Keep site navigation onMenu, withMenu.Sectionfor groups that open and close.Collapse: A panel of any content that opens and closes, whose open state works the wayMenu.Section's does.- Helper Props: All Bulma utility helpers are supported.
Additional Resources
You can use all Bulma helper props with <Menu /> and its subcomponents for powerful utility-based styling.
Props
Menu
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
children | React.ReactNode | — | Menu content (labels, lists, items, etc). |
... | All standard HTML attributes and Bulma helper props | — | See Helper Props |
Subcomponents:
Menu.Label: Bulma Menu label component.Menu.List: MenuList appliesmenu-listclass only at the top level (not for nested lists). Every level rendersrole="list", which Safari needs: WebKit stops exposing a<ul>as a list once its markers are hidden, and Bulma's reset hides them.Menu.Item: MenuItem supportsasprop for custom link components, e.g., react-router-dom Link. Give each item anhref, a router link throughas, oras="button", so the keyboard can reach it: an<a>with nohrefisn't a link, and Tab skips it.Menu.Section: A section of the menu that opens and closes: its heading is a button drawn likeMenu.Label, and itsMenu.Listof links shows while it's open. It's APG's disclosure navigation, for a sidebar of link groups: the links stay ordinary links, each its own tab stop, and a closed section's list ishiddenandinert, so Tab and assistive tech skip it. It goes directly inMenu, holding aMenu.List. Closing it while focus is on one of its links moves focus to its heading.
Menu.Label
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
children | React.ReactNode | — | Label content. |
... | All standard <p> attributes and Bulma helper props | — | See Helper Props |
Menu.List
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
children | React.ReactNode | — | List items. |
role | React.AriaRole | — | Ignored: every level renders role="list" in its place, so a menu or tree role can't go here. |
... | All standard <ul> attributes and Bulma helper props | — | See Helper Props |
Menu.Item
| Prop | Type | Default | Description |
|---|---|---|---|
as | React.ElementType | 'a' | Custom link component (e.g. Link from router). 'button' renders type="button" unless you pass type="submit" or type="reset", so an item inside a form does not submit it. |
className | string | — | Additional CSS classes for the wrapping <li>. |
children | React.ReactNode | — | Item content and optional nested MenuList. |
active | boolean | false | Marks the item as the current one. It adds Bulma's is-active class, and aria-current="page" on the element as renders, so assistive tech can tell which item is current too. An aria-current you pass wins, even undefined: use it to say something else ("location" for a section of one page, say), or aria-current={false} to turn it off. A menu should mark one page, so when the section above the current page is active too, give it aria-current="location", and give an active item that isn't navigation, such as an as="button" filter, aria-current={false}. |
style | React.CSSProperties | — | Inline styles for the wrapping <li>. |
id | string | — | id for the wrapping <li>. |
title | string | — | title for the wrapping <li>. |
role | React.AriaRole | — | ARIA role for the wrapping <li>. |
tabIndex | number | — | Tab index for the wrapping <li>. |
data-testid | string | — | Test id for the wrapping <li>. |
ref | PolymorphicRef<React.ElementType> | — | Ref forwarded to the inner element as renders, not the wrapping <li>, typed from as: the DOM node for an intrinsic tag, or whatever handle a custom component exposes. |
... | Remaining props of the element or component selected by as (default <a>) and Bulma helper props | — | See Helper Props |
Menu.Section
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes for the wrapping <div>. |
label | React.ReactNode | — | Required. The section's heading, drawn like Menu.Label inside the button that opens and closes it. Keep links and buttons out of it, since it's inside one. |
children | React.ReactNode | — | The Menu.List of links the section shows while it's open. The section itself goes directly in Menu, beside its labels and lists: inside a Menu.Item it would render inside the item's link, and inside a Menu.List it wouldn't be a list item. |
open | boolean | — | Controlled open state. If provided, the section is controlled, and its button only reports through onOpenChange. |
defaultOpen | boolean | false | Initial open state for uncontrolled usage. Open the section that holds the current page, so the reader sees where they are. |
onOpenChange | (open: boolean) => void | — | Called with the state the button asks for (true to open) each time it is clicked or activated with Enter or Space, in every mode. Pair it with open to let the button drive a controlled section. |
... | All standard <div> attributes and Bulma helper props | — | See Helper Props |
CSS & Sass Variables
Menu registers these variables on its own .menu element. Override them there (or via className) — a value set on an ancestor is only inherited, and loses to the component-level declaration. See Theme.
| CSS Variable | Sass Variable | Default |
|---|---|---|
--bulma-menu-section-gap | $menu-section-gap | 0.375em |
--bulma-menu-section-chevron-size | $menu-section-chevron-size | 1em |
--bulma-menu-section-chevron-color | $menu-section-chevron-color | currentColor |
--bulma-menu-section-chevron-duration | $menu-section-chevron-duration | var(--bulma-duration) |
--bulma-menu-item-h | $menu-item-h | var(--bulma-scheme-h) |
--bulma-menu-item-s | $menu-item-s | var(--bulma-scheme-s) |
--bulma-menu-item-l | $menu-item-l | var(--bulma-scheme-main-l) |
--bulma-menu-item-background-l | $menu-item-background-l | var(--bulma-scheme-main-l) |
--bulma-menu-item-background-l-delta | $menu-item-background-l-delta | 0% |
--bulma-menu-item-hover-background-l-delta | $menu-item-hover-background-l-delta | var(--bulma-hover-background-l-delta) |
--bulma-menu-item-active-background-l-delta | $menu-item-active-background-l-delta | var(--bulma-active-background-l-delta) |
--bulma-menu-item-color-l | $menu-item-color-l | var(--bulma-text-l) |
--bulma-menu-item-radius | $menu-item-radius | var(--bulma-radius-small) |
--bulma-menu-item-selected-h | $menu-item-selected-h | var(--bulma-link-h) |
--bulma-menu-item-selected-s | $menu-item-selected-s | var(--bulma-link-s) |
--bulma-menu-item-selected-l | $menu-item-selected-l | var(--bulma-link-l) |
--bulma-menu-item-selected-background-l | $menu-item-selected-background-l | var(--bulma-link-l) |
--bulma-menu-item-selected-color-l | $menu-item-selected-color-l | var(--bulma-link-invert-l) |
--bulma-menu-list-border-left | $menu-list-border-left | 1px solid var(--bulma-border) |
--bulma-menu-list-line-height | $menu-list-line-height | 1.25 |
--bulma-menu-list-link-padding | $menu-list-link-padding | 0.5em 0.75em |
--bulma-menu-nested-list-margin | $menu-nested-list-margin | 0.75em |
--bulma-menu-nested-list-padding-left | $menu-nested-list-padding-left | 0.75em |
--bulma-menu-label-color | $menu-label-color | var(--bulma-text-weak) |
--bulma-menu-label-font-size | $menu-label-font-size | 0.75em |
--bulma-menu-label-letter-spacing | $menu-label-letter-spacing | 0.1em |
--bulma-menu-label-spacing | $menu-label-spacing | 1em |