shshadcn-htmx

Components

Menubar

A visually-persistent, app-style horizontal bar of menus. Built on the native Popover API + the APG menu/menubar keyboard contract — arrows along the bar, ArrowDown opens a menu, type-to-find, ESC closes.

Installation

1. Install via the shadcn CLI

npx shadcn@latest add http://localhost/r/menubar.json

2. Use it

components/ui/menubar.tsx
import { Menubar, MenubarMenu, MenubarItem,
  MenubarSeparator, MenubarLabel } from "@/components/ui/menubar"

<Menubar ariaLabel="Application">
  <MenubarMenu label="File" id="mb-file">
    <MenubarItem>New File</MenubarItem>
    <MenubarItem>Open…</MenubarItem>
    <MenubarSeparator />
    <MenubarItem variant="destructive">Delete project</MenubarItem>
  </MenubarMenu>
  <MenubarMenu label="Edit" id="mb-edit">
    <MenubarItem>Undo</MenubarItem>
    <MenubarItem>Redo</MenubarItem>
  </MenubarMenu>
</Menubar>
Or copy the source manually
components/ui/menubar.tsx
/** @jsxImportSource hono/jsx */
import type { PropsWithChildren } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"

// Menubar — shadcn-htmx, htmx v4 + Tailwind v4.
//
// An app-style, visually-persistent horizontal bar of menus (File / Edit /
// View …). Each top-level item is a button that opens a submenu.
//
// shadcn-ui ships @radix-ui/react-menubar; we rebuild the SAME semantics on
// web standards instead of a JS-driven overlay:
//   - The bar is role="menubar" (default aria-orientation horizontal).
//   - Each trigger is role="menuitem" with aria-haspopup="menu" +
//     aria-expanded, and opens its submenu via the native Popover API
//     (popovertarget + popover="auto"). The platform then gives us light
//     dismiss, ESC, top-layer rendering, and focus restoration for free —
//     exactly like dropdown-menu.tsx.
//   - Submenu items are role="menuitem" and reuse the dropdown-menu item
//     styling/contract.
//
// The APG composite-widget keyboard contract (roving tabindex on the bar,
// ArrowLeft/Right along the bar, ArrowDown to open + focus first item,
// ArrowUp/Down inside a menu, Home/End, type-ahead, ESC) lives in
// public/site.js keyed on data-slot="menubar" — the platform does not
// provide composite menu navigation.
//
// Refs:
//   repos/aria-practices/content/patterns/menubar/menu-and-menubar-pattern.html
//     (Menu and Menubar pattern: roles/states + full keyboard contract)
//   repos/aria-practices/content/patterns/menubar/examples/menubar-navigation.html
//     (roving tabindex: first menuitem tabindex=0, parents carry
//      aria-haspopup + aria-expanded)
//   repos/mdn/files/en-us/web/api/popover_api/
//   repos/mdn/files/en-us/web/accessibility/aria/reference/roles/menubar_role/
//   registry/ui/dropdown-menu.tsx (submenu/popover + item styling reused here)

// --- Root --------------------------------------------------------------

const menubarBase =
  "inline-flex h-9 items-center gap-0.5 rounded-md border bg-background p-1 shadow-xs"

export function Menubar(
  props: PropsWithChildren<{
    ariaLabel?: string
    // Id of a visible element (e.g. a heading) that names the menubar.
    // APG prefers aria-labelledby over aria-label when a visible label
    // exists (menu-and-menubar-pattern.html:220-222; MDN menubar_role:32).
    ariaLabelledby?: string
    class?: ClassValue
  }>,
) {
  const { ariaLabel, ariaLabelledby, class: className, children } = props
  return (
    <div
      role="menubar"
      data-slot="menubar"
      aria-label={ariaLabel}
      aria-labelledby={ariaLabelledby}
      class={cn(menubarBase, className)}
    >
      {children}
    </div>
  )
}

// --- Menu (trigger + popover submenu) ----------------------------------

const menubarTriggerBase =
  "flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none " +
  "focus:bg-accent focus:text-accent-foreground " +
  "aria-[expanded=true]:bg-accent aria-[expanded=true]:text-accent-foreground " +
  "data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50"

const menubarContentBase =
  "z-50 m-0 min-w-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none " +
  "[&:not(:popover-open)]:hidden " +
  "[&:popover-open]:animate-[scn-popover-in_120ms_ease-out] " +
  "anchor-popover-bottom"

// MenubarMenu pairs a top-level trigger with its submenu popover. The
// trigger is the parent menuitem (aria-haspopup + aria-expanded); the
// popover is role="menu". `id` wires popovertarget → popover.
export function MenubarMenu(
  props: PropsWithChildren<{
    // Visible label of the top-level menu (e.g. "File").
    label: string
    // Unique id; the trigger's popovertarget and the menu's id.
    id: string
    disabled?: boolean
    triggerClass?: ClassValue
    contentClass?: ClassValue
  }>,
) {
  const { label, id, disabled, triggerClass, contentClass, children } = props
  return (
    <div data-slot="menubar-menu" class="contents">
      <button
        type="button"
        role="menuitem"
        // Roving tabindex: site.js promotes the first enabled trigger to
        // tabindex="0" on boot; the rest stay at -1.
        tabindex={-1}
        popovertarget={id}
        popovertargetaction="toggle"
        aria-haspopup="menu"
        aria-expanded="false"
        data-slot="menubar-trigger"
        data-menu-for={id}
        data-disabled={disabled ? "true" : undefined}
        disabled={disabled || undefined}
        class={cn(menubarTriggerBase, triggerClass)}
      >
        {label}
      </button>
      <div
        id={id}
        popover="auto"
        role="menu"
        aria-label={label}
        data-slot="menubar-content"
        data-side="bottom"
        class={cn(menubarContentBase, contentClass)}
      >
        {children}
      </div>
    </div>
  )
}

