Transfer / Dual List

Pro

Dual-list transfer widget for moving items between a source and target panel, with per-panel search, check-all and move-all controls.

</>sv-transfer transferdual-listselection

Also belongs under Forms & Inputs — it is catalogued in Data Display and stays there.

Complexity tier: Application The interactive bar, plus a stated data-volume ceiling, a persistence contract, a bundle budget and an accessible parity surface.

import { SvTransferComponent } from '@styloviz-pro/display';

Demo & usage

Basic usage

Pro

Usage example — open the gallery for the full interactive demo.

Pro

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Without search

Pro

Persisting a move, and putting it back

Pro

A pane that loads from the server

Pro

When to use

  • Moving items between two explicit lists: assign members, pick columns, build playlists.
  • You want per-panel search and move-all / check-all controls.
  • For ordering within one list, use the Drag-and-Drop List.

Async states

  • loading
  • error
  • retry

Item identity & owned state

Restorable state
  • sourceChecked
  • targetChecked

Bind these to put a saved view back — the open sections, the hidden columns, the selected rows — and read their Change events to store what the user does.

Server writes

Writes
  • transferChange

Each payload carries a requestKey — send it with the request so a retry cannot apply the same change twice.

Rollback
Call event.rollback() when the save fails — the change is already on screen, and this is what puts it back.
While saving
Bind saving — with two writes outstanding there is no single state to roll back to, so the action stays locked until the first one lands.
On failure
Pass the failure to moveError to show it in place — without taking away what the user was working on.

Inputs

NameTypeDefaultDescription
sourceItemsTransferItem[][]Items in the source (left) panel.
targetItemsTransferItem[][]Items in the target (right) panel.
sourceTitlestring'Source'Source panel heading.
targetTitlestring'Target'Target panel heading.
searchablebooleantrueShow per-panel search fields.
disabledbooleanfalseDisabled state — no interactions possible.
serverSidebooleanfalseLet the server do the searching. The supplied items are shown exactly as given — client-side search can only ever look at the page already loaded.
debounceMsnumber300Delay before a search event fires, in ms. `0` emits immediately.
loadingSourcebooleanfalseTrue while the source pane is being fetched.
loadingTargetbooleanfalseTrue while the target pane is being fetched.
sourceErrorSvDataError | string | nullnullFailure loading the source pane, rendered in its place.
targetErrorSvDataError | string | nullnullFailure loading the target pane, rendered in its place.
savingbooleanfalseTrue while a move is being written. Every move button and every tick stops responding, so a second move cannot race the first and leave `rollback()` holding an arrangement that predates it.
moveErrorSvDataError | string | nullnullThe last MOVE failed, shown above both panes. Spelled `moveError` rather than `error` because `sourceError` and `targetError` are already here and mean something else: *that pane would not load*. Those replace a pane and offer a retry, because a failed fetch leaves nothing to press. A failed move leaves both panes full and the buttons where they were, so it is reported and not retryable — you retry by pressing the arrow again. Overloading one input with both meanings is what the split exists to stop.
sourceCheckedSet<string> (two-way)new Set()Which source-pane items are ticked (two-way). A `model` because it is the state the two transfer buttons act on, so a host driving the component — preselecting a saved set, or enabling its own "move" control — has to be able to both write it and watch it. Held in a plain signal it was readable only from inside the template.
targetCheckedSet<string> (two-way)new Set()Which target-pane items are ticked (two-way). Same reasoning as sourceChecked.

Outputs

EventPayload typeDescription
transferChangeTransferChangeEventEmitted whenever items are transferred.
sourceSearchChangestringThe source pane's search term changed — fetch matching items. Debounced.
targetSearchChangestringThe target pane's search term changed — fetch matching items. Debounced.
retrySourcevoidThe host should re-run the failed source load.
retryTargetvoidThe host should re-run the failed target load.
sourceCheckedChangeSet<string>Emits the new `sourceChecked` whenever it changes — the output half of `[(sourceChecked)]`.
targetCheckedChangeSet<string>Emits the new `targetChecked` whenever it changes — the output half of `[(targetChecked)]`.

