Qure UI
Components

Menubar

An application menu bar, for a workspace with more commands than a toolbar can hold.

'use client'import {  Menubar, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu,  MenubarSeparator, MenubarShortcut, MenubarTrigger,} from '@/registry/qure/ui/menubar'export default function MenubarDemo() {  return (    <Menubar>      <MenubarMenu>        <MenubarTrigger>Study</MenubarTrigger>        <MenubarContent>          <MenubarGroup>            <MenubarLabel>ACC-4471902</MenubarLabel>            <MenubarItem>              Open prior              <MenubarShortcut>⇧P</MenubarShortcut>            </MenubarItem>            <MenubarItem>              Reload series              <MenubarShortcut>⌘R</MenubarShortcut>            </MenubarItem>          </MenubarGroup>          <MenubarSeparator />          <MenubarItem>            Sign report            <MenubarShortcut>⌘⏎</MenubarShortcut>          </MenubarItem>          <MenubarItem variant="destructive">Discard draft</MenubarItem>        </MenubarContent>      </MenubarMenu>      <MenubarMenu>        <MenubarTrigger>Measure</MenubarTrigger>        <MenubarContent>          <MenubarItem>            Length            <MenubarShortcut>L</MenubarShortcut>          </MenubarItem>          <MenubarItem>            Ellipse ROI            <MenubarShortcut>E</MenubarShortcut>          </MenubarItem>          <MenubarItem>            Hounsfield probe            <MenubarShortcut>H</MenubarShortcut>          </MenubarItem>          <MenubarSeparator />          <MenubarItem>Clear all measurements</MenubarItem>        </MenubarContent>      </MenubarMenu>      <MenubarMenu>        <MenubarTrigger>Help</MenubarTrigger>        <MenubarContent>          <MenubarItem>Keyboard shortcuts</MenubarItem>          <MenubarItem>Report a problem</MenubarItem>        </MenubarContent>      </MenubarMenu>    </Menubar>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/menubar.json

Usage

import {
  Menubar, MenubarContent, MenubarItem, MenubarMenu,
  MenubarSeparator, MenubarShortcut, MenubarTrigger,
} from '@/components/ui/menubar'
<Menubar>
  <MenubarMenu>
    <MenubarTrigger>Study</MenubarTrigger>
    <MenubarContent>
      <MenubarItem>
        Open prior
        <MenubarShortcut>⇧P</MenubarShortcut>
      </MenubarItem>
      <MenubarSeparator />
      <MenubarItem variant="destructive">Discard draft</MenubarItem>
    </MenubarContent>
  </MenubarMenu>
</Menubar>

Reach for this when a surface has enough commands that a Toolbar would either overflow or hide most of them — a viewer, a report editor. For the two or three actions attached to one thing, use a Dropdown Menu; for the actions attached to a row, a Context Menu.

Composition

<Menubar>              {/* the bar; owns roving focus between menus */}
  <MenubarMenu>        {/* one menu — an ordinary Menu.Root */}
    <MenubarTrigger>   {/* "Study" */}
    <MenubarContent>   {/* portal + positioner + popup */}
      <MenubarGroup>   {/* a labelled run of items */}
        <MenubarLabel> {/* the group heading */}
      <MenubarItem>
      <MenubarSeparator />
</Menubar>

Base UI splits this in two: Menubar is only the container, and each menu inside it is a normal Menu.Root. The container is what makes the bar behave like one control rather than several buttons — left and right move between menus, and once one menu is open, hovering the next one swaps to it without a second click.

MenubarLabel is the heading of a group and only of a group. It reads the group's context to publish its aria-labelledby, so rendering one outside a MenubarGroup throws at runtime — neither tsc nor the build catches it.

'use client'import {  Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator,  MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger,} from '@/registry/qure/ui/menubar'export default function MenubarSubmenu() {  return (    <Menubar>      <MenubarMenu>        <MenubarTrigger>Export</MenubarTrigger>        <MenubarContent>          <MenubarItem>Copy accession number</MenubarItem>          <MenubarSeparator />          <MenubarSub>            <MenubarSubTrigger>Send to</MenubarSubTrigger>            <MenubarSubContent>              <MenubarItem>Referring clinician</MenubarItem>              <MenubarItem>MDT list</MenubarItem>              <MenubarSub>                <MenubarSubTrigger>Another PACS</MenubarSubTrigger>                <MenubarSubContent>                  <MenubarItem>Royal Infirmary</MenubarItem>                  <MenubarItem>Teleradiology night desk</MenubarItem>                </MenubarSubContent>              </MenubarSub>            </MenubarSubContent>          </MenubarSub>          <MenubarSub>            <MenubarSubTrigger>Download</MenubarSubTrigger>            <MenubarSubContent>              <MenubarItem>DICOM (full study)</MenubarItem>              <MenubarItem>Key images as PNG</MenubarItem>              <MenubarItem>Report as PDF</MenubarItem>            </MenubarSubContent>          </MenubarSub>        </MenubarContent>      </MenubarMenu>    </Menubar>  )}

MenubarSub nests, and MenubarSubContent already opens to the right with the offsets set. Two levels is usually the limit worth designing for: past that, the mouse path to the leaf gets long enough that people give up and use search instead.

Checkbox items

'use client'import * as React from 'react'import {  Menubar, MenubarCheckboxItem, MenubarContent, MenubarMenu, MenubarSeparator,  MenubarShortcut, MenubarTrigger,} from '@/registry/qure/ui/menubar'export default function MenubarCheckbox() {  const [heatmap, setHeatmap] = React.useState(true)  const [boxes, setBoxes] = React.useState(true)  const [measurements, setMeasurements] = React.useState(false)  return (    <Menubar>      <MenubarMenu>        <MenubarTrigger>Overlays</MenubarTrigger>        <MenubarContent>          <MenubarCheckboxItem checked={heatmap} onCheckedChange={setHeatmap}>            AI heatmap            <MenubarShortcut>⇧H</MenubarShortcut>          </MenubarCheckboxItem>          <MenubarCheckboxItem checked={boxes} onCheckedChange={setBoxes}>            Finding boxes            <MenubarShortcut>⇧B</MenubarShortcut>          </MenubarCheckboxItem>          <MenubarCheckboxItem checked={measurements} onCheckedChange={setMeasurements}>            Measurements          </MenubarCheckboxItem>          <MenubarSeparator />          <MenubarCheckboxItem checked={false} disabled>            Prior overlay — no prior on file          </MenubarCheckboxItem>        </MenubarContent>      </MenubarMenu>    </Menubar>  )}

For overlays and other things that are on or off. The indicator column is reserved for every item in the menu, so rows do not shift as things are toggled.

Radio items

Viewport layout: 1x2

'use client'import * as React from 'react'import {  Menubar, MenubarContent, MenubarMenu, MenubarRadioGroup, MenubarRadioItem,  MenubarShortcut, MenubarTrigger,} from '@/registry/qure/ui/menubar'export default function MenubarRadio() {  const [layout, setLayout] = React.useState('1x2')  return (    <div className="flex flex-col items-center gap-3">      <Menubar>        <MenubarMenu>          <MenubarTrigger>Layout</MenubarTrigger>          <MenubarContent>            <MenubarRadioGroup value={layout} onValueChange={setLayout}>              <MenubarRadioItem value="1x1">                Single                <MenubarShortcut>1</MenubarShortcut>              </MenubarRadioItem>              <MenubarRadioItem value="1x2">                Side by side                <MenubarShortcut>2</MenubarShortcut>              </MenubarRadioItem>              <MenubarRadioItem value="2x2">                Four up                <MenubarShortcut>4</MenubarShortcut>              </MenubarRadioItem>            </MenubarRadioGroup>          </MenubarContent>        </MenubarMenu>      </Menubar>      <p className="text-sm text-muted-foreground">        Viewport layout: <code>{layout}</code>      </p>    </div>  )}

For a set where exactly one applies — a viewport layout, a window preset. Wrap them in MenubarRadioGroup and drive it with value / onValueChange; the group is what makes the set announce as a set.

Disabled

'use client'import {  Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarTrigger,} from '@/registry/qure/ui/menubar'/** * A disabled row and a disabled menu. Both stay in the bar and stay * announced — a command that vanishes when it is unavailable teaches the * reader that it never existed. */export default function MenubarState() {  return (    <Menubar>      <MenubarMenu>        <MenubarTrigger>Report</MenubarTrigger>        <MenubarContent>          <MenubarItem>Save draft</MenubarItem>          <MenubarItem disabled>Sign — awaiting consultant review</MenubarItem>          <MenubarSeparator />          <MenubarItem variant="destructive">Withdraw report</MenubarItem>        </MenubarContent>      </MenubarMenu>      <MenubarMenu>        <MenubarTrigger disabled>Teaching file</MenubarTrigger>        <MenubarContent>          <MenubarItem>Add study</MenubarItem>        </MenubarContent>      </MenubarMenu>    </Menubar>  )}

A whole menu or a single row can be disabled, and both stay in place rather than disappearing. A command that vanishes when it is unavailable teaches the reader that it never existed, so they stop looking for it.

Where you can, say why in the label — "Sign — awaiting consultant review" answers the question the greyed-out row would otherwise raise.

API Reference

PropTypeDefaultDescription
variant"default" | "destructive""default"destructive tints the row red.
PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""bottom"Which side of the trigger to open on.
sideOffsetnumber6Gap from the trigger.
align"start" | "center" | "end""start"Alignment along that side.
alignOffsetnumber0Nudge along the alignment axis.

MenubarSubContent is the same component with side="right", sideOffset={4} and alignOffset={-8}.

Everything else — MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem, MenubarSub, MenubarSubTrigger, MenubarLinkItem — forwards to Base UI unchanged. See the Base UI Menu and Menubar documentation for the full API.

On this page