Skip to content

Dual listbox

Picks several options by moving them from one list to another, in order. For a short list of choices, use Checkbox or Combobox.

Fields to display
Press Control and the right or left arrow to move the selected options between the lists, or Command and the arrows on a Mac. Space selects or clears an option, and Shift with the arrows selects a range.
Available Fields
Annual Revenue
Billing City
Billing Country
Created Date
Employees
Industry
Last Activity
Rating
Website
Visible Fields
Account Name
Billing State/Province
Phone
Type
Account Owner Alias

Installation

pnpm dlx shadcn@latest add @wisty/dual-listbox

The CLI installs the Radix or Base UI file to match your project, with what it builds on: button, field, spinner, tooltip, icon.

If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.

When to use

Use it

  • People pick several options from a long list and the order of what they pick matters, like the columns of a list view ("Select Fields to Display") or the fields on a layout.
  • People pick several options and should see what's left beside what's chosen, like the languages an agent supports or the permission sets a user has.
  • Some options must always stay chosen. Pass them as requiredOptions. They lead the selected list with a lock.
  • The choice has limits, like "two to four". min, max and required refuse a move that breaks them and say why.
  • It replaces lightning-dual-listbox.

Don't use it

  • There are only a few options, or the order doesn't matter and space is tight. Use Checkbox, or Combobox with several values.
  • People pick records, like accounts or contacts. Use Lookup.
  • People pick one option. Use Select or Radio group.
  • People sort a list without choosing from another. A dual listbox always shows both lists.

Usage

import { FieldLegend, FieldSet } from "@/components/ui/field"
import { DualListbox } from "@/components/wisty/dual-listbox"

Name the group from a FieldLegend, as a visual picker is named, and hold the value in state when the page reads it:

<DualListbox
  options={fields}
  sourceLabel="Available Fields"
  selectedLabel="Visible Fields"
  value={visibleFields}
  onValueChange={setVisibleFields}
  aria-labelledby="fields-legend"
/>

value is the selected list in its order, with requiredOptions first. onValueChange gets that list after every move and reorder. The options people highlight are the dual listbox's own state, not the value. With name, the selected values submit with a form, one entry per value, and an invalid selection stops the submit and shows its error. The error under the lists is the dual listbox's own; point the group's aria-describedby at a FieldDescription for help text.

Examples

Required, with limits

Two to four languages with `required`, `min` and `max`: a move that breaks a limit is refused and its message shows.

*Support languages

Choose two to four languages the agent answers cases in.

Press Control and the right or left arrow to move the selected options between the lists, or Command and the arrows on a Mac. Space selects or clears an option, and Shift with the arrows selects a range.
Available Languages
Chinese (Simplified)
Dutch
German
Italian
Japanese
Korean
Portuguese (Brazil)
Spanish
Selected Languages
English
French

Locked options

Opportunity Name and Stage stay at the top of the selected list, gray and locked by `requiredOptions`, so they can't be removed.

Fields to display
Press Control and the right or left arrow to move the selected options between the lists, or Command and the arrows on a Mac. Space selects or clears an option, and Shift with the arrows selects a range.
Available Fields
Account Name
Forecast Category
Lead Source
Next Step
Opportunity Owner Alias
Probability (%)
Type
Visible Fields
Opportunity Name: item cannot be removed from Visible Fields
Stage: item cannot be removed from Visible Fields
Amount
Close Date

Disabled

A `disabled` dual listbox shows its selection in gray boxes, with no Tab stops and every button disabled.

Fields to display
Press Control and the right or left arrow to move the selected options between the lists, or Command and the arrows on a Mac. Space selects or clears an option, and Shift with the arrows selects a range.
Available Fields
Mailing City
Mobile
Contact Owner Alias
Visible Fields
Name
Account Name
Title
Phone
Email

Loading

With `loading`, a spinner covers the source list while its options load, and the move buttons wait.

Permission sets
Press Control and the right or left arrow to move the selected options between the lists, or Command and the arrows on a Mac. Space selects or clears an option, and Shift with the arrows selects a range.
Available Permission Sets
Enabled Permission Sets
Sales User
Report Builder

Without reordering

A set whose order doesn't matter: `disableReordering` leaves out the up and down buttons, and `size={5}` shows five rows.

Territory industries
Press Control and the right or left arrow to move the selected options between the lists, or Command and the arrows on a Mac. Space selects or clears an option, and Shift with the arrows selects a range.
Available
Agriculture
Construction
Education
Energy
Healthcare
Manufacturing
Retail
Technology
In Territory
Banking
Insurance

API reference

DualListbox

