Combobox
Accessible typeable autocomplete with consumer-driven filtering.
Basic Combobox
Type to filter; you handle the filtering and pass matching options.
- Apple
- Apricot
- Banana
- Blueberry
- Cherry
- Coconut
- Cranberry
- Grape
- Kiwi
- Lemon
- Lime
- Mango
- Orange
- Papaya
- Peach
- Pear
- Pineapple
- Plum
- Raspberry
- Strawberry
<BitBhCombobox TValue="string"
Value="@selectedFruit"
OnValueChange="v => selectedFruit = v"
OnQueryChange="HandleFruitQuery"
DisplayValue="v => v ?? string.Empty"
class="relative inline-block min-w-72">
<div class="relative flex items-center rounded-lg border border-zinc-300 bg-white transition-colors focus-within:border-blue-600">
<BitBhComboboxInput TValue="string" Placeholder="Pick a fruit…"
class="flex-1 border-0 bg-transparent px-3.5 py-2 text-sm text-zinc-900 outline-none" />
<BitBhComboboxButton TValue="string"
class="inline-flex items-center justify-center px-2.5 cursor-pointer text-zinc-500 bg-transparent border-0">
<Template Context="b">
<span class="@($"text-zinc-500 transition-transform duration-200 {(b.IsOpen ? "rotate-180" : "")}")">▾</span>
</Template>
</BitBhComboboxButton>
</div>
<BitBhComboboxOptions TValue="string"
class="absolute top-[calc(100%+0.25rem)] left-0 right-0 z-30 m-0 max-h-64 overflow-y-auto list-none rounded-lg border border-zinc-200 bg-white p-1 shadow-lg">
@if (filteredFruits.Count == 0)
{
<li class="px-2.5 py-2.5 text-sm text-zinc-500 italic list-none">No matches.</li>
}
else
{
@foreach (var fruit in filteredFruits)
{
<BitBhComboboxOption TValue="string" Value="@fruit"
class="flex items-center justify-between 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">
<Template Context="o">
<span>@fruit</span>
@if (o.IsSelected) { <span class="text-blue-600 font-bold">✓</span> }
</Template>
</BitBhComboboxOption>
}
}
</BitBhComboboxOptions>
</BitBhCombobox>
@code {
private string? selectedFruit;
private readonly string[] allFruits =
{
"Apple", "Apricot", "Banana", "Blueberry", "Cherry", "Coconut", "Cranberry",
"Grape", "Kiwi", "Lemon", "Lime", "Mango", "Orange", "Papaya",
"Peach", "Pear", "Pineapple", "Plum", "Raspberry", "Strawberry",
};
private List<string> filteredFruits = new();
private void HandleFruitQuery(string q) =>
filteredFruits = string.IsNullOrWhiteSpace(q) ? allFruits.ToList()
: allFruits.Where(f => f.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
protected override void OnInitialized() => filteredFruits = allFruits.ToList();
}Multi-select with chips
Selected values render as chips above the input.
- design
- engineering
- marketing
- sales
- support
- research
- product
- operations
- finance
- legal
<BitBhCombobox TValue="string" Multiple="true"
Values="@pickedTags"
OnValuesChange="v => pickedTags = v"
OnQueryChange="HandleTagQuery"
class="relative inline-block min-w-72">
<div class="relative flex flex-wrap items-center rounded-lg border border-zinc-300 bg-white transition-colors focus-within:border-blue-600 p-1 gap-1">
@foreach (var tag in pickedTags)
{
<span class="inline-flex items-center gap-1 rounded-full bg-blue-50 px-2 py-0.5 text-xs font-semibold text-blue-700">
@tag
<button type="button"
class="cursor-pointer rounded-full px-1 text-base leading-tight hover:bg-blue-100"
@onclick="() => RemoveTag(tag)"
aria-label="@($"Remove {tag}")">×</button>
</span>
}
<BitBhComboboxInput TValue="string"
Placeholder="@(pickedTags.Count == 0 ? "Pick tags…" : "")"
class="flex-1 border-0 bg-transparent text-sm text-zinc-900 outline-none w-auto min-w-24 px-1 py-1" />
<BitBhComboboxButton TValue="string"
class="inline-flex items-center justify-center px-2.5 cursor-pointer text-zinc-500 bg-transparent border-0">
<Template Context="b">
<span class="@($"text-zinc-500 transition-transform duration-200 {(b.IsOpen ? "rotate-180" : "")}")">▾</span>
</Template>
</BitBhComboboxButton>
</div>
<BitBhComboboxOptions TValue="string"
class="absolute top-[calc(100%+0.25rem)] left-0 right-0 z-30 m-0 max-h-64 overflow-y-auto list-none rounded-lg border border-zinc-200 bg-white p-1 shadow-lg">
@if (filteredTags.Count == 0)
{
<li class="px-2.5 py-2.5 text-sm text-zinc-500 italic list-none">No matches.</li>
}
else
{
@foreach (var tag in filteredTags)
{
<BitBhComboboxOption TValue="string" Value="@tag"
class="flex items-center justify-between 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>@tag</span>
</Template>
</BitBhComboboxOption>
}
}
</BitBhComboboxOptions>
</BitBhCombobox>
@code {
private IReadOnlyCollection<string> pickedTags = Array.Empty<string>();
private readonly string[] allTags =
{
"design", "engineering", "marketing", "sales", "support",
"research", "product", "operations", "finance", "legal",
};
private List<string> filteredTags = new();
private void HandleTagQuery(string q) =>
filteredTags = string.IsNullOrWhiteSpace(q) ? allTags.ToList()
: allTags.Where(t => t.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
private void RemoveTag(string tag) =>
pickedTags = pickedTags.Where(t => t != tag).ToList();
protected override void OnInitialized() => filteredTags = allTags.ToList();
}Object values with rich content
Bind to objects; provide DisplayValue.
- AAAlice Anderson [email protected]
- BBBenjamin Brown [email protected]
- CCCaterina Costa [email protected]
- DDDaniel Davis [email protected]
- EEEleanor Evans [email protected]
- FFFelix Foster [email protected]
- GGGenevieve Gomez [email protected]
- HHHenry Hughes [email protected]
<BitBhCombobox TValue="Person"
Value="@selectedPerson"
OnValueChange="v => selectedPerson = v"
OnQueryChange="HandlePersonQuery"
DisplayValue="p => p?.Name ?? string.Empty"
class="relative inline-block min-w-72">
<div class="relative flex items-center rounded-lg border border-zinc-300 bg-white transition-colors focus-within:border-blue-600">
<BitBhComboboxInput TValue="Person" Placeholder="Search people…"
class="flex-1 border-0 bg-transparent px-3.5 py-2 text-sm text-zinc-900 outline-none" />
<BitBhComboboxButton TValue="Person"
class="inline-flex items-center justify-center px-2.5 cursor-pointer text-zinc-500 bg-transparent border-0">
<Template Context="b">
<span class="@($"text-zinc-500 transition-transform duration-200 {(b.IsOpen ? "rotate-180" : "")}")">▾</span>
</Template>
</BitBhComboboxButton>
</div>
<BitBhComboboxOptions TValue="Person"
class="absolute top-[calc(100%+0.25rem)] left-0 right-0 z-30 m-0 max-h-64 overflow-y-auto list-none rounded-lg border border-zinc-200 bg-white p-1 shadow-lg">
@if (filteredPeople.Count == 0)
{
<li class="px-2.5 py-2.5 text-sm text-zinc-500 italic list-none">No matches.</li>
}
else
{
@foreach (var person in filteredPeople)
{
<BitBhComboboxOption TValue="Person" Value="@person"
class="flex items-center justify-between 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">
<div class="flex w-full items-center gap-2.5">
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full bg-indigo-100 text-xs font-bold text-blue-700">@person.Initials</span>
<div class="flex flex-1 flex-col leading-tight">
<strong class="text-sm text-zinc-900">@person.Name</strong>
<span class="text-xs text-zinc-500">@person.Email</span>
</div>
@if (o.IsSelected) { <span class="text-blue-600 font-bold">✓</span> }
</div>
</Template>
</BitBhComboboxOption>
}
}
</BitBhComboboxOptions>
</BitBhCombobox>
@code {
public sealed record Person(string Name, string Email)
{
public string Initials =>
string.Concat(Name.Split(' ', StringSplitOptions.RemoveEmptyEntries).Take(2).Select(p => p[0]));
}
private Person? selectedPerson;
private readonly Person[] allPeople =
{
new("Alice Anderson", "[email protected]"),
new("Benjamin Brown", "[email protected]"),
new("Caterina Costa", "[email protected]"),
new("Daniel Davis", "[email protected]"),
new("Eleanor Evans", "[email protected]"),
new("Felix Foster", "[email protected]"),
new("Genevieve Gomez", "[email protected]"),
new("Henry Hughes", "[email protected]"),
};
private List<Person> filteredPeople = new();
private void HandlePersonQuery(string q) =>
filteredPeople = string.IsNullOrWhiteSpace(q) ? allPeople.ToList()
: allPeople.Where(p =>
p.Name.Contains(q, StringComparison.OrdinalIgnoreCase) ||
p.Email.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
protected override void OnInitialized() => filteredPeople = allPeople.ToList();
}Anchor Positioning
Anchor auto-positions and flips.
Bottom Start
- Apple
- Apricot
- Banana
- Blueberry
- Cherry
- Coconut
- Cranberry
- Grape
- Kiwi
- Lemon
- Lime
- Mango
- Orange
- Papaya
- Peach
- Pear
- Pineapple
- Plum
- Raspberry
- Strawberry
Top Start
- Apple
- Apricot
- Banana
- Blueberry
- Cherry
- Coconut
- Cranberry
- Grape
- Kiwi
- Lemon
- Lime
- Mango
- Orange
- Papaya
- Peach
- Pear
- Pineapple
- Plum
- Raspberry
- Strawberry
<BitBhCombobox TValue="string" Value="@anchorFruit" OnValueChange="v => anchorFruit = v"
OnQueryChange="HandleAnchorFruitQuery" DisplayValue="v => v ?? string.Empty" class="inline-block min-w-72">
<div class="relative flex items-center rounded-lg border border-zinc-300 bg-white transition-colors focus-within:border-blue-600">
<BitBhComboboxInput TValue="string" Placeholder="Pick a fruit…"
class="flex-1 border-0 bg-transparent px-3.5 py-2 text-sm text-zinc-900 outline-none" />
<BitBhComboboxButton TValue="string"
class="inline-flex items-center justify-center px-2.5 cursor-pointer text-zinc-500 bg-transparent border-0">
<Template Context="b">
<span class="@($"text-zinc-500 transition-transform duration-200 {(b.IsOpen ? "rotate-180" : "")}")">▾</span>
</Template>
</BitBhComboboxButton>
</div>
<BitBhComboboxOptions TValue="string" Anchor="@(new BitBhAnchorOptions { To = "bottom start", Gap = 4 })"
class="z-[9999] m-0 min-w-72 max-h-64 overflow-y-auto list-none rounded-lg border border-zinc-200 bg-white p-1 shadow-lg">
@if (anchorFilteredFruits.Count == 0)
{
<li class="px-2.5 py-2.5 text-sm text-zinc-500 italic list-none">No matches.</li>
}
else
{
@foreach (var fruit in anchorFilteredFruits)
{
<BitBhComboboxOption TValue="string" Value="@fruit"
class="flex items-center justify-between 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>@fruit</span>
@if (o.IsSelected) { <span class="text-blue-600 font-bold">✓</span> }
</Template>
</BitBhComboboxOption>
}
}
</BitBhComboboxOptions>
</BitBhCombobox>
@code {
private string? anchorFruit;
private readonly string[] allFruits =
{
"Apple", "Apricot", "Banana", "Blueberry", "Cherry", "Coconut", "Cranberry",
"Grape", "Kiwi", "Lemon", "Lime", "Mango", "Orange", "Papaya",
"Peach", "Pear", "Pineapple", "Plum", "Raspberry", "Strawberry",
};
private List<string> anchorFilteredFruits = new();
private void HandleAnchorFruitQuery(string q) =>
anchorFilteredFruits = string.IsNullOrWhiteSpace(q) ? allFruits.ToList()
: allFruits.Where(f => f.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
protected override void OnInitialized() => anchorFilteredFruits = allFruits.ToList();
}Large lists (virtualized)
Items + ItemTemplate + Virtualize="true" render only the visible options (each h-9 = ItemSize 36px).
OnQueryChange filters 10,000 items with BitBhFilter.Contains (case- and accent-insensitive); BitBhComboboxEmpty covers no matches.
<BitBhCombobox TValue="string"
@bind-Value="virtualPick"
OnQueryChange="HandleVirtualQuery"
DisplayValue="v => v ?? string.Empty"
class="inline-block min-w-72">
<div class="flex items-center rounded-lg border border-zinc-300 bg-white focus-within:border-blue-600">
<BitBhComboboxInput TValue="string" Placeholder="Search 10,000 animals…"
class="min-w-0 flex-1 border-0 bg-transparent px-3.5 py-2 text-sm outline-none" />
<BitBhComboboxButton TValue="string" class="inline-flex items-center px-2.5 text-zinc-500">
<Template Context="b">
<span class="@($"transition-transform duration-200 {(b.IsOpen ? "rotate-180" : "")}")">▾</span>
</Template>
</BitBhComboboxButton>
</div>
@* Bounded scroll container; each option exactly ItemSize (36px = h-9) *@
<BitBhComboboxOptions TValue="string"
Items="@virtualMatches"
Virtualize="true"
ItemSize="36"
Anchor="@(new BitBhAnchorOptions { To = "bottom start", Gap = 4 })"
class="z-9999 m-0 max-h-64 w-72 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 items-center justify-between 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" : "")}")">
@o.Value
@if (o.IsSelected) { <span class="font-bold text-blue-600">✓</span> }
</span>
</ItemTemplate>
<ChildContent>
<BitBhComboboxEmpty TValue="string" class="px-2.5 py-2.5 text-sm italic text-zinc-500">No animals match.</BitBhComboboxEmpty>
</ChildContent>
</BitBhComboboxOptions>
</BitBhCombobox>
@code {
// 10 adjectives × 10 animals × 100 = 10,000 items.
private static readonly string[] VirtualItems = BuildVirtualItems();
private IReadOnlyList<string> virtualMatches = VirtualItems;
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();
}
// Case- and accent-insensitive "contains" match.
private void HandleVirtualQuery(string query) =>
virtualMatches = VirtualItems.Where(item => BitBhFilter.Contains(item, query)).ToArray();
}