Skip to main content

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.

info

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>


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 Menu renders as <aside class="menu">. A menu that's navigation is better inside a <nav> with an aria-label, which gives assistive tech a navigation landmark to jump to.
  • Every Menu.List has role="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 active sets aria-current="page" on the element as renders, the link by default, so the current item isn't only a color. An aria-current you pass wins: a menu of sections on one page can say "location", and aria-current={false} turns it off.
  • Use semantic nesting (Menu.List inside Menu.Item) for submenus.
  • Give each item an href, a router link through as, or as="button", so the keyboard can reach it.
  • Menu.Item as="button" defaults to type="button", so a menu item inside a form doesn't submit it. Pass type="submit" or type="reset" and yours is used instead; any other value renders type="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-active or is-selected item 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 in bestax.css and extras.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"> with aria-expanded, and aria-controls naming 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, and active still sets aria-current="page".
  • A closed section's list is hidden and inert, so it isn't drawn, Tab skips its links, and assistive tech and find-in-page don't reach them. inert keeps the links out of reach even if a stylesheet sets display on the list and shows it. The list stays in the page, so aria-controls always 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 defaultOpen or open, so the link marked aria-current can 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.
  • label is 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.
note

For router integrations, pass the correct as and to/href props to Menu.Item.


  • 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 on Menu, with Menu.Section for groups that open and close.
  • Collapse: A panel of any content that opens and closes, whose open state works the way Menu.Section's does.
  • Helper Props: All Bulma utility helpers are supported.

Additional Resources​

Pro Tip

You can use all Bulma helper props with <Menu /> and its subcomponents for powerful utility-based styling.


Props​

PropTypeDefaultDescription
classNamestring—Additional CSS classes.
childrenReact.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 applies menu-list class only at the top level (not for nested lists). Every level renders role="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 supports as prop for custom link components, e.g., react-router-dom Link. Give each item an href, a router link through as, or as="button", so the keyboard can reach it: an <a> with no href isn't a link, and Tab skips it.
  • Menu.Section: A section of the menu that opens and closes: its heading is a button drawn like Menu.Label, and its Menu.List of 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 is hidden and inert, so Tab and assistive tech skip it. It goes directly in Menu, holding a Menu.List. Closing it while focus is on one of its links moves focus to its heading.
PropTypeDefaultDescription
classNamestring—Additional CSS classes.
childrenReact.ReactNode—Label content.
...All standard <p> attributes and Bulma helper props—See Helper Props
PropTypeDefaultDescription
classNamestring—Additional CSS classes.
childrenReact.ReactNode—List items.
roleReact.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
PropTypeDefaultDescription
asReact.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.
classNamestring—Additional CSS classes for the wrapping <li>.
childrenReact.ReactNode—Item content and optional nested MenuList.
activebooleanfalseMarks 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}.
styleReact.CSSProperties—Inline styles for the wrapping <li>.
idstring—id for the wrapping <li>.
titlestring—title for the wrapping <li>.
roleReact.AriaRole—ARIA role for the wrapping <li>.
tabIndexnumber—Tab index for the wrapping <li>.
data-testidstring—Test id for the wrapping <li>.
refPolymorphicRef<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
PropTypeDefaultDescription
classNamestring—Additional CSS classes for the wrapping <div>.
labelReact.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.
childrenReact.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.
openboolean—Controlled open state. If provided, the section is controlled, and its button only reports through onOpenChange.
defaultOpenbooleanfalseInitial 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 VariableSass VariableDefault
--bulma-menu-section-gap$menu-section-gap0.375em
--bulma-menu-section-chevron-size$menu-section-chevron-size1em
--bulma-menu-section-chevron-color$menu-section-chevron-colorcurrentColor
--bulma-menu-section-chevron-duration$menu-section-chevron-durationvar(--bulma-duration)
--bulma-menu-item-h$menu-item-hvar(--bulma-scheme-h)
--bulma-menu-item-s$menu-item-svar(--bulma-scheme-s)
--bulma-menu-item-l$menu-item-lvar(--bulma-scheme-main-l)
--bulma-menu-item-background-l$menu-item-background-lvar(--bulma-scheme-main-l)
--bulma-menu-item-background-l-delta$menu-item-background-l-delta0%
--bulma-menu-item-hover-background-l-delta$menu-item-hover-background-l-deltavar(--bulma-hover-background-l-delta)
--bulma-menu-item-active-background-l-delta$menu-item-active-background-l-deltavar(--bulma-active-background-l-delta)
--bulma-menu-item-color-l$menu-item-color-lvar(--bulma-text-l)
--bulma-menu-item-radius$menu-item-radiusvar(--bulma-radius-small)
--bulma-menu-item-selected-h$menu-item-selected-hvar(--bulma-link-h)
--bulma-menu-item-selected-s$menu-item-selected-svar(--bulma-link-s)
--bulma-menu-item-selected-l$menu-item-selected-lvar(--bulma-link-l)
--bulma-menu-item-selected-background-l$menu-item-selected-background-lvar(--bulma-link-l)
--bulma-menu-item-selected-color-l$menu-item-selected-color-lvar(--bulma-link-invert-l)
--bulma-menu-list-border-left$menu-list-border-left1px solid var(--bulma-border)
--bulma-menu-list-line-height$menu-list-line-height1.25
--bulma-menu-list-link-padding$menu-list-link-padding0.5em 0.75em
--bulma-menu-nested-list-margin$menu-nested-list-margin0.75em
--bulma-menu-nested-list-padding-left$menu-nested-list-padding-left0.75em
--bulma-menu-label-color$menu-label-colorvar(--bulma-text-weak)
--bulma-menu-label-font-size$menu-label-font-size0.75em
--bulma-menu-label-letter-spacing$menu-label-letter-spacing0.1em
--bulma-menu-label-spacing$menu-label-spacing1em