const [fields, setFields] = useState(['name', 'status']);
<TransferListSelector
label="Visible fields"
description="Choose which fields appear. Changes take effect immediately."
triggerLabel={fields.length + ' visible fields'}
options={[
{value: 'name', label: 'Name'},
{value: 'status', label: 'Status'},
{value: 'owner', label: 'Owner'},
]}
value={fields}
onChange={setFields}
selectedLabel="Visible fields"
availableLabel="Available fields"
hasSelectAll
hasClear
/>Transfer List Selector
A selector field for moving options between selected and available lists and optionally ordering the selected values. Changes commit immediately by default; staged commit behavior adds an internal draft and Apply/Cancel footer. TransferList remains the lower-level content primitive for custom ComplexSelector surfaces.
Authored source examples
Source examples recorded in the documentation for this entry.
const [fields, setFields] = useState(['name', 'status']);
<TransferListSelector
label="Visible fields"
description="Review the complete selection before applying it."
options={fieldOptions}
value={fields}
onChange={setFields}
commitBehavior="staged"
selectedLabel="Visible fields"
availableLabel="Available fields"
hasSelectAll
hasClear
/><TransferListSelector
label="Visible fields"
options={[
{
value: 'name',
label: 'Name',
isTransferDisabled: true,
disabledMessage: 'Name must remain visible.',
},
{
value: 'status',
label: 'Status',
isReorderDisabled: true,
disabledMessage: 'Status is fixed in position but can be removed.',
},
{value: 'owner', label: 'Owner'},
]}
value={fields}
onChange={setFields}
/>const savedViews = {
standard: ['name', 'status', 'owner'],
ownership: ['name', 'owner', 'team'],
};
const [applied, setApplied] = useState(savedViews.standard);
const [draft, setDraft] = useState(applied);
const activeSavedView =
Object.entries(savedViews).find(
([, columns]) =>
columns.length === draft.length &&
columns.every((column, index) => column === draft[index]),
)?.[0] ?? 'custom';
function ResetDraftOnOpen({isOpen, value, onReset}) {
const wasOpenRef = useRef(false);
useEffect(() => {
if (isOpen && !wasOpenRef.current) {
onReset([...value]);
}
wasOpenRef.current = isOpen;
}, [isOpen, onReset, value]);
return null;
}
<ComplexSelector
label="View options"
isLabelHidden
triggerLabel="View options"
value={applied}
onChange={setApplied}>
{(_appliedValue, commit, close, state) => (
<>
<ResetDraftOnOpen
isOpen={state.isOpen}
value={applied}
onReset={setDraft}
/>
<Selector
label="Saved view"
options={savedViewOptions}
value={activeSavedView}
onChange={nextSavedView => {
setDraft(savedViews[nextSavedView]);
}}
/>
<TransferList
label="Visible fields"
isLabelHidden
options={fieldOptions}
value={draft}
onChange={setDraft}
selectedLabel="Visible fields"
availableLabel="Available fields"
hasSelectAll
hasClear
/>
<Button label="Cancel" variant="ghost" onClick={close} />
<Button
label="Apply"
variant="primary"
onClick={() => {
commit(draft);
close();
}}
/>
</>
)}
</ComplexSelector>Usage
Use TransferListSelector by default for medium-to-large, inspectable sets where membership and selected order need explicit control. Immediate behavior commits each edit and renders no footer. Choose staged behavior when users must review a set of changes before Apply. Use TransferList directly only in a deliberately custom ComplexSelector surface, such as table column settings with saved views and a custom footer.
- Start with immediate behavior. Add, remove, bulk, and reorder changes call onChange as they occur, and closing or dismissing the popover keeps those committed changes.
- Set commitBehavior="staged" when the complete selection should be reviewed or persisted as one transaction. Apply commits a changed draft; Cancel, Escape, and light dismiss discard it.
- Use TransferList directly inside ComplexSelector only when the surface needs custom structure such as saved views, presets, headers, or footer actions. Keep applied and draft state separate and reset the draft whenever the surface opens.
- Use isTransferDisabled and isReorderDisabled independently. Provide disabledMessage whenever either action is unavailable so the constraint is discoverable.
- Keep rows concise and single-line. Use option description as searchable metadata, or renderOption when richer visible content is necessary.
- Commit add, remove, bulk, and keyboard reorder changes immediately. During pointer reordering, keep rows stationary, lock the translucent preview to its source-column position, use vertical pointer movement to determine insertion, and commit once on release. Keep the preview in the nearest popover or dialog top layer.
- Use commitBehavior as a presentation or container choice. Both values render the same selector popover; use explicit composition for a different shell.
- Use a transfer list for a handful of simple choices; CheckboxInput or MultiSelector is more compact.
Typed props
TransferListSelector
| Prop | Type and behavior |
|---|---|
label | string · requiredAccessible field label shown outside the popover unless isLabelHidden is true. |
options | readonly TransferListOption<T>[] · requiredComplete option pool. Each option has value and label, with optional searchable description metadata, group, independent isTransferDisabled and isReorderDisabled constraints, and disabledMessage. |
value | readonly T[] · requiredCommitted selected values in display order. In staged behavior this is the applied value copied into a local draft when the selector opens. |
onChange | (nextValue: readonly T[]) => void · requiredCalled after every permitted list edit in immediate behavior, or once when Apply commits a changed draft in staged behavior. |
commitBehavior | TransferListSelectorCommitBehavior · default 'immediate'Controls when changes commit. Immediate updates on each list edit without a footer; staged waits for Apply and supports Cancel or dismiss. |
triggerLabel | ReactNode · default `${value.length} selected`Content shown in the closed selector trigger. |
description | stringSupporting field guidance shown with the external label. |
isLabelHidden | boolean · default falseVisually hides the field label while preserving its accessible name. |
selectedLabel | string · default 'Selected'Heading and accessible name for the selected list. |
availableLabel | string · default 'Available'Heading and accessible name for the available list. |
hasSearch | boolean · default falseShows a shared search field that filters both lists by label and description. |
searchLabel | string · default 'Search ' followed by labelAccessible label for the shared search field. |
searchPlaceholder | string · default 'Search...'Placeholder shown in the shared search field. |
isReorderable | boolean · default trueShows a left-side grip for every selected option. Per-option isReorderDisabled keeps its grip visible but disabled and makes it a fixed-order barrier. |
hasSelectAll | boolean · default falseShows Add all and adds every transfer-enabled available option to the current selection. |
hasClear | boolean · default falseShows Clear and removes every transfer-enabled selected option from the current selection. |
renderOption | (option: TransferListOption<T>) => ReactNodeCustomizes row content while retaining built-in labels, actions, constraints, and reorder interaction. |
selectedEmptyText | string · default 'No selected options'Message shown when the selected list is empty. |
availableEmptyText | string · default 'No available options'Message shown when every option has been selected. |
noResultsText | string · default 'No results'Message shown when search has no matches in a list. |
applyLabel | string · default 'Apply'Label for the staged Apply action. Only valid with commitBehavior="staged". |
cancelLabel | string · default 'Cancel'Label for the staged Cancel action. Only valid with commitBehavior="staged". |
size | 'sm' | 'md' | 'lg' · default 'md'Trigger and field size. |
width | SizeValue · default 'min(41rem, calc(100vw - 32px))'Width of the field and its popup; both track this one value. |
placement | 'above' | 'below' | 'start' | 'end' · default 'below'Popover placement relative to the trigger. |
isOptional | booleanMarks the field optional. |
isRequired | booleanMarks the field required. |
isDisabled | booleanDisables the selector trigger and transfer interaction. |
isLoading | booleanShows loading state on the trigger and disables transfer interaction while busy. |
status | {type: 'warning' | 'error' | 'success', message?: string}Validation status for the field. |
statusVariant | FieldStatusVariantPlacement treatment for the status message. |
labelTooltip | stringTooltip text displayed next to the field label. |
changeAction | (value: readonly T[]) => void | Promise<void>Optional async action run after each immediate commit or a changed staged Apply; drives optimistic and busy state. |
xstyle | StyleXStylesStyleX styles for the external selector field. |
className | stringClass name applied to the external selector field. |
style | CSSPropertiesInline styles applied to the external selector field. |
data-testid | stringTest ID for the selector trigger container. |
TransferList
| Prop | Type and behavior |
|---|---|
label | string · requiredAccessible name for the complete transfer control. |
isLabelHidden | boolean · default falseVisually hides the control label while retaining its accessible name. |
description | stringSupporting guidance shown below the control label. |
options | readonly TransferListOption<T>[] · requiredComplete option pool with optional metadata, grouping, transfer constraints, and reorder constraints. |
value | readonly T[] · requiredSelected option values in display order. |
onChange | (nextValue: readonly T[]) => void · requiredCalled immediately after a permitted option is added, removed, bulk changed, or reordered. |
selectedLabel | string · default 'Selected'Heading and accessible name for the selected list. |
availableLabel | string · default 'Available'Heading and accessible name for the available list. |
hasSearch | boolean · default falseShows a shared search field that filters both lists by label and description. |
searchLabel | stringAccessible label for the shared search field. |
searchPlaceholder | string · default 'Search...'Placeholder shown in the shared search field. |
isReorderable | boolean · default trueEnables pointer and keyboard ordering. Per-option isReorderDisabled keeps a visible disabled grip and fixed-order barrier. |
hasSelectAll | boolean · default falseShows Add all for transfer-enabled available options. |
hasClear | boolean · default falseShows Clear for transfer-enabled selected options; transfer-disabled values remain selected. |
renderOption | (option: TransferListOption<T>) => ReactNodeCustomizes row content without replacing built-in actions and constraints. |
selectedEmptyText | string · default 'No selected options'Message shown when the selected list is empty. |
availableEmptyText | string · default 'No available options'Message shown when every option has been selected. |
noResultsText | string · default 'No results'Message shown when search has no matches in a list. |
xstyle | StyleXStylesStyleX styles for the TransferList root. |
Anatomy
Names and explains the selector outside the popover so placement does not change with supporting text.
Summarizes the committed value and opens the transfer-list popover.
Filters the selected and available lists without changing their values and uses a uniform 12px inset.
Places two unframed semantic lists beside one divider. Narrow containers stack the sections, use the body background behind each column header, expand both sections to their rows, and delegate overflow to one scrollable containing surface.
Uses a direction-neutral X to remove and plus to add. Selected rows retain a start-aligned grip when reordering is enabled; constraints disable each control independently.
Optional Clear and Add all text actions that honor transfer-disabled options.
Rendered only for staged behavior. Apply commits a changed draft; Cancel closes without changing the committed value.
Reports add, remove, bulk, and reorder results without relying on visual position alone.
Theming
Targets
astryx-transfer-listastryx-transfer-list-collectionastryx-transfer-list-panelVisual props: side
astryx-transfer-list-itemVisual props: side, state