// --- Items (mirrors dropdown-menu item contract) -----------------------

const itemBase =
  "relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none " +
  "focus:bg-accent focus:text-accent-foreground " +
  "data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 " +
  "[&_svg]:size-4 [&_svg]:shrink-0"

export type MenubarItemVariant = "default" | "destructive"

const variantMap: Record<MenubarItemVariant, string> = {
  default: "",
  destructive: "text-destructive focus:bg-destructive/10 focus:text-destructive",
}

type MenubarItemProps = PropsWithChildren<{
  onclick?: string
  href?: string
  disabled?: boolean
  variant?: MenubarItemVariant
  class?: ClassValue
  "hx-get"?: string
  "hx-post"?: string
  "hx-target"?: string
  "hx-swap"?: string
  "hx-trigger"?: string
}>

export function MenubarItem(props: MenubarItemProps) {
  const { children, onclick, href, disabled, variant = "default", class: className, ...rest } = props
  const Tag: any = href ? "a" : "button"
  return (
    <Tag
      role="menuitem"
      type={href ? undefined : "button"}
      tabindex={-1}
      href={href}
      onclick={onclick}
      data-slot="menubar-item"
      data-disabled={disabled ? "true" : undefined}
      class={cn(itemBase, variantMap[variant], className)}
      {...rest}
    >
      {children}
    </Tag>
  )
}

export function MenubarSeparator(props: { class?: ClassValue }) {
  return (
    <div
      role="separator"
      aria-orientation="horizontal"
      data-slot="menubar-separator"
      class={cn("-mx-1 my-1 h-px bg-border", props.class)}
    />
  )
}

export function MenubarLabel(props: PropsWithChildren<{ class?: ClassValue }>) {
  return (
    <div
      data-slot="menubar-label"
      class={cn("px-2 py-1.5 text-xs font-semibold tracking-wider text-muted-foreground uppercase", props.class)}
    >
      {props.children}
    </div>
  )
}

// MenubarShortcut — presentational accelerator hint (e.g. "⌘S") shown at
// the trailing edge of a MenubarItem. aria-hidden so AT does not announce
// the visual glyph; expose the real shortcut to AT via aria-keyshortcuts on
// the MenubarItem itself (passed through with the rest spread). See
// repos/mdn/files/en-us/web/accessibility/aria/reference/attributes/aria-keyshortcuts/index.md
export function MenubarShortcut(props: PropsWithChildren<{ class?: ClassValue }>) {
  return (
    <span
      aria-hidden="true"
      data-slot="menubar-shortcut"
      class={cn("ml-auto text-xs tracking-widest text-muted-foreground", props.class)}
    >
      {props.children}
    </span>
  )
}

1. Save the file

Copy menubar.html into templates/components/.

2. Use it

templates/components/menubar.html
{% from "components/menubar.html" import
   menubar_open, menubar_close, menu_open, menu_close,
   menubar_item, menubar_separator, menubar_label %}

