BlazorDualListbox 1.0.1
dotnet add package BlazorDualListbox --version 1.0.1
NuGet\Install-Package BlazorDualListbox -Version 1.0.1
<PackageReference Include="BlazorDualListbox" Version="1.0.1" />
<PackageVersion Include="BlazorDualListbox" Version="1.0.1" />
<PackageReference Include="BlazorDualListbox" />
paket add BlazorDualListbox --version 1.0.1
#r "nuget: BlazorDualListbox, 1.0.1"
#:package BlazorDualListbox@1.0.1
#addin nuget:?package=BlazorDualListbox&version=1.0.1
#tool nuget:?package=BlazorDualListbox&version=1.0.1
BlazorDualListbox
A generic, accessible dual-listbox (pick-list) component for Blazor. Two side-by-side list boxes with controls to move items between them — via buttons, double-click, or the keyboard.
- Render-mode agnostic — works in Blazor Server, WebAssembly, and .NET 8+ unified hosting.
- Multi-targets
net6.0,net8.0, andnet10.0. - No CSS framework dependency — clean default styling via CSS isolation, themeable with CSS custom properties. Bring Bootstrap/Tailwind/your own classes if you want them.
- Accessible —
role="listbox"/role="option",aria-multiselectable,aria-selected, managed focus, and full keyboard operability. - Two-way bindable — the parent stays the source of truth; the component only mutates the bound collections on an actual move.
Installation
dotnet add package BlazorDualListbox
Add the namespace to _Imports.razor (or the individual page):
@using BlazorDualListbox
Quick start
@using BlazorDualListbox
<DualListbox TItem="Person"
@bind-Source="available"
@bind-Selected="chosen"
TextSelector="p => p.Name"
ValueSelector="p => p.Id"
Filterable="true"
SourceHeader="Available"
SelectedHeader="Selected"
OnMoved="HandleMoved" />
@code {
private IEnumerable<Person> available = new List<Person>
{
new(1, "Alice"), new(2, "Bob"), new(3, "Carol"),
};
private IEnumerable<Person> chosen = new List<Person>();
private void HandleMoved(DualListboxMoveEventArgs<Person> e)
=> Console.WriteLine($"{e.Items.Count} item(s) moved {e.Direction}");
public record Person(int Id, string Name);
}
Source and Selected are both @bind-able. Because the component raises the change
callbacks on every move, your bound fields always reflect the current state.
Moving items
| Action | How |
|---|---|
| Move selected item(s) → | › button, Enter, or double-click an item |
| Move all → | » button (moves only filtered/visible items) |
| Move selected item(s) ← | ‹ button, Enter, or double-click an item |
| Move all ← | « button |
Selection: click to select, Ctrl/Cmd+click to toggle, Shift+click to select a range,
Ctrl/Cmd+A to select all visible.
Keyboard: focus a list, then ↑/↓/Home/End to navigate, Space to toggle
selection, Enter to move highlighted items.
Parameters
Data
| Parameter | Type | Description |
|---|---|---|
Source |
IEnumerable<TItem>? |
Items in the left (available) list. @bind-able. |
Selected |
IEnumerable<TItem>? |
Items in the right (selected) list. @bind-able. |
Presentation
| Parameter | Type | Default | Description |
|---|---|---|---|
TextSelector |
Func<TItem,string>? |
ToString() |
Projects an item to its display text. |
ValueSelector |
Func<TItem,object>? |
the item itself | Stable identity for equality/selection. Supply this when items lack value equality but have a key (e.g. an Id). |
ItemTemplate |
RenderFragment<TItem>? |
— | Custom rendering per item. Overrides TextSelector output. |
SourceHeader |
string? |
— | Heading above the source list. |
SelectedHeader |
string? |
— | Heading above the selected list. |
EmptyText |
string |
"No items" |
Shown in a list that has no items. |
AddSingleButtonText |
string |
› |
Text for the move-selected-right button. |
AddAllButtonText |
string |
» |
Text for the move-all-right button. |
RemoveSingleButtonText |
string |
‹ |
Text for the move-selected-left button. |
RemoveAllButtonText |
string |
« |
Text for the move-all-left button. |
Button styling
| Parameter | Type | Description |
|---|---|---|
ButtonClass |
string? |
CSS class(es) applied to all four move buttons. |
AddSingleButtonClass |
string? |
Extra class(es) for the move-selected-right button only. |
AddAllButtonClass |
string? |
Extra class(es) for the move-all-right button only. |
RemoveAllButtonClass |
string? |
Extra class(es) for the move-all-left button only. |
RemoveSingleButtonClass |
string? |
Extra class(es) for the move-selected-left button only. |
Classes compose as dl-btn + ButtonClass + the per-button class — the built-in
dl-btn is always kept, so you're adding to the default styling rather than replacing it.
<DualListbox TItem="Person"
@bind-Source="available"
@bind-Selected="chosen"
ButtonClass="btn"
AddSingleButtonClass="btn-primary"
RemoveSingleButtonClass="btn-danger" />
The › button above renders as class="dl-btn btn btn-primary".
Behaviour
| Parameter | Type | Default | Description |
|---|---|---|---|
Filterable |
bool |
false |
Shows a search box above each list. |
FilterPlaceholder |
string |
"Filter…" |
Placeholder for the filter boxes. |
AllowMoveAll |
bool |
true |
Shows the "move all" (»/«) buttons. |
Disabled |
bool |
false |
Makes the whole component non-interactive. |
Any unmatched attributes (e.g. class, id, data-*) are splatted onto the root element.
Events
| Parameter | Type | Description |
|---|---|---|
SourceChanged |
EventCallback<IEnumerable<TItem>> |
Enables @bind-Source. |
SelectedChanged |
EventCallback<IEnumerable<TItem>> |
Enables @bind-Selected. |
OnChanged |
EventCallback |
Raised after any move, once the bound collections are updated. |
OnMoved |
EventCallback<DualListboxMoveEventArgs<TItem>> |
Raised after a move with the moved items and Direction (ToSelected / ToSource). |
Theming
The component ships with CSS isolation and exposes CSS custom properties. Override them from
your own stylesheet on .dl-root (or any ancestor):
.dl-root {
--dl-gap: 0.5rem;
--dl-border-color: #ccc;
--dl-radius: 6px;
--dl-bg: #fff;
--dl-fg: #1a1a1a;
--dl-muted-fg: #666;
--dl-hover-bg: rgba(0, 0, 0, 0.05);
--dl-highlight-bg: #2563eb;
--dl-highlight-fg: #fff;
--dl-accent: #2563eb;
--dl-list-height: 16rem;
--dl-min-width: 12rem;
}
For button styling specifically, prefer the *ButtonClass parameters described above.
Accessibility
Each list is a role="listbox" with aria-multiselectable="true"; items are role="option"
with aria-selected. Focus is tracked with aria-activedescendant, and the component is fully
operable by keyboard. This is a deliberate differentiator versus many existing components.
Requirements
- .NET 6, .NET 8, or .NET 10 (the package multi-targets
net6.0;net8.0;net10.0). - Works with any Blazor render mode (Server, WebAssembly, or .NET 8+ Auto/unified hosting).
Contributing
Issues and pull requests are welcome. The repository contains the component (RCL), a demo app, and a bUnit test suite:
git clone <repository-url>
cd blazor-dual-listbox/src
dotnet build
dotnet test
Run the demo app to explore the component interactively:
dotnet run --project DemoApp/DemoApp.csproj
Then browse to /dual-listbox.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Components.Web (>= 10.0.0)
-
net6.0
- Microsoft.AspNetCore.Components.Web (>= 6.0.36)
-
net8.0
- Microsoft.AspNetCore.Components.Web (>= 8.0.11)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.