Menu
An accessible dropdown menu with keyboard navigation, typeahead, and zero visual opinion.
Basic Menu
Click the button or press Space / Enter / arrow keys to open. Use Up / Down to
navigate, Home / End to jump, Enter / Space to activate, Escape to close.
Type a letter to jump to the first matching item. ARIA wiring
(aria-haspopup="menu", aria-expanded,
aria-activedescendant) is handled automatically.
<BitBhMenu class="menu">
<BitBhMenuButton class="menu-button">
<Template Context="b">
Options
<span class="menu-chevron @(b.IsOpen ? "open" : "")">▾</span>
</Template>
</BitBhMenuButton>
<BitBhMenuItems class="menu-items">
<BitBhMenuItem OnClick='@(() => Log("Edit"))' class="menu-item" Label="Edit">Edit</BitBhMenuItem>
<BitBhMenuItem OnClick='@(() => Log("Duplicate"))' class="menu-item" Label="Duplicate">Duplicate</BitBhMenuItem>
<BitBhMenuItem OnClick='@(() => Log("Archive"))' class="menu-item" Label="Archive">Archive</BitBhMenuItem>
<BitBhMenuItem OnClick='@(() => Log("Delete"))' class="menu-item" Label="Delete">Delete</BitBhMenuItem>
</BitBhMenuItems>
</BitBhMenu>
<span class="demo-output">Last action: <strong>@(lastAction ?? "(none)")</strong></span>
@code {
private string? lastAction;
private void Log(string action) => lastAction = action;
}/* All styles are consumer-authored — the library ships zero CSS */
.menu { position: relative; display: inline-block; }
.menu-button {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.9rem;
border: 1px solid #d4d4d8;
border-radius: 0.5rem;
background: #fff;
font-size: 0.9rem;
font-family: inherit;
cursor: pointer;
transition: border-color 120ms ease;
}
.menu-button:hover { border-color: #a1a1af; }
.menu-button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.menu-button[data-state="open"] { border-color: #2563eb; }
.menu-button[data-disabled] { opacity: 0.5; cursor: not-allowed; }
.menu-chevron { transition: transform 200ms ease; color: #71717a; }
.menu-chevron.open { transform: rotate(180deg); }
/* Options panel — sits above the click-outside overlay (z-index: 30 inline) */
.menu-items {
position: absolute;
top: calc(100% + 0.25rem);
left: 0;
z-index: 31;
min-width: 10rem;
margin: 0;
padding: 0.25rem;
list-style: none;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
.menu-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.65rem;
border-radius: 0.35rem;
font-size: 0.9rem;
cursor: pointer;
color: #18181b;
list-style: none;
text-decoration: none;
}
/* Hooks emitted by BitBhMenuItem */
.menu-item[data-active] { background: #eff6ff; color: #1d4ed8; }
.menu-item[data-disabled] { opacity: 0.4; cursor: not-allowed; }
.demo-output { font-size: 0.875rem; color: #71717a; }Disabled Items
Set Disabled="true" on individual items. Disabled items receive
data-disabled for CSS, are skipped by arrow-key nav, and are
ignored by typeahead.
<BitBhMenu class="menu">
<BitBhMenuButton class="menu-button">
<Template Context="b">
Account
<span class="menu-chevron @(b.IsOpen ? "open" : "")">▾</span>
</Template>
</BitBhMenuButton>
<BitBhMenuItems class="menu-items">
<BitBhMenuItem OnClick='@(() => Log("Profile"))' class="menu-item" Label="Profile">Profile</BitBhMenuItem>
<BitBhMenuItem OnClick='@(() => Log("Settings"))' class="menu-item" Label="Settings">Settings</BitBhMenuItem>
<BitBhMenuItem OnClick='@(() => Log("Billing"))' class="menu-item" Label="Billing"
Disabled="true">Billing (unavailable)</BitBhMenuItem>
<BitBhMenuItem OnClick='@(() => Log("Sign out"))' class="menu-item" Label="Sign out">Sign out</BitBhMenuItem>
</BitBhMenuItems>
</BitBhMenu>/* All styles are consumer-authored — the library ships zero CSS */
.menu { position: relative; display: inline-block; }
.menu-button {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.9rem;
border: 1px solid #d4d4d8;
border-radius: 0.5rem;
background: #fff;
font-size: 0.9rem;
font-family: inherit;
cursor: pointer;
transition: border-color 120ms ease;
}
.menu-button:hover { border-color: #a1a1af; }
.menu-button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.menu-button[data-state="open"] { border-color: #2563eb; }
.menu-button[data-disabled] { opacity: 0.5; cursor: not-allowed; }
.menu-chevron { transition: transform 200ms ease; color: #71717a; }
.menu-chevron.open { transform: rotate(180deg); }
/* Options panel — sits above the click-outside overlay (z-index: 30 inline) */
.menu-items {
position: absolute;
top: calc(100% + 0.25rem);
left: 0;
z-index: 31;
min-width: 10rem;
margin: 0;
padding: 0.25rem;
list-style: none;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
.menu-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.65rem;
border-radius: 0.35rem;
font-size: 0.9rem;
cursor: pointer;
color: #18181b;
list-style: none;
text-decoration: none;
}
/* Hooks emitted by BitBhMenuItem. Arrow-key navigation and typeahead skip items
marked with data-disabled automatically. */
.menu-item[data-active] { background: #eff6ff; color: #1d4ed8; }
.menu-item[data-disabled] { opacity: 0.4; cursor: not-allowed; }
.demo-output { font-size: 0.875rem; color: #71717a; }Rich item content
Items can contain any markup. Use the data-active hook to drive
the highlighted style and provide a Label string so typeahead
still works when the content is rich.
<BitBhMenu class="menu">
<BitBhMenuButton class="menu-button">
<Template Context="b">
File
<span class="menu-chevron @(b.IsOpen ? "open" : "")">▾</span>
</Template>
</BitBhMenuButton>
<BitBhMenuItems class="menu-items menu-items-wide">
<BitBhMenuItem OnClick='@(() => Log("New file"))' class="menu-item" Label="New file">
<Template Context="i">
<span class="menu-item-icon">📄</span>
<span class="menu-item-body">
<strong>New file</strong>
<span>Create a blank document</span>
</span>
<span class="menu-item-shortcut">⌘N</span>
</Template>
</BitBhMenuItem>
<BitBhMenuItem OnClick='@(() => Log("Open"))' class="menu-item" Label="Open">
<Template Context="i">
<span class="menu-item-icon">📂</span>
<span class="menu-item-body">
<strong>Open…</strong>
<span>Browse for a file</span>
</span>
<span class="menu-item-shortcut">⌘O</span>
</Template>
</BitBhMenuItem>
<BitBhMenuItem OnClick='@(() => Log("Save"))' class="menu-item" Label="Save">
<Template Context="i">
<span class="menu-item-icon">💾</span>
<span class="menu-item-body">
<strong>Save</strong>
<span>Save the current document</span>
</span>
<span class="menu-item-shortcut">⌘S</span>
</Template>
</BitBhMenuItem>
<BitBhMenuItem OnClick='@(() => Log("Export"))' class="menu-item" Label="Export"
Disabled="true">
<Template Context="i">
<span class="menu-item-icon">📤</span>
<span class="menu-item-body">
<strong>Export</strong>
<span>Not available in this plan</span>
</span>
</Template>
</BitBhMenuItem>
</BitBhMenuItems>
</BitBhMenu>
@* Provide Label="New file" so typeahead matches the visible text,
not the icon or shortcut. *@/* All styles are consumer-authored — the library ships zero CSS */
.menu { position: relative; display: inline-block; }
.menu-button {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.9rem;
border: 1px solid #d4d4d8;
border-radius: 0.5rem;
background: #fff;
font-size: 0.9rem;
font-family: inherit;
cursor: pointer;
transition: border-color 120ms ease;
}
.menu-button:hover { border-color: #a1a1af; }
.menu-button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.menu-button[data-state="open"] { border-color: #2563eb; }
.menu-button[data-disabled] { opacity: 0.5; cursor: not-allowed; }
.menu-chevron { transition: transform 200ms ease; color: #71717a; }
.menu-chevron.open { transform: rotate(180deg); }
/* Options panel — sits above the click-outside overlay (z-index: 30 inline) */
.menu-items {
position: absolute;
top: calc(100% + 0.25rem);
left: 0;
z-index: 31;
min-width: 10rem;
margin: 0;
padding: 0.25rem;
list-style: none;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
.menu-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.65rem;
border-radius: 0.35rem;
font-size: 0.9rem;
cursor: pointer;
color: #18181b;
list-style: none;
text-decoration: none;
}
/* Hooks emitted by BitBhMenuItem */
.menu-item[data-active] { background: #eff6ff; color: #1d4ed8; }
.menu-item[data-disabled] { opacity: 0.4; cursor: not-allowed; }
.demo-output { font-size: 0.875rem; color: #71717a; }
/* Rich item layout: icon + body (title + description) + shortcut */
.menu-items-wide { min-width: 16rem; }
.menu-item-icon { font-size: 1rem; flex-shrink: 0; }
.menu-item-shortcut { margin-left: auto; font-size: 0.75rem; color: #71717a; }
.menu-item-body {
display: flex;
flex-direction: column;
flex: 1 1 auto;
line-height: 1.25;
}
.menu-item-body strong { font-size: 0.875rem; }
.menu-item-body span { font-size: 0.75rem; color: #71717a; }
/* Active state overrides the muted description colour */
.menu-item[data-active] .menu-item-body span { color: #93c5fd; }Sections and dividers
Group related items with a plain <li> divider or a
section heading. These are regular HTML elements โ the menu component
doesn't need to know about them.
<BitBhMenu class="menu">
<BitBhMenuButton class="menu-button">
<Template Context="b">
More
<span class="menu-chevron @(b.IsOpen ? "open" : "")">▾</span>
</Template>
</BitBhMenuButton>
<BitBhMenuItems class="menu-items">
<li class="menu-section-label" role="presentation">View</li>
<BitBhMenuItem OnClick='@(() => Log("Zoom in"))' class="menu-item" Label="Zoom in">Zoom in</BitBhMenuItem>
<BitBhMenuItem OnClick='@(() => Log("Zoom out"))' class="menu-item" Label="Zoom out">Zoom out</BitBhMenuItem>
<BitBhMenuItem OnClick='@(() => Log("Full screen"))' class="menu-item" Label="Full screen">Full screen</BitBhMenuItem>
<li class="menu-divider" role="separator"></li>
<li class="menu-section-label" role="presentation">Help</li>
<BitBhMenuItem OnClick='@(() => Log("Documentation"))' class="menu-item" Label="Documentation">Documentation</BitBhMenuItem>
<BitBhMenuItem OnClick='@(() => Log("Keyboard shortcuts"))' class="menu-item" Label="Keyboard shortcuts">Keyboard shortcuts</BitBhMenuItem>
</BitBhMenuItems>
</BitBhMenu>/* All styles are consumer-authored — the library ships zero CSS */
.menu { position: relative; display: inline-block; }
.menu-button {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.9rem;
border: 1px solid #d4d4d8;
border-radius: 0.5rem;
background: #fff;
font-size: 0.9rem;
font-family: inherit;
cursor: pointer;
transition: border-color 120ms ease;
}
.menu-button:hover { border-color: #a1a1af; }
.menu-button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.menu-button[data-state="open"] { border-color: #2563eb; }
.menu-button[data-disabled] { opacity: 0.5; cursor: not-allowed; }
.menu-chevron { transition: transform 200ms ease; color: #71717a; }
.menu-chevron.open { transform: rotate(180deg); }
/* Options panel — sits above the click-outside overlay (z-index: 30 inline) */
.menu-items {
position: absolute;
top: calc(100% + 0.25rem);
left: 0;
z-index: 31;
min-width: 10rem;
margin: 0;
padding: 0.25rem;
list-style: none;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
.menu-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.65rem;
border-radius: 0.35rem;
font-size: 0.9rem;
cursor: pointer;
color: #18181b;
list-style: none;
text-decoration: none;
}
/* Hooks emitted by BitBhMenuItem */
.menu-item[data-active] { background: #eff6ff; color: #1d4ed8; }
.menu-item[data-disabled] { opacity: 0.4; cursor: not-allowed; }
.demo-output { font-size: 0.875rem; color: #71717a; }
/* Section label — not an interactive item, just a visual grouping header */
.menu-section-label {
padding: 0.35rem 0.65rem 0.15rem;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #71717a;
list-style: none;
}
/* Divider between sections */
.menu-divider {
height: 1px;
background: #e4e4e7;
margin: 0.25rem 0;
list-style: none;
}Anchor Positioning
Set Anchor on BitBhMenuItems to automatically position the
dropdown relative to the button. The panel uses position: fixed and
auto-updates on scroll/resize. It flips to the opposite side if there's not enough
space, and shifts to stay within the viewport. CSS custom properties
--button-width, --anchor-gap are exposed on the panel.
@* Anchor positions the dropdown relative to the button automatically.
It flips to the opposite side if there's not enough space. *@
<div style="display:flex; gap:1.5rem; flex-wrap:wrap; align-items:flex-start;">
<BitBhMenu class="menu">
<BitBhMenuButton class="menu-button">
<Template Context="b">
Bottom Start
<span class="menu-chevron @(b.IsOpen ? "open" : "")">▾</span>
</Template>
</BitBhMenuButton>
<BitBhMenuItems Anchor="@(new BitBhAnchorOptions { To = "bottom start", Gap = 4 })"
class="menu-items-anchored">
<BitBhMenuItem OnClick="@(() => Log("Edit"))" class="menu-item" Label="Edit">Edit</BitBhMenuItem>
<BitBhMenuItem OnClick="@(() => Log("Duplicate"))" class="menu-item" Label="Duplicate">Duplicate</BitBhMenuItem>
<BitBhMenuItem OnClick="@(() => Log("Archive"))" class="menu-item" Label="Archive">Archive</BitBhMenuItem>
</BitBhMenuItems>
</BitBhMenu>
<BitBhMenu class="menu">
<BitBhMenuButton class="menu-button">
<Template Context="b">
Bottom End
<span class="menu-chevron @(b.IsOpen ? "open" : "")">▾</span>
</Template>
</BitBhMenuButton>
<BitBhMenuItems Anchor="@(new BitBhAnchorOptions { To = "bottom end", Gap = 4 })"
class="menu-items-anchored">
<BitBhMenuItem OnClick="@(() => Log("Cut"))" class="menu-item" Label="Cut">Cut</BitBhMenuItem>
<BitBhMenuItem OnClick="@(() => Log("Copy"))" class="menu-item" Label="Copy">Copy</BitBhMenuItem>
<BitBhMenuItem OnClick="@(() => Log("Paste"))" class="menu-item" Label="Paste">Paste</BitBhMenuItem>
</BitBhMenuItems>
</BitBhMenu>
@* Use --button-width to match the dropdown width to the trigger *@
<BitBhMenu class="menu">
<BitBhMenuButton class="menu-button">
<Template Context="b">
Match Width
<span class="menu-chevron @(b.IsOpen ? "open" : "")">▾</span>
</Template>
</BitBhMenuButton>
<BitBhMenuItems Anchor="@(new BitBhAnchorOptions { To = "bottom", Gap = 4 })"
class="menu-items-anchored menu-items-match-width">
<BitBhMenuItem OnClick="@(() => Log("Option A"))" class="menu-item" Label="Option A">Option A</BitBhMenuItem>
<BitBhMenuItem OnClick="@(() => Log("Option B"))" class="menu-item" Label="Option B">Option B</BitBhMenuItem>
<BitBhMenuItem OnClick="@(() => Log("Option C"))" class="menu-item" Label="Option C">Option C</BitBhMenuItem>
</BitBhMenuItems>
</BitBhMenu>
</div>/* All styles are consumer-authored — the library ships zero CSS */
.menu { position: relative; display: inline-block; }
.menu-button {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.9rem;
border: 1px solid #d4d4d8;
border-radius: 0.5rem;
background: #fff;
font-size: 0.9rem;
font-family: inherit;
cursor: pointer;
transition: border-color 120ms ease;
}
.menu-button:hover { border-color: #a1a1af; }
.menu-button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.menu-button[data-state="open"] { border-color: #2563eb; }
.menu-button[data-disabled] { opacity: 0.5; cursor: not-allowed; }
.menu-chevron { transition: transform 200ms ease; color: #71717a; }
.menu-chevron.open { transform: rotate(180deg); }
/* When using Anchor, the panel is position:fixed by the JS engine.
No need for position:relative on the parent or position:absolute on the panel.
Use a high z-index to ensure the panel renders above all other page content. */
.menu-items-anchored {
z-index: 9999;
min-width: 10rem;
margin: 0;
padding: 0.25rem;
list-style: none;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
/* Use the --button-width CSS variable to match the trigger width */
.menu-items-match-width {
min-width: unset;
width: var(--button-width);
}
/* Items inside the anchored panel */
.menu-items-anchored .menu-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.65rem;
border-radius: 0.35rem;
font-size: 0.9rem;
cursor: pointer;
color: #18181b;
list-style: none;
}
.menu-items-anchored .menu-item[data-active] { background: #eff6ff; color: #1d4ed8; }
.menu-items-anchored .menu-item[data-disabled] { opacity: 0.4; cursor: not-allowed; }
.demo-output { font-size: 0.875rem; color: #71717a; }Polymorphic items
Set As="a" on an item to render it as an anchor tag โ useful for
navigation links inside a menu. The item still participates in keyboard nav
and receives all the ARIA attributes.
<BitBhMenu class="menu">
<BitBhMenuButton class="menu-button">
<Template Context="b">
Navigate
<span class="menu-chevron @(b.IsOpen ? "open" : "")">▾</span>
</Template>
</BitBhMenuButton>
<BitBhMenuItems class="menu-items">
<BitBhMenuItem As="a" href="/button" class="menu-item" Label="Button">Button</BitBhMenuItem>
<BitBhMenuItem As="a" href="/switch" class="menu-item" Label="Switch">Switch</BitBhMenuItem>
<BitBhMenuItem As="a" href="/disclosure" class="menu-item" Label="Disclosure">Disclosure</BitBhMenuItem>
<BitBhMenuItem As="a" href="/accordion" class="menu-item" Label="Accordion">Accordion</BitBhMenuItem>
</BitBhMenuItems>
</BitBhMenu>/* All styles are consumer-authored — the library ships zero CSS */
.menu { position: relative; display: inline-block; }
.menu-button {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.9rem;
border: 1px solid #d4d4d8;
border-radius: 0.5rem;
background: #fff;
font-size: 0.9rem;
font-family: inherit;
cursor: pointer;
transition: border-color 120ms ease;
}
.menu-button:hover { border-color: #a1a1af; }
.menu-button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.menu-button[data-state="open"] { border-color: #2563eb; }
.menu-button[data-disabled] { opacity: 0.5; cursor: not-allowed; }
.menu-chevron { transition: transform 200ms ease; color: #71717a; }
.menu-chevron.open { transform: rotate(180deg); }
/* Options panel — sits above the click-outside overlay (z-index: 30 inline) */
.menu-items {
position: absolute;
top: calc(100% + 0.25rem);
left: 0;
z-index: 31;
min-width: 10rem;
margin: 0;
padding: 0.25rem;
list-style: none;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
/* When As="a" the item renders as an anchor. The same .menu-item styles apply
because the data-active / data-disabled hooks are emitted regardless of tag.
text-decoration:none keeps the anchor looking like a regular menu item. */
.menu-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.65rem;
border-radius: 0.35rem;
font-size: 0.9rem;
cursor: pointer;
color: #18181b;
list-style: none;
text-decoration: none;
}
/* Hooks emitted by BitBhMenuItem */
.menu-item[data-active] { background: #eff6ff; color: #1d4ed8; }
.menu-item[data-disabled] { opacity: 0.4; cursor: not-allowed; }