{{ menubar_open(aria_label="Application") }}
  {{ menu_open("File", id="mb-file") }}
    {{ menubar_item("New File") }}
    {{ menubar_item("Open…") }}
    {{ menubar_separator() }}
    {{ menubar_item("Delete project", variant="destructive") }}
  {{ menu_close() }}
  {{ menu_open("Edit", id="mb-edit") }}
    {{ menubar_item("Undo") }}
    {{ menubar_item("Redo") }}
  {{ menu_close() }}
{{ menubar_close() }}
View source
templates/components/menubar.html
{# Menubar macros — shadcn-htmx, htmx v4 + Tailwind v4.
   App-style horizontal menubar. role="menubar" > role="menuitem" triggers
   open native [popover] submenus (role="menu"). The APG composite keyboard
   contract (roving tabindex, ArrowLeft/Right, ArrowDown to open, etc.) is
   wired up in public/site.js keyed on data-slot="menubar". Mirrors
   registry/ui/menubar.tsx exactly. #}

{# aria_labelledby: id of a visible element naming the menubar. APG prefers
   aria-labelledby over aria-label when a visible label exists
   (menu-and-menubar-pattern.html:220-222; MDN menubar_role:32). #}
{% macro menubar_open(aria_label=none, aria_labelledby=none, extra_class="") %}
<div role="menubar" data-slot="menubar"
     {%- if aria_label %} aria-label="{{ aria_label }}"{% endif %}
     {%- if aria_labelledby %} aria-labelledby="{{ aria_labelledby }}"{% endif %}
     class="inline-flex h-9 items-center gap-0.5 rounded-md border bg-background p-1 shadow-xs {{ extra_class }}">
{% endmacro %}

{% macro menubar_close() %}</div>{% endmacro %}

{# menu_open / menu_close wrap one top-level menu: trigger + popover submenu.
   `id` is the popovertarget + popover id. Put items between them. #}
{% macro menu_open(label, id, disabled=false, trigger_class="", content_class="") %}
<div data-slot="menubar-menu" class="contents">
  <button type="button" role="menuitem" tabindex="-1"
          popovertarget="{{ id }}" popovertargetaction="toggle"
          aria-haspopup="menu" aria-expanded="false"
          data-slot="menubar-trigger" data-menu-for="{{ id }}"
          {%- if disabled %} data-disabled="true" disabled{% endif %}
          class="flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground aria-[expanded=true]:bg-accent aria-[expanded=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 {{ trigger_class }}">{{ label }}</button>
  <div id="{{ id }}" popover="auto" role="menu" aria-label="{{ label }}"
       data-slot="menubar-content" data-side="bottom"
       class="z-50 m-0 min-w-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none [&:not(:popover-open)]:hidden [&:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-popover-bottom {{ content_class }}">
{% endmacro %}

{% macro menu_close() %}</div></div>{% endmacro %}

{% macro menubar_item(label, href=none, onclick=none, disabled=false, variant="default", extra_class="", **attrs) %}
{%- set base -%}
relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0
{%- endset -%}
{%- set destructive -%}
{% if variant == "destructive" %}text-destructive focus:bg-destructive/10 focus:text-destructive{% endif %}
{%- endset -%}
{% if href %}
<a role="menuitem" tabindex="-1" href="{{ href }}"
   data-slot="menubar-item"
   {%- if disabled %} data-disabled="true"{% endif %}
   class="{{ base }} {{ destructive }} {{ extra_class }}"
   {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>{{ label }}</a>
{% else %}
<button type="button" role="menuitem" tabindex="-1"
        {% if onclick %}onclick="{{ onclick }}"{% endif %}
        data-slot="menubar-item"
        {%- if disabled %} data-disabled="true"{% endif %}
        class="{{ base }} {{ destructive }} {{ extra_class }}"
        {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>{{ label }}</button>
{% endif %}
{% endmacro %}

{% macro menubar_separator() %}
<div role="separator" aria-orientation="horizontal" data-slot="menubar-separator" class="-mx-1 my-1 h-px bg-border"></div>
{% endmacro %}

{% macro menubar_label(text) %}
<div data-slot="menubar-label" class="px-2 py-1.5 text-xs font-semibold tracking-wider text-muted-foreground uppercase">{{ text }}</div>
{% endmacro %}

{# menubar_shortcut: presentational accelerator hint (e.g. "⌘S") at the
   trailing edge of an item. aria-hidden so AT does not announce the glyph;
   expose the real shortcut via aria-keyshortcuts on menubar_item.
   repos/.../attributes/aria-keyshortcuts/index.md #}
{% macro menubar_shortcut(text) %}
<span aria-hidden="true" data-slot="menubar-shortcut" class="ml-auto text-xs tracking-widest text-muted-foreground">{{ text }}</span>
{% endmacro %}

1. Save the file

Add menubar.tmpl alongside your other templates.

2. Use it

components/menubar.tmpl
{{template "menubar" (dict "AriaLabel" "Application" "Body" (htmlSafe (printf "%s%s"
  (template "menubar_menu" (dict "Label" "File" "ID" "mb-file" "Body" (htmlSafe `
    …items…`)))
  (template "menubar_menu" (dict "Label" "Edit" "ID" "mb-edit" "Body" (htmlSafe `
    …items…`))))))}}
View source
components/menubar.tmpl
{{/* Menubar templates — shadcn-htmx, htmx v4 + Tailwind v4.
     App-style horizontal menubar. role="menubar" > role="menuitem" triggers
     open native [popover] submenus (role="menu"). The APG composite keyboard
     contract lives in public/site.js keyed on data-slot="menubar".
     Mirrors registry/ui/menubar.tsx exactly. */}}

{{/* AriaLabelledby: id of a visible element naming the menubar. APG prefers
     aria-labelledby over aria-label when a visible label exists
     (menu-and-menubar-pattern.html:220-222; MDN menubar_role:32). */}}
{{define "menubar"}}
<div role="menubar" data-slot="menubar"
     {{if .AriaLabel}}aria-label="{{.AriaLabel}}"{{end}}
     {{if .AriaLabelledby}}aria-labelledby="{{.AriaLabelledby}}"{{end}}
     class="inline-flex h-9 items-center gap-0.5 rounded-md border bg-background p-1 shadow-xs {{.Class}}">
  {{.Body}}
</div>
{{end}}

{{/* menubar_menu renders one top-level menu: trigger + popover submenu.
     ID is the popovertarget + popover id; Body holds the items. */}}
{{define "menubar_menu"}}
<div data-slot="menubar-menu" class="contents">
  <button type="button" role="menuitem" tabindex="-1"
          popovertarget="{{.ID}}" popovertargetaction="toggle"
          aria-haspopup="menu" aria-expanded="false"
          data-slot="menubar-trigger" data-menu-for="{{.ID}}"
          {{if .Disabled}}data-disabled="true" disabled{{end}}
          class="flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground aria-[expanded=true]:bg-accent aria-[expanded=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 {{.TriggerClass}}">{{.Label}}</button>
  <div id="{{.ID}}" popover="auto" role="menu" aria-label="{{.Label}}"
       data-slot="menubar-content" data-side="bottom"
       class="z-50 m-0 min-w-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none [&:not(:popover-open)]:hidden [&:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-popover-bottom {{.ContentClass}}">
    {{.Body}}
  </div>
</div>
{{end}}

{{define "menubar_item"}}
{{- $variant := or .Variant "default" -}}
{{- $base := "relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0" -}}
{{- $destr := "" -}}{{- if eq $variant "destructive" -}}{{- $destr = "text-destructive focus:bg-destructive/10 focus:text-destructive" -}}{{- end -}}
{{if .Href}}
<a role="menuitem" tabindex="-1" href="{{.Href}}" data-slot="menubar-item"
   {{if .Disabled}}data-disabled="true"{{end}}
   class="{{$base}} {{$destr}}"
   {{- range $k, $v := .Attrs}} {{$k}}="{{$v}}"{{end -}}
>{{.Label}}</a>
{{else}}
<button type="button" role="menuitem" tabindex="-1" data-slot="menubar-item"
        {{if .Disabled}}data-disabled="true"{{end}}
        {{- if .Onclick}} onclick="{{.Onclick}}"{{end}}
        class="{{$base}} {{$destr}}"
        {{- range $k, $v := .Attrs}} {{$k}}="{{$v}}"{{end -}}
>{{.Label}}</button>
{{end}}
{{end}}

{{define "menubar_separator"}}
<div role="separator" aria-orientation="horizontal" data-slot="menubar-separator" class="-mx-1 my-1 h-px bg-border"></div>
{{end}}

{{define "menubar_label"}}
<div data-slot="menubar-label" class="px-2 py-1.5 text-xs font-semibold tracking-wider text-muted-foreground uppercase">{{.Text}}</div>
{{end}}

{{/* menubar_shortcut: presentational accelerator hint (e.g. "⌘S") at the
     trailing edge of an item. aria-hidden so AT does not announce the glyph;
     expose the real shortcut via aria-keyshortcuts on menubar_item.
     repos/.../attributes/aria-keyshortcuts/index.md */}}
{{define "menubar_shortcut"}}
<span aria-hidden="true" data-slot="menubar-shortcut" class="ml-auto text-xs tracking-widest text-muted-foreground">{{.Text}}</span>
{{end}}

1. Save the file

Drop menubar.ex into lib/my_app_web/components/.

2. Use it

lib/my_app_web/components/menubar.ex
<.menubar aria_label="Application">
  <.menubar_menu label="File" id="mb-file">
    <.menubar_item>New File</.menubar_item>
    <.menubar_item>Open</.menubar_item>
    <.menubar_separator />
    <.menubar_item variant="destructive">Delete project</.menubar_item>
  </.menubar_menu>
  <.menubar_menu label="Edit" id="mb-edit">
    <.menubar_item>Undo</.menubar_item>
    <.menubar_item>Redo</.menubar_item>
  </.menubar_menu>
</.menubar>
View source
lib/my_app_web/components/menubar.ex
defmodule ShadcnHtmx.Components.Menubar do
  @moduledoc """
  Menubar — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.

  An app-style, visually-persistent horizontal bar of menus. The bar is
  `role="menubar"`; each top-level item is a `role="menuitem"` button with
  `aria-haspopup="menu"` + `aria-expanded` that opens its submenu via the
  native Popover API (`popovertarget` + `popover="auto"`). The APG composite
  keyboard contract (roving tabindex, ArrowLeft/Right, ArrowDown to open,
  ArrowUp/Down inside a menu, Home/End, type-ahead, ESC) is wired up in
  public/site.js keyed on `data-slot="menubar"`.

  ## Examples

      <.menubar aria_label="Application">
        <.menubar_menu label="File" id="mb-file">
          <.menubar_item>New File</.menubar_item>
          <.menubar_item>Open…</.menubar_item>
          <.menubar_separator />
          <.menubar_item variant="destructive">Delete project</.menubar_item>
        </.menubar_menu>
        <.menubar_menu label="Edit" id="mb-edit">
          <.menubar_item>Undo</.menubar_item>
          <.menubar_item>Redo</.menubar_item>
        </.menubar_menu>
      </.menubar>
  """

  use Phoenix.Component

  attr :aria_label, :string, default: nil
  # aria_labelledby: id of a visible element naming the menubar. APG prefers
  # aria-labelledby over aria-label when a visible label exists
  # (menu-and-menubar-pattern.html:220-222; MDN menubar_role:32).
  attr :aria_labelledby, :string, default: nil
  attr :class, :string, default: nil
  attr :rest, :global
  slot :inner_block, required: true

  def menubar(assigns) do
    ~H"""
    <div
      role="menubar"
      data-slot="menubar"
      aria-label={@aria_label}
      aria-labelledby={@aria_labelledby}
      class={[
        "inline-flex h-9 items-center gap-0.5 rounded-md border bg-background p-1 shadow-xs",
        @class
      ]}
      {@rest}
    >
      {render_slot(@inner_block)}
    </div>
    """
  end

  attr :label, :string, required: true
  attr :id, :string, required: true
  attr :disabled, :boolean, default: false
  attr :trigger_class, :string, default: nil
  attr :content_class, :string, default: nil
  slot :inner_block, required: true

  def menubar_menu(assigns) do
    ~H"""
    <div data-slot="menubar-menu" class="contents">
      <button
        type="button"
        role="menuitem"
        tabindex="-1"
        popovertarget={@id}
        popovertargetaction="toggle"
        aria-haspopup="menu"
        aria-expanded="false"
        data-slot="menubar-trigger"
        data-menu-for={@id}
        data-disabled={@disabled && "true"}
        disabled={@disabled}
        class={[
          "flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none",
          "focus:bg-accent focus:text-accent-foreground",
          "aria-[expanded=true]:bg-accent aria-[expanded=true]:text-accent-foreground",
          "data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
          @trigger_class
        ]}
      >
        {@label}
      </button>
      <div
        id={@id}
        popover="auto"
        role="menu"
        aria-label={@label}
        data-slot="menubar-content"
        data-side="bottom"
        class={[
          "z-50 m-0 min-w-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none",
          "[&:not(:popover-open)]:hidden",
          "[&:popover-open]:animate-[scn-popover-in_120ms_ease-out]",
          "anchor-popover-bottom",
          @content_class
        ]}
      >
        {render_slot(@inner_block)}
      </div>
    </div>
    """
  end

  attr :href, :string, default: nil
  attr :disabled, :boolean, default: false
  attr :variant, :string, default: "default", values: ~w(default destructive)
  attr :class, :string, default: nil
  attr :rest, :global
  slot :inner_block, required: true

  def menubar_item(assigns) do
    base =
      "relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none " <>
        "focus:bg-accent focus:text-accent-foreground " <>
        "data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 " <>
        "[&_svg]:size-4 [&_svg]:shrink-0"

    destr =
      if assigns.variant == "destructive",
        do: "text-destructive focus:bg-destructive/10 focus:text-destructive",
        else: ""

    assigns = assign(assigns, base: base, destr: destr)

    cond do
      assigns.href ->
        ~H"""
        <a
          role="menuitem"
          tabindex="-1"
          href={@href}
          data-slot="menubar-item"
          data-disabled={@disabled && "true"}
          class={[@base, @destr, @class]}
          {@rest}
        >
          {render_slot(@inner_block)}
        </a>
        """

      true ->
        ~H"""
        <button
          type="button"
          role="menuitem"
          tabindex="-1"
          data-slot="menubar-item"
          data-disabled={@disabled && "true"}
          class={[@base, @destr, @class]}
          {@rest}
        >
          {render_slot(@inner_block)}
        </button>
        """
    end
  end

  def menubar_separator(assigns) do
    ~H"""
    <div
      role="separator"
      aria-orientation="horizontal"
      data-slot="menubar-separator"
      class="-mx-1 my-1 h-px bg-border"
    />
    """
  end

  attr :class, :string, default: nil
  slot :inner_block, required: true

  def menubar_label(assigns) do
    ~H"""
    <div
      data-slot="menubar-label"
      class={[
        "px-2 py-1.5 text-xs font-semibold tracking-wider text-muted-foreground uppercase",
        @class
      ]}
    >
      {render_slot(@inner_block)}
    </div>
    """
  end

  attr :class, :string, default: nil
  slot :inner_block, required: true

  @doc """
  Presentational accelerator hint (e.g. "⌘S") at the trailing edge of an
  item. aria-hidden so AT does not announce the glyph; expose the real
  shortcut via aria-keyshortcuts on menubar_item.
  repos/.../attributes/aria-keyshortcuts/index.md
  """
  def menubar_shortcut(assigns) do
    ~H"""
    <span
      aria-hidden="true"
      data-slot="menubar-shortcut"
      class={["ml-auto text-xs tracking-widest text-muted-foreground", @class]}
    >
      {render_slot(@inner_block)}
    </span>
    """
  end
end

1. Save the file

Includes the keyboard contract script. Copy once per page; it relies only on the theme tokens in styles.css.

2. Use it

snippets/menubar.html
<div role="menubar" data-slot="menubar" aria-label="Application" class="…">
  <div data-slot="menubar-menu" class="contents">
    <button role="menuitem" tabindex="-1" popovertarget="mb-file"
            popovertargetaction="toggle" aria-haspopup="menu"
            aria-expanded="false" data-slot="menubar-trigger"
            data-menu-for="mb-file" class="…">File</button>
    <div id="mb-file" popover="auto" role="menu" aria-label="File"
         data-slot="menubar-content" data-side="bottom" class="…">
      <button role="menuitem" tabindex="-1" data-slot="menubar-item">New File</button>
    </div>
  </div>
</div>
View source
snippets/menubar.html
<!--
  shadcn-htmx — raw HTML menubar snippet.

  App-style horizontal menubar. role="menubar" > role="menuitem" triggers
  open native [popover] submenus (role="menu"). Native [popover] gives
  open/close + light dismiss + ESC + top-layer; the inline script below adds
  the APG composite keyboard contract (roving tabindex, ArrowLeft/Right along
  the bar, ArrowDown to open a menu, ArrowUp/Down inside a menu, Home/End,
  type-ahead). Relies only on the theme tokens in styles.css.

  Naming: this example uses aria-label. When a VISIBLE heading names the bar,
  prefer aria-labelledby="<heading-id>" instead (APG menu-and-menubar-pattern
  :220-222; MDN menubar_role:32).
-->

<div role="menubar" data-slot="menubar" aria-label="Application"
     class="inline-flex h-9 items-center gap-0.5 rounded-md border bg-background p-1 shadow-xs">

  <div data-slot="menubar-menu" class="contents">
    <button type="button" role="menuitem" tabindex="-1"
            popovertarget="mb-file" popovertargetaction="toggle"
            aria-haspopup="menu" aria-expanded="false"
            data-slot="menubar-trigger" data-menu-for="mb-file"
            class="flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground aria-[expanded=true]:bg-accent aria-[expanded=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50">File</button>
    <div id="mb-file" popover="auto" role="menu" aria-label="File"
         data-slot="menubar-content" data-side="bottom"
         class="z-50 m-0 min-w-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none [&:not(:popover-open)]:hidden [&:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-popover-bottom">
      <!-- aria-keyshortcuts exposes the accelerator to AT; the trailing
           data-slot="menubar-shortcut" span is the aria-hidden visual hint.
           repos/.../attributes/aria-keyshortcuts/index.md -->
      <button type="button" role="menuitem" tabindex="-1" data-slot="menubar-item" aria-keyshortcuts="Control+N" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0">New File<span aria-hidden="true" data-slot="menubar-shortcut" class="ml-auto text-xs tracking-widest text-muted-foreground">⌃N</span></button>
      <button type="button" role="menuitem" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0">Open…</button>
      <div role="separator" aria-orientation="horizontal" data-slot="menubar-separator" class="-mx-1 my-1 h-px bg-border"></div>
      <button type="button" role="menuitem" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 text-destructive focus:bg-destructive/10 focus:text-destructive">Delete project</button>
    </div>
  </div>

  <div data-slot="menubar-menu" class="contents">
    <button type="button" role="menuitem" tabindex="-1"
            popovertarget="mb-edit" popovertargetaction="toggle"
            aria-haspopup="menu" aria-expanded="false"
            data-slot="menubar-trigger" data-menu-for="mb-edit"
            class="flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground aria-[expanded=true]:bg-accent aria-[expanded=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50">Edit</button>
    <div id="mb-edit" popover="auto" role="menu" aria-label="Edit"
         data-slot="menubar-content" data-side="bottom"
         class="z-50 m-0 min-w-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none [&:not(:popover-open)]:hidden [&:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-popover-bottom">
      <button type="button" role="menuitem" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0">Undo</button>
      <button type="button" role="menuitem" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0">Redo</button>
    </div>
  </div>
</div>

<!-- Keyboard contract — copy once per page -->
<script>
  document.querySelectorAll('[data-slot="menubar"]').forEach(function (bar) {
    var triggers = function () {
      return [].slice.call(bar.querySelectorAll('[data-slot="menubar-trigger"]:not([disabled])'))
    }
    // Roving tabindex: first enabled trigger is the single tab stop.
    var ts = triggers()
    ts.forEach(function (t, i) { t.setAttribute('tabindex', i === 0 ? '0' : '-1') })
    var roll = function (target) {
      triggers().forEach(function (t) { t.setAttribute('tabindex', '-1') })
      target.setAttribute('tabindex', '0')
      target.focus()
    }
    var menuOf = function (trig) { return document.getElementById(trig.getAttribute('data-menu-for')) }
    bar.addEventListener('keydown', function (e) {
      var trig = e.target.closest && e.target.closest('[data-slot="menubar-trigger"]')
      if (!trig) return
      var list = triggers(); var i = list.indexOf(trig)
      if (e.key === 'ArrowRight') { e.preventDefault(); roll(list[(i + 1) % list.length]) }
      else if (e.key === 'ArrowLeft') { e.preventDefault(); roll(list[(i - 1 + list.length) % list.length]) }
      else if (e.key === 'Home') { e.preventDefault(); roll(list[0]) }
      else if (e.key === 'End') { e.preventDefault(); roll(list[list.length - 1]) }
      else if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowUp') {
        var menu = menuOf(trig); if (!menu || typeof menu.showPopover !== 'function') return
        e.preventDefault()
        if (!menu.matches(':popover-open')) menu.showPopover()
        var items = [].slice.call(menu.querySelectorAll('[role="menuitem"]:not([data-disabled="true"])'))
        if (items.length) setTimeout(function () { items[e.key === 'ArrowUp' ? items.length - 1 : 0].focus() }, 0)
      }
    })
    // Reflect open/closed state onto aria-expanded.
    bar.querySelectorAll('[data-slot="menubar-content"]').forEach(function (menu) {
      menu.addEventListener('toggle', function (e) {
        var trig = bar.querySelector('[data-menu-for="' + CSS.escape(menu.id) + '"]')
        if (trig) trig.setAttribute('aria-expanded', e.newState === 'open' ? 'true' : 'false')
      })
    })
  })
  // Inside a submenu: ArrowUp/Down/Home/End/type-ahead + ArrowLeft/Right hop bars.
  document.addEventListener('keydown', function (e) {
    var item = e.target.closest && e.target.closest('[data-slot="menubar-content"] [role="menuitem"]')
    if (!item) return
    var menu = item.closest('[data-slot="menubar-content"]'); if (!menu) return
    var bar = menu.closest('[data-slot="menubar"]'); if (!bar) return
    var items = [].slice.call(menu.querySelectorAll('[role="menuitem"]:not([data-disabled="true"])'))
    var i = items.indexOf(item)
    var bars = [].slice.call(bar.querySelectorAll('[data-slot="menubar-trigger"]:not([disabled])'))
    var trig = bar.querySelector('[data-menu-for="' + CSS.escape(menu.id) + '"]')
    var bi = bars.indexOf(trig)
    var hop = function (dir) {
      menu.hidePopover()
      var next = bars[(bi + dir + bars.length) % bars.length]
      bars.forEach(function (t) { t.setAttribute('tabindex', '-1') })
      next.setAttribute('tabindex', '0')
      var nm = document.getElementById(next.getAttribute('data-menu-for'))
      if (nm && typeof nm.showPopover === 'function') {
        nm.showPopover()
        var ni = [].slice.call(nm.querySelectorAll('[role="menuitem"]:not([data-disabled="true"])'))
        if (ni.length) setTimeout(function () { ni[0].focus() }, 0)
      } else next.focus()
    }
    if (e.key === 'ArrowDown') { e.preventDefault(); items[(i + 1) % items.length].focus() }
    else if (e.key === 'ArrowUp') { e.preventDefault(); items[(i - 1 + items.length) % items.length].focus() }
    else if (e.key === 'Home') { e.preventDefault(); items[0].focus() }
    else if (e.key === 'End') { e.preventDefault(); items[items.length - 1].focus() }
    else if (e.key === 'ArrowRight') { e.preventDefault(); hop(1) }
    else if (e.key === 'ArrowLeft') { e.preventDefault(); hop(-1) }
    else if (e.key.length === 1 && /\S/.test(e.key) && !e.ctrlKey && !e.metaKey && !e.altKey) {
      var ch = e.key.toLowerCase()
      for (var k = 1; k <= items.length; k++) {
        var c = items[(i + k) % items.length]
        if ((c.textContent || '').trim().toLowerCase().startsWith(ch)) { c.focus(); break }
      }
    }
  })
  // Click a leaf item closes its menu (APG: items perform an action + close).
  document.addEventListener('click', function (e) {
    var item = e.target.closest && e.target.closest('[data-slot="menubar-item"]')
    if (!item || item.getAttribute('data-disabled') === 'true') return
    var menu = item.closest('[data-slot="menubar-content"]')
    if (menu && typeof menu.hidePopover === 'function') menu.hidePopover()
  })
</script>

Examples

App menubar — open a menu, then keyboard takes over

Tab onto the bar, ←/→ move between menus, ↓ opens a menu and focuses its first item, ↑/↓ cycle inside it, ESC closes, Enter activates. Type a letter to jump.

The menubar is a composite widget: the whole bar is a single tab stop with a roving tabindex, so Tab enters/leaves it rather than walking each menu. Each trigger is a role="menuitem" that opens a popover submenu, so the browser handles light-dismiss, ESC, and focus restoration; site.js adds the arrow-key contract.

<Menubar ariaLabel="Application">
  <MenubarMenu label="File" id="mb-file">
    <MenubarLabel>File</MenubarLabel>
    <MenubarItem>New File</MenubarItem>
    <MenubarItem>Open…</MenubarItem>
    <MenubarSeparator />
    <MenubarItem variant="destructive">Delete project</MenubarItem>
  </MenubarMenu>
  <MenubarMenu label="Edit" id="mb-edit">
    <MenubarItem>Undo</MenubarItem>
    <MenubarItem>Redo</MenubarItem>
  </MenubarMenu>
</Menubar>
{{ menubar_open(aria_label="Application") }}
  {{ menu_open("File", id="mb-file") }}
    {{ menubar_item("New File") }}
    {{ menubar_item("Open…") }}
    {{ menubar_separator() }}
    {{ menubar_item("Delete project", variant="destructive") }}
  {{ menu_close() }}
{{ menubar_close() }}
{{template "menubar" (dict "AriaLabel" "Application" "Body" (htmlSafe `…menus…`))}}
<.menubar aria_label="Application">
  <.menubar_menu label="File" id="mb-file">
    <.menubar_item>New File</.menubar_item>
    <.menubar_item variant="destructive">Delete project</.menubar_item>
  </.menubar_menu>
</.menubar>
<div class="flex items-center justify-center">
  <div role="menubar" data-slot="menubar" aria-label="Application" class="inline-flex h-9 items-center gap-0.5 rounded-md border bg-background p-1 shadow-xs">
    <div data-slot="menubar-menu" class="contents">
      <button type="button" role="menuitem" tabindex="-1" popovertarget="ex-mb-file" popovertargetaction="toggle" aria-haspopup="menu" aria-expanded="false" data-slot="menubar-trigger" data-menu-for="ex-mb-file" class="flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground aria-[expanded=true]:bg-accent aria-[expanded=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50">File</button>
      <div id="ex-mb-file" popover="auto" role="menu" aria-label="File" data-slot="menubar-content" data-side="bottom" class="z-50 m-0 min-w-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none [&amp;:not(:popover-open)]:hidden [&amp;:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-popover-bottom">
        <div data-slot="menubar-label" class="px-2 py-1.5 text-xs font-semibold tracking-wider text-muted-foreground uppercase">File</div>
        <button role="menuitem" type="button" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">New File</button>
        <button role="menuitem" type="button" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Open…</button>
        <button role="menuitem" type="button" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Save</button>
        <div role="separator" aria-orientation="horizontal" data-slot="menubar-separator" class="-mx-1 my-1 h-px bg-border">
        </div>
        <button role="menuitem" type="button" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 text-destructive focus:bg-destructive/10 focus:text-destructive">Delete project</button>
      </div>
    </div>
    <div data-slot="menubar-menu" class="contents">
      <button type="button" role="menuitem" tabindex="-1" popovertarget="ex-mb-edit" popovertargetaction="toggle" aria-haspopup="menu" aria-expanded="false" data-slot="menubar-trigger" data-menu-for="ex-mb-edit" class="flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground aria-[expanded=true]:bg-accent aria-[expanded=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50">Edit</button>
      <div id="ex-mb-edit" popover="auto" role="menu" aria-label="Edit" data-slot="menubar-content" data-side="bottom" class="z-50 m-0 min-w-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none [&amp;:not(:popover-open)]:hidden [&amp;:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-popover-bottom">
        <button role="menuitem" type="button" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Undo</button>
        <button role="menuitem" type="button" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Redo</button>
        <div role="separator" aria-orientation="horizontal" data-slot="menubar-separator" class="-mx-1 my-1 h-px bg-border">
        </div>
        <button role="menuitem" type="button" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Cut</button>
        <button role="menuitem" type="button" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Copy</button>
        <button role="menuitem" type="button" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Paste</button>
      </div>
    </div>
    <div data-slot="menubar-menu" class="contents">
      <button type="button" role="menuitem" tabindex="-1" popovertarget="ex-mb-view" popovertargetaction="toggle" aria-haspopup="menu" aria-expanded="false" data-slot="menubar-trigger" data-menu-for="ex-mb-view" class="flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground aria-[expanded=true]:bg-accent aria-[expanded=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50">View</button>
      <div id="ex-mb-view" popover="auto" role="menu" aria-label="View" data-slot="menubar-content" data-side="bottom" class="z-50 m-0 min-w-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none [&amp;:not(:popover-open)]:hidden [&amp;:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-popover-bottom">
        <button role="menuitem" type="button" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Zoom In</button>
        <button role="menuitem" type="button" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Zoom Out</button>
        <button role="menuitem" type="button" tabindex="-1" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Reset Zoom</button>
      </div>
    </div>
  </div>
</div>

Navigation menubar — link items

Menu items can be links (href). Each section opens a submenu of in-app links. ESC closes; clicking a link navigates.

APG notes the menubar pattern is heavier than most site navigation needs — a disclosure is usually a better fit. Reach for menubar when you are building an actual application chrome (an editor, a desktop-style app) where the persistent command bar matches user expectations.

<Menubar ariaLabel="Mythical University">
  <MenubarMenu label="About" id="mb-about">
    <MenubarItem href="/overview">Overview</MenubarItem>
    <MenubarItem href="/facts">Facts</MenubarItem>
  </MenubarMenu>
</Menubar>
{{ menu_open("About", id="mb-about") }}
  {{ menubar_item("Overview", href="/overview") }}
  {{ menubar_item("Facts", href="/facts") }}
{{ menu_close() }}
{{template "menubar_item" (dict "Label" "Overview" "Href" "/overview")}}
<.menubar_menu label="About" id="mb-about">
  <.menubar_item href="/overview">Overview</.menubar_item>
</.menubar_menu>
<div class="flex items-center justify-center">
  <div role="menubar" data-slot="menubar" aria-label="Mythical University" class="inline-flex h-9 items-center gap-0.5 rounded-md border bg-background p-1 shadow-xs">
    <div data-slot="menubar-menu" class="contents">
      <button type="button" role="menuitem" tabindex="-1" popovertarget="ex-mb-about" popovertargetaction="toggle" aria-haspopup="menu" aria-expanded="false" data-slot="menubar-trigger" data-menu-for="ex-mb-about" class="flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground aria-[expanded=true]:bg-accent aria-[expanded=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50">About</button>
      <div id="ex-mb-about" popover="auto" role="menu" aria-label="About" data-slot="menubar-content" data-side="bottom" class="z-50 m-0 min-w-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none [&amp;:not(:popover-open)]:hidden [&amp;:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-popover-bottom">
        <a role="menuitem" tabindex="-1" href="#overview" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Overview</a>
        <a role="menuitem" tabindex="-1" href="#administration" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Administration</a>
        <a role="menuitem" tabindex="-1" href="#facts" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Facts</a>
      </div>
    </div>
    <div data-slot="menubar-menu" class="contents">
      <button type="button" role="menuitem" tabindex="-1" popovertarget="ex-mb-admissions" popovertargetaction="toggle" aria-haspopup="menu" aria-expanded="false" data-slot="menubar-trigger" data-menu-for="ex-mb-admissions" class="flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground aria-[expanded=true]:bg-accent aria-[expanded=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50">Admissions</button>
      <div id="ex-mb-admissions" popover="auto" role="menu" aria-label="Admissions" data-slot="menubar-content" data-side="bottom" class="z-50 m-0 min-w-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none [&amp;:not(:popover-open)]:hidden [&amp;:popover-open]:animate-[scn-popover-in_120ms_ease-out] anchor-popover-bottom">
        <a role="menuitem" tabindex="-1" href="#apply" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Apply</a>
        <a role="menuitem" tabindex="-1" href="#visit" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Visit</a>
        <a role="menuitem" tabindex="-1" href="#tuition" data-slot="menubar-item" class="relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&amp;_svg]:size-4 [&amp;_svg]:shrink-0">Tuition &amp; Aid</a>
      </div>
    </div>
  </div>
</div>

API Reference

<Menubar>

PropTypeDefaultDescription
ariaLabelledbystring
Id of a visible element (e.g. a heading) that names the menubar. APG prefers aria-labelledby over aria-label when a visible label exists (alternative to ariaLabel).MDNmenubar role
<MenubarShortcut>Child
Presentational accelerator hint (e.g. "⌘S") rendered at the trailing edge of a MenubarItem. It is aria-hidden; expose the real shortcut to assistive tech by passing aria-keyshortcuts on the MenubarItem (forwarded onto the element).MDNaria-keyshortcuts
ariaLabelstring
Accessible name for the menubar. APG: a menubar without a visible label must have aria-label (or aria-labelledby).APGMenu and Menubar pattern
classstring
Extra Tailwind classes appended to the root element.