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.json2. Use it
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
/** @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
{% 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
{# 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
{{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
{{/* 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
<.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
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
<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
<!--
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 [&:not(:popover-open)]:hidden [&: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 [&_svg]:size-4 [&_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 [&_svg]:size-4 [&_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 [&_svg]:size-4 [&_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 [&_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="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 [&:not(:popover-open)]:hidden [&: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 [&_svg]:size-4 [&_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 [&_svg]:size-4 [&_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 [&_svg]:size-4 [&_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 [&_svg]:size-4 [&_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 [&_svg]:size-4 [&_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 [&:not(:popover-open)]:hidden [&: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 [&_svg]:size-4 [&_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 [&_svg]:size-4 [&_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 [&_svg]:size-4 [&_svg]:shrink-0">Reset Zoom</button>
</div>
</div>
</div>
</div>Further reading
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 [&:not(:popover-open)]:hidden [&: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 [&_svg]:size-4 [&_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 [&_svg]:size-4 [&_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 [&_svg]:size-4 [&_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 [&:not(:popover-open)]:hidden [&: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 [&_svg]:size-4 [&_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 [&_svg]:size-4 [&_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 [&_svg]:size-4 [&_svg]:shrink-0">Tuition & Aid</a>
</div>
</div>
</div>
</div>Further reading
API Reference
<Menubar>
| Prop | Type | Default | Description |
|---|---|---|---|
ariaLabelledby | 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 (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 |
ariaLabel | string | — | Accessible name for the menubar. APG: a menubar without a visible label must have aria-label (or aria-labelledby).APGMenu and Menubar pattern |
class | string | — | Extra Tailwind classes appended to the root element. |