Customization & theming

  • Label each side with `sourceTitle` / `targetTitle`.
  • Enable `searchable` for per-panel filtering.
  • Read moved items from the `transferChange` event; replacing the `sourceItems`/`targetItems` set re-initialises the panels while in-progress moves survive unrelated re-renders.
  • For a pane too large to hold in memory, set `serverSide` and drive it from `sourceSearchChange` (debounced by `debounceMs`) plus `loadingSource` / `sourceError`.
  • `sourceChecked` / `targetChecked` are two-way, so a host can preselect a saved set or drive its own move control.
  • `moveError` accepts a bare string or a structured `SvDataError`, and renders above both panes rather than replacing either.

Best practices

  • Do: Label both panels clearly so source vs target is unambiguous.
  • Do: Enable `searchable` once either panel can hold many items.
  • Do: Bind `saving` while a move is being written. Every arrow and tick locks, so a second move cannot overtake the first and leave `rollback()` holding an arrangement that predates it. Search deliberately stays live.
  • Do: Call `event.rollback()` when the write fails. The panes rearrange the instant an arrow is pressed, so the component holds the only copy of the previous arrangement — including the ticks the move cleared.
  • Do: Send `event.requestKey` and ignore a key you have already applied, so a retried request cannot grant the same item twice.
  • Do: Move your own state only on success. A rejected move is rolled back inside the component, so writing `event.source` / `event.target` into your signals regardless would make the failure stick.
  • Don't: Do not use `moveError` for a failed fetch — `sourceError` / `targetError` replace their pane and offer a retry, because a failed load leaves nothing to press. A failed move leaves both panes full.
  • Don't: Do not expect the panes to wait for the server before moving. A dual list that pauses on a round trip reads as a broken control.
  • Tip: Read `event.moved` and `event.direction` instead of diffing the two panes — a host persisting one grant at a time needs exactly those.
  • Tip: Pass stable array references; swapping in a new item set (e.g. after an async load) re-syncs the panels.
  • Do: Read the check-all contract before wiring your own: it covers the **filtered** set, never the whole loaded list. The pane discloses any ticks the search is hiding, and a move takes those too.
  • Do: Handle `transferChange.moved` rather than diffing the panes — it is already the skipped-items-excluded list, and a disabled item is never in it.

Accessibility

  • Keyboard at scale (X-10): each pane is ONE tab stop over its item checkboxes: ↑/↓/Home/End, PageUp/PageDown (ten items, clamped) and typeahead over labels move within the pane, while ←/→ keep their meaning — move the checked items across. Documented ceiling: 2,000 items in a pane, past which a dev-mode warning fires.
  • Each panel is a labelled group of individually-named checkboxes; the header check-all reflects a tri-state (indeterminate) when partially selected.
  • The check-all box **names its own scope** — "Select all 2 matching in Available" while a search is running, "Select all 5,000 in Available" when it is not — so the contract is legible before the click rather than inferable after it.
  • Move buttons are labelled with the count and the keyboard shortcut ("Move 2 selected to Selected. Right arrow."); the arrow glyphs are hidden from assistive tech.
  • Right and Left arrow move the ticked items from inside the list, and Ctrl/⌘ with them moves everything shown — so a keyboard user never has to tab out to the control column between the panes and back for each item. Space and Enter are left to the checkbox under the cursor.
  • A polite live region announces each transfer, and the same sentence is **rendered** under the panes — a move that skipped something has to say so to everyone, and the most likely reading of an item that stayed put is that the click missed. "Move undone." on a rollback, so the jump is not silent.
  • While `saving`, the move buttons carry `aria-busy="true"` — in flight, not merely switched off.
  • A failed move is announced through `role="alert"` above both panes.
  • A selection the current search is hiding is disclosed in the pane header — "2 more selected, hidden by this search — a move takes 4 in total" — because ticks survive a search, and without it the header reads "all selected" over the visible rows while a move takes every hidden one too.

Feedback on Transfer / Dual List

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.