PropTypeDefaultDescription
options*readonly DualListboxOption[]Every option, in the order the source list shows them.
sourceLabel*stringThe source list's label, like "Available".
selectedLabel*stringThe selected list's label, like "Selected".
valuereadonly string[]The selected options' values, in the selected list's order. Controlled.
defaultValuereadonly string[]
onValueChange(value: string[]) => voidCalled with the selected values after a move or a reorder.
requiredOptionsreadonly string[][]Values that are always selected and can't be removed. They lead the selected list, with a lock.
minnumber0The fewest options the selected list may hold. A move that leaves fewer is refused.
maxnumberThe most options the selected list may hold. A move that adds more is refused.
requiredbooleanfalseAt least one option must be selected.
disabledbooleanfalseDisables both lists and every button.
disableReorderingbooleanfalseHides the up and down buttons.
sizenumberHow many options each list shows before it scrolls.
loadingbooleanfalseCovers the source list with a spinner and disables the move buttons.
namestringSubmits the selected values with a form, one entry per value; an invalid selection blocks the submit.
formstringThe id of a form the values belong to, outside it.
addButtonLabelstring`Move selection to ${selectedLabel}`The add button's name and tooltip. Defaults to "Move selection to" and the selected list's label.
removeButtonLabelstring`Move selection to ${sourceLabel}`The remove button's name and tooltip. Defaults to "Move selection to" and the source list's label.
upButtonLabelstring"Move selection up"
downButtonLabelstring"Move selection down"
messageWhenValueMissingstring"An option must be selected."The error when required and nothing is selected.
messageWhenRangeOverflowstringThe error when more than max options would be selected.
messageWhenRangeUnderflowstringThe error when fewer than min options would be selected.

Also takes every <div> (opens in a new tab) prop.

DualListboxOptiontype

MemberTypeDefaultDescription
value*stringWhat the option submits, and what value holds.
label*stringWhat the option shows.

SLDS mapping

DualListbox is slds-dueling-list inside a role="group" named by the field's legend. Each list is a slds-dueling-list__column_responsive column: a slds-form-element__label over slds-dueling-list__options, holding a slds-listbox_vertical listbox of slds-listbox__option_plain options. The buttons are slds-button_icon-container icon buttons in two slds-dueling-list__column columns. A locked option is aria-disabled with utility:lock after its label. The error is slds-form-element__help with slds-has-error.

Known gaps:

  • No drag and drop between or within the lists, as in lightning-dual-listbox. The buttons and the keyboard move and reorder.
  • The chevrons are lucide's, where SLDS draws utility:right, left, up and down. The lock is Salesforce's utility:lock, so it needs the icons the Icon component loads.
  • The keyboard instructions, the announcements of each move, the locked option's text and the spinner's name are English.
  • No label-inline layout, no options with descriptions or icons, and no filter inside the lists.
PropValueSLDSNotes
(default)default (added)slds-dueling-list with slds-dueling-list__column_responsive columnsBoth lists share the width, 6rem at the narrowest, and are 15rem tall.
size(a number) (added)style="height: (size × 2.25 + 1)rem" on slds-dueling-list__optionsHow many options show before the lists scroll, as lightning-dual-listbox sets it.
requiredOptions(values) (added)slds-listbox__option[aria-disabled="true"] with utility:lock in slds-media__figure_reverseLocked options lead the selected list in gray, with a lock after the label.
disableReorderingtrue (added)no fourth slds-dueling-list__columnThe up and down buttons are left out.
disabledtrue (added)slds-dueling-list__options slds-is-disabledGray boxes and text, no highlight, and every button disabled.
loadingtrue (added)slds-is-relative column with slds-spinner_container and slds-spinner_brand slds-spinner_smallA spinner covers the source column; the move buttons are disabled.

Keyboard interactions

Key Action
Up, Down Moves focus to the previous or next option and highlights it alone.
Home, End Moves focus to the first or last option and highlights it alone.
Shift + Up, Down Moves focus and highlights the range from where the highlight started.
Ctrl + Shift + Home, End Highlights the range from where the highlight started to the first or last option.
Ctrl + Up, Down Moves focus without changing the highlight.
Space, Enter, Ctrl + Space Adds the focused option to the highlight, or takes it out.
Ctrl + A Highlights every option in the list, or clears them all when they already are.
Ctrl + Right Moves the highlighted options to the selected list and focuses the first of them.
Ctrl + Left Moves the highlighted options back to the source list and focuses the first of them.
Letters and digits Moves focus to the next option whose label starts with what was typed.
Tab Moves between the two lists and the buttons. Each list is one Tab stop.

On a Mac, Command works where the table says Ctrl. With the mouse, a click highlights one option, Ctrl or Command with a click adds or removes one, and Shift with a click highlights a range.

Accessibility

  • Name the group: point aria-labelledby at a FieldLegend, or pass aria-label. sourceLabel and selectedLabel name the two lists.
  • Each list describes its keyboard shortcuts to screen readers, and every move and reorder is announced.
  • A locked option still takes focus, so screen reader users hear that it can't be removed.
  • After a button moves options, focus stays on the button. When that move disables the button, focus goes to the first moved option.

Responsive behavior

  • The two lists share the width equally: 15rem each where there's room, 6rem at the narrowest, plus 5rem for the buttons. Narrower than 17rem, the dual listbox overflows its container.
  • size sets how many options show before a list scrolls. Long labels truncate, with the full label in a tooltip.

Rules

  • Keep option values unique and stable. The value, the highlight and the focus follow them.
  • Keep requiredOptions inside options. A required value with no option is ignored.
  • Give the dual listbox a name for its group. Without it, the lists' names don't say what they choose.
  • Never pass asChild or render.