Listbox
Accessible custom select with single/multi-select, full keyboard navigation, and typeahead.
Basic Listbox
Click or use Space / Enter / arrow keys / typeahead.
- Alice
- Benjamin
- Caterina
- Daniel
- Eleanor
- Felix
- Genevieve
- Henry
<BitBhListbox TValue="string" Value="@favPerson" OnValueChange="v => favPerson = v" class="relative inline-block min-w-56">
<BitBhListboxButton aria-label="Person" TValue="string"
class="flex w-full items-center justify-between rounded-lg border border-zinc-300 bg-white px-3.5 py-2 text-sm cursor-pointer transition-colors hover:border-zinc-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 data-state-open:border-blue-600 data-disabled:opacity-50 data-disabled:cursor-not-allowed">
<Template Context="b">
<span>@(string.IsNullOrEmpty(b.Value) ? "Select a person…" : b.Value)</span>
<span class="@($"text-zinc-500 transition-transform duration-200 {(b.IsOpen ? "rotate-180" : "")}")">▾</span>
</Template>
</BitBhListboxButton>
<BitBhListboxOptions TValue="string"
class="absolute top-[calc(100%+0.25rem)] left-0 right-0 z-30 m-0 max-h-56 overflow-y-auto list-none rounded-lg border border-zinc-200 bg-white p-1 shadow-lg">
<BitBhListboxOption TValue="string" Value="@("Alice")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold data-disabled:opacity-40 data-disabled:cursor-not-allowed">Alice</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Benjamin")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold data-disabled:opacity-40 data-disabled:cursor-not-allowed">Benjamin</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Caterina")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold data-disabled:opacity-40 data-disabled:cursor-not-allowed">Caterina</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Daniel")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold data-disabled:opacity-40 data-disabled:cursor-not-allowed">Daniel</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Eleanor")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold data-disabled:opacity-40 data-disabled:cursor-not-allowed">Eleanor</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Felix")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold data-disabled:opacity-40 data-disabled:cursor-not-allowed">Felix</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Genevieve")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold data-disabled:opacity-40 data-disabled:cursor-not-allowed">Genevieve</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Henry")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold data-disabled:opacity-40 data-disabled:cursor-not-allowed">Henry</BitBhListboxOption>
</BitBhListboxOptions>
</BitBhListbox>
@code {
private string? favPerson;
}Multi-select
Set Multiple="true". Use data-selected: for the check.
- Red
- Green
- Blue
- Yellow
- Purple
- Orange
<BitBhListbox TValue="string" Multiple="true" Values="@favColors" OnValuesChange="v => favColors = v" class="relative inline-block min-w-56">
<BitBhListboxButton aria-label="Colours" TValue="string"
class="flex w-full items-center justify-between rounded-lg border border-zinc-300 bg-white px-3.5 py-2 text-sm cursor-pointer transition-colors hover:border-zinc-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 data-state-open:border-blue-600 data-disabled:opacity-50 data-disabled:cursor-not-allowed">
<Template Context="b">
<span>@(b.Values.Count == 0 ? "Pick colours…" : string.Join(", ", b.Values))</span>
<span class="@($"text-zinc-500 transition-transform duration-200 {(b.IsOpen ? "rotate-180" : "")}")">▾</span>
</Template>
</BitBhListboxButton>
<BitBhListboxOptions TValue="string"
class="absolute top-[calc(100%+0.25rem)] left-0 right-0 z-30 m-0 max-h-56 overflow-y-auto list-none rounded-lg border border-zinc-200 bg-white p-1 shadow-lg">
<BitBhListboxOption TValue="string" Value="@("Red")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">
<Template Context="o">
<span class="inline-block w-4 text-blue-600 font-bold">@(o.IsSelected ? "✓" : "")</span>
<span>Red</span>
</Template>
</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Green")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">
<Template Context="o">
<span class="inline-block w-4 text-blue-600 font-bold">@(o.IsSelected ? "✓" : "")</span>
<span>Green</span>
</Template>
</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Blue")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">
<Template Context="o">
<span class="inline-block w-4 text-blue-600 font-bold">@(o.IsSelected ? "✓" : "")</span>
<span>Blue</span>
</Template>
</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Yellow")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">
<Template Context="o">
<span class="inline-block w-4 text-blue-600 font-bold">@(o.IsSelected ? "✓" : "")</span>
<span>Yellow</span>
</Template>
</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Purple")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">
<Template Context="o">
<span class="inline-block w-4 text-blue-600 font-bold">@(o.IsSelected ? "✓" : "")</span>
<span>Purple</span>
</Template>
</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Orange")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">
<Template Context="o">
<span class="inline-block w-4 text-blue-600 font-bold">@(o.IsSelected ? "✓" : "")</span>
<span>Orange</span>
</Template>
</BitBhListboxOption>
</BitBhListboxOptions>
</BitBhListbox>
@code {
private IReadOnlyCollection<string> favColors = Array.Empty<string>();
}Disabled Options
data-disabled: dims and skips disabled options.
- Standard
- Pro
- Enterprise (coming soon)
<BitBhListbox TValue="string" DefaultValue='@("standard")' class="relative inline-block min-w-56">
<BitBhListboxButton aria-label="Plan" TValue="string"
class="flex w-full items-center justify-between rounded-lg border border-zinc-300 bg-white px-3.5 py-2 text-sm cursor-pointer transition-colors hover:border-zinc-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 data-state-open:border-blue-600 data-disabled:opacity-50 data-disabled:cursor-not-allowed">
<Template Context="b">
<span>@(b.Value ?? "Pick a plan")</span>
<span class="@($"text-zinc-500 transition-transform duration-200 {(b.IsOpen ? "rotate-180" : "")}")">▾</span>
</Template>
</BitBhListboxButton>
<BitBhListboxOptions TValue="string"
class="absolute top-[calc(100%+0.25rem)] left-0 right-0 z-30 m-0 max-h-56 overflow-y-auto list-none rounded-lg border border-zinc-200 bg-white p-1 shadow-lg">
<BitBhListboxOption TValue="string" Value="@("standard")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold data-disabled:opacity-40 data-disabled:cursor-not-allowed">Standard</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("pro")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold data-disabled:opacity-40 data-disabled:cursor-not-allowed">Pro</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("enterprise")" Disabled="true"
class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold data-disabled:opacity-40 data-disabled:cursor-not-allowed">Enterprise (coming soon)</BitBhListboxOption>
</BitBhListboxOptions>
</BitBhListbox>Custom Option Content
Rich content per option; provide Label for typeahead.
- 🇺🇸 United States us
- 🇬🇧 United Kingdom gb
- 🇫🇷 France fr
- 🇩🇪 Germany de
- 🇯🇵 Japan jp
- 🇧🇷 Brazil br
<BitBhListbox TValue="string" Value="@country" OnValueChange="v => country = v" class="relative inline-block min-w-56">
<BitBhListboxButton aria-label="Country" TValue="string"
class="flex w-full items-center justify-between rounded-lg border border-zinc-300 bg-white px-3.5 py-2 text-sm cursor-pointer transition-colors hover:border-zinc-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 data-state-open:border-blue-600">
<Template Context="b">
<span>@(LookupCountry(b.Value)?.Name ?? "Pick a country")</span>
<span class="@($"text-zinc-500 transition-transform duration-200 {(b.IsOpen ? "rotate-180" : "")}")">▾</span>
</Template>
</BitBhListboxButton>
<BitBhListboxOptions TValue="string"
class="absolute top-[calc(100%+0.25rem)] left-0 right-0 z-30 m-0 max-h-56 overflow-y-auto list-none rounded-lg border border-zinc-200 bg-white p-1 shadow-lg">
<BitBhListboxOption TValue="string" Value="@("us")" Label="United States" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">
<span class="text-base">🇺🇸</span>
<span class="flex-1">United States</span>
<span class="text-xs text-zinc-500 uppercase tracking-wide">us</span>
</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("gb")" Label="United Kingdom" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">
<span class="text-base">🇬🇧</span>
<span class="flex-1">United Kingdom</span>
<span class="text-xs text-zinc-500 uppercase tracking-wide">gb</span>
</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("fr")" Label="France" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">
<span class="text-base">🇫🇷</span>
<span class="flex-1">France</span>
<span class="text-xs text-zinc-500 uppercase tracking-wide">fr</span>
</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("de")" Label="Germany" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">
<span class="text-base">🇩🇪</span>
<span class="flex-1">Germany</span>
<span class="text-xs text-zinc-500 uppercase tracking-wide">de</span>
</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("jp")" Label="Japan" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">
<span class="text-base">🇯🇵</span>
<span class="flex-1">Japan</span>
<span class="text-xs text-zinc-500 uppercase tracking-wide">jp</span>
</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("br")" Label="Brazil" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">
<span class="text-base">🇧🇷</span>
<span class="flex-1">Brazil</span>
<span class="text-xs text-zinc-500 uppercase tracking-wide">br</span>
</BitBhListboxOption>
</BitBhListboxOptions>
</BitBhListbox>
@code {
private record Country(string Code, string Name, string Flag);
private readonly Country[] countries =
{
new("us", "United States", "🇺🇸"),
new("gb", "United Kingdom", "🇬🇧"),
new("fr", "France", "🇫🇷"),
new("de", "Germany", "🇩🇪"),
new("jp", "Japan", "🇯🇵"),
new("br", "Brazil", "🇧🇷"),
};
private string? country = "fr";
private Country? LookupCountry(string? code) =>
code is null ? null : countries.FirstOrDefault(c => c.Code == code);
}Anchor Positioning
Anchor + --button-width for matched-width dropdowns.
Bottom Start (width matched)
- Alice
- Benjamin
- Caterina
- Daniel
- Eleanor
- Felix
- Genevieve
- Henry
Top Start (flips down if no space)
- Alice
- Benjamin
- Caterina
- Daniel
- Eleanor
- Felix
- Genevieve
- Henry
<BitBhListbox TValue="string" Value="@anchorPerson" OnValueChange="v => anchorPerson = v" class="inline-block min-w-56">
<BitBhListboxButton aria-label="Person" TValue="string"
class="flex w-full items-center justify-between rounded-lg border border-zinc-300 bg-white px-3.5 py-2 text-sm cursor-pointer transition-colors hover:border-zinc-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 data-state-open:border-blue-600">
<Template Context="b">
<span>@(string.IsNullOrEmpty(b.Value) ? "Select a person…" : b.Value)</span>
<span class="@($"text-zinc-500 transition-transform duration-200 {(b.IsOpen ? "rotate-180" : "")}")">▾</span>
</Template>
</BitBhListboxButton>
<BitBhListboxOptions TValue="string" Anchor="@(new BitBhAnchorOptions { To = "bottom start", Gap = 4 })"
class="z-[9999] m-0 max-h-56 overflow-y-auto list-none rounded-lg border border-zinc-200 bg-white p-1 shadow-lg w-[var(--button-width)]">
<BitBhListboxOption TValue="string" Value="@("Alice")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">Alice</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Benjamin")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">Benjamin</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Caterina")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">Caterina</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Daniel")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">Daniel</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Eleanor")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">Eleanor</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Felix")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">Felix</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Genevieve")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">Genevieve</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("Henry")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">Henry</BitBhListboxOption>
</BitBhListboxOptions>
</BitBhListbox>Form Submission
Set Name to render hidden inputs.
<form @onsubmit="HandleFormSubmit" @onsubmit:preventDefault="true" class="flex flex-col items-start gap-3">
<BitBhListbox TValue="string" Name="role" DefaultValue='@("user")' class="relative inline-block min-w-56">
<BitBhListboxButton aria-label="Role" TValue="string"
class="flex w-full items-center justify-between rounded-lg border border-zinc-300 bg-white px-3.5 py-2 text-sm cursor-pointer transition-colors hover:border-zinc-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 data-state-open:border-blue-600">
<Template Context="b">
<span class="capitalize">@(b.Value)</span>
<span class="@($"text-zinc-500 transition-transform duration-200 {(b.IsOpen ? "rotate-180" : "")}")">▾</span>
</Template>
</BitBhListboxButton>
<BitBhListboxOptions TValue="string"
class="absolute top-[calc(100%+0.25rem)] left-0 right-0 z-30 m-0 max-h-56 overflow-y-auto list-none rounded-lg border border-zinc-200 bg-white p-1 shadow-lg">
<BitBhListboxOption TValue="string" Value="@("user")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">User</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("admin")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">Admin</BitBhListboxOption>
<BitBhListboxOption TValue="string" Value="@("owner")" class="flex items-center gap-2 rounded-md px-2.5 py-2 text-sm cursor-pointer text-zinc-900 list-none data-active:bg-blue-50 data-selected:font-semibold">Owner</BitBhListboxOption>
</BitBhListboxOptions>
</BitBhListbox>
<button type="submit" class="inline-flex items-center gap-2 rounded-lg border border-blue-600 bg-blue-600 px-4 py-2 text-sm font-medium text-white cursor-pointer hover:bg-blue-700">Submit</button>
@if (lastSubmitted is not null)
{
<span class="text-sm text-zinc-500">@lastSubmitted</span>
}
</form>
@code {
private string? lastSubmitted;
private void HandleFormSubmit() =>
lastSubmitted = "(submitted — single-select posts role=<value>; multi posts a separate field per value)";
}Large lists (virtualized)
Items + ItemTemplate + Virtualize="true" render only the visible options of 10,000 —
give the panel a fixed height (h-64 overflow-y-auto) and each option exactly ItemSize px (h-9 = 36).
A content-less BitBhListboxButton shows the selection or its Placeholder (data-placeholder:).
<BitBhListbox TValue="string" @bind-Value="virtualPick" class="relative inline-block min-w-64">
@* No content: shows the selected text, or the placeholder (data-placeholder) *@
<BitBhListboxButton aria-label="Item" TValue="string" Placeholder="Choose one of 10,000…"
class="flex w-full items-center rounded-lg border border-zinc-300 bg-white py-2 pl-3.5 pr-9 text-left text-sm text-zinc-900 cursor-pointer hover:border-zinc-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 data-state-open:border-blue-600 data-placeholder:text-zinc-500" />
<span aria-hidden="true" class="pointer-events-none absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-500">▾</span>
@* Fixed-height scroll container; each option exactly ItemSize (36px = h-9) *@
<BitBhListboxOptions TValue="string"
Items="@VirtualItems"
Virtualize="true"
ItemSize="36"
Anchor="@(new BitBhAnchorOptions { To = "bottom start", Gap = 4 })"
class="z-9999 m-0 h-64 w-(--button-width) list-none overflow-y-auto rounded-lg border border-zinc-200 bg-white p-1 shadow-lg">
<ItemTemplate Context="o">
<span class="@($"flex h-9 cursor-pointer items-center gap-2 whitespace-nowrap rounded-md px-2.5 text-sm {(o.IsActive ? "bg-blue-50 text-blue-900" : "text-zinc-900")} {(o.IsSelected ? "font-semibold" : "")}")">
<span class="w-4 font-bold text-blue-600">@(o.IsSelected ? "✓" : "")</span>
@o.Value
</span>
</ItemTemplate>
</BitBhListboxOptions>
</BitBhListbox>
@code {
// 10 adjectives × 10 animals × 100 = 10,000 options.
private static readonly string[] VirtualItems = BuildVirtualItems();
private string? virtualPick;
private static string[] BuildVirtualItems()
{
string[] adjectives = ["Amber", "Brave", "Calm", "Dusty", "Eager", "Fuzzy", "Gentle", "Hazy", "Icy", "Jolly"];
string[] animals = ["Badger", "Crane", "Dingo", "Falcon", "Gecko", "Heron", "Ibis", "Jaguar", "Koala", "Lynx"];
return Enumerable.Range(0, 10_000)
.Select(i => $"{adjectives[i / 1000]} {animals[i / 100 % 10]} {i % 100 + 1}")
.ToArray();
}
}