Transfer
A dual-list picker: move items between a source and a target list with checkboxes, per-panel search, and select-all. The classic pattern for assigning permissions, columns, tags, or members.
Check rows in either panel, then use the arrows to move them. The select-all checkbox in a panel header toggles everything currently visible in that panel.
Import
import { Transfer } from "@mikenotthepope/substrateui"With search
showSearch adds a filter to each panel. Select-all then applies to the filtered rows only, so you can bulk-move a subset.
Controlled
Pass targetKeys with onChange to own the state — the standard shape for saving to a form or server.
granted: [audit]
Labels
Both move buttons are icon-only, and the per-panel count is assembled from numbers. Override with the labels prop. This is one of the few label sets with no LabelsProvider key, so it is per instance only; wrap it in a component of your own if you use it in more than one place.
| Prop | Type | Default | Description |
|---|---|---|---|
searchPlaceholder | string | "Search..." | Placeholder for each panel's search input. |
empty | string | "No items" | Shown when a panel has nothing in it. |
moveToTarget | string | "Move to target" | Accessible name for the button that moves the selection across. |
moveToSource | string | "Move to source" | Accessible name for the button that moves it back. |
itemCount | (selected, total) => string | "n/m" | The per-panel count. A function so the separator and order are the caller's. |
Accessibility
Each row is a <label> wrapping its checkbox, so the whole row is a click target and screen readers announce the item text with its checked state. Panel headers use a select-all checkbox labelled by the panel title, and report a mixed state when only some rows are checked. The move buttons carry explicit accessible names and disable themselves when nothing movable is selected. Items marked disabled are skipped by select-all and can never be moved.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
dataSource | TransferItem[] | — | Every item, across both lists. Each has key, label, and optional disabled. |
targetKeys | string[] | — | Keys in the target (right) list — controlled. |
defaultTargetKeys | string[] | [] | Keys in the target list initially — uncontrolled. |
onChange | (targetKeys, direction, movedKeys) => void | — | Fired after a move with the next target keys and what moved. |
selectedKeys | string[] | — | Checked keys across both panels — controlled. |
onSelectChange | (selectedKeys: string[]) => void | — | Fired when the checked set changes. |
titles | [ReactNode, ReactNode] | ["Source", "Target"] | Panel headings. |
showSearch | boolean | false | Show a search input in each panel. |
listHeight | number | 240 | Height of each scrollable list, in px. |
disabled | boolean | false | Disable the whole control. |
labels | TransferLabels | — | Text overrides: searchPlaceholder, empty, moveToTarget, moveToSource, itemCount. |
TransferItem
| Prop | Type | Default | Description |
|---|---|---|---|
key | string | — | Unique key across the whole data source. |
label | React.ReactNode | — | Rendered row content. |
disabled | boolean | — | Dim the row and block selection — it can never be moved. |