EXEPERTAI LAB

Research alpha

Time Machine
EXEPERTAI LAB
GALLERY / COLLECTION
← Browse Astryx gallery
Selector·component·@astryxdesign/labExperimental

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.

@astryxdesign/lab

Authored source examples

Source examples recorded in the documentation for this entry.

Default immediate selector
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
/>
Staged commit
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
/>
Independent transfer and reorder constraints
<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}
/>
Saved views in a custom ComplexSelector
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

PropType and behavior
labelstring · required
Accessible field label shown outside the popover unless isLabelHidden is true.
optionsreadonly TransferListOption<T>[] · required
Complete option pool. Each option has value and label, with optional searchable description metadata, group, independent isTransferDisabled and isReorderDisabled constraints, and disabledMessage.
valuereadonly T[] · required
Committed 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 · required
Called after every permitted list edit in immediate behavior, or once when Apply commits a changed draft in staged behavior.
commitBehaviorTransferListSelectorCommitBehavior · default 'immediate'
Controls when changes commit. Immediate updates on each list edit without a footer; staged waits for Apply and supports Cancel or dismiss.
triggerLabelReactNode · default `${value.length} selected`
Content shown in the closed selector trigger.
descriptionstring
Supporting field guidance shown with the external label.
isLabelHiddenboolean · default false
Visually hides the field label while preserving its accessible name.
selectedLabelstring · default 'Selected'
Heading and accessible name for the selected list.
availableLabelstring · default 'Available'
Heading and accessible name for the available list.
hasSearchboolean · default false
Shows a shared search field that filters both lists by label and description.
searchLabelstring · default 'Search ' followed by label
Accessible label for the shared search field.
searchPlaceholderstring · default 'Search...'
Placeholder shown in the shared search field.
isReorderableboolean · default true
Shows a left-side grip for every selected option. Per-option isReorderDisabled keeps its grip visible but disabled and makes it a fixed-order barrier.
hasSelectAllboolean · default false
Shows Add all and adds every transfer-enabled available option to the current selection.
hasClearboolean · default false
Shows Clear and removes every transfer-enabled selected option from the current selection.
renderOption(option: TransferListOption<T>) => ReactNode
Customizes row content while retaining built-in labels, actions, constraints, and reorder interaction.
selectedEmptyTextstring · default 'No selected options'
Message shown when the selected list is empty.
availableEmptyTextstring · default 'No available options'
Message shown when every option has been selected.
noResultsTextstring · default 'No results'
Message shown when search has no matches in a list.
applyLabelstring · default 'Apply'
Label for the staged Apply action. Only valid with commitBehavior="staged".
cancelLabelstring · default 'Cancel'
Label for the staged Cancel action. Only valid with commitBehavior="staged".
size'sm' | 'md' | 'lg' · default 'md'
Trigger and field size.
widthSizeValue · 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.
isOptionalboolean
Marks the field optional.
isRequiredboolean
Marks the field required.
isDisabledboolean
Disables the selector trigger and transfer interaction.
isLoadingboolean
Shows loading state on the trigger and disables transfer interaction while busy.
status{type: 'warning' | 'error' | 'success', message?: string}
Validation status for the field.
statusVariantFieldStatusVariant
Placement treatment for the status message.
labelTooltipstring
Tooltip 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.
xstyleStyleXStyles
StyleX styles for the external selector field.
classNamestring
Class name applied to the external selector field.
styleCSSProperties
Inline styles applied to the external selector field.
data-testidstring
Test ID for the selector trigger container.

TransferList

PropType and behavior
labelstring · required
Accessible name for the complete transfer control.
isLabelHiddenboolean · default false
Visually hides the control label while retaining its accessible name.
descriptionstring
Supporting guidance shown below the control label.
optionsreadonly TransferListOption<T>[] · required
Complete option pool with optional metadata, grouping, transfer constraints, and reorder constraints.
valuereadonly T[] · required
Selected option values in display order.
onChange(nextValue: readonly T[]) => void · required
Called immediately after a permitted option is added, removed, bulk changed, or reordered.
selectedLabelstring · default 'Selected'
Heading and accessible name for the selected list.
availableLabelstring · default 'Available'
Heading and accessible name for the available list.
hasSearchboolean · default false
Shows a shared search field that filters both lists by label and description.
searchLabelstring
Accessible label for the shared search field.
searchPlaceholderstring · default 'Search...'
Placeholder shown in the shared search field.
isReorderableboolean · default true
Enables pointer and keyboard ordering. Per-option isReorderDisabled keeps a visible disabled grip and fixed-order barrier.
hasSelectAllboolean · default false
Shows Add all for transfer-enabled available options.
hasClearboolean · default false
Shows Clear for transfer-enabled selected options; transfer-disabled values remain selected.
renderOption(option: TransferListOption<T>) => ReactNode
Customizes row content without replacing built-in actions and constraints.
selectedEmptyTextstring · default 'No selected options'
Message shown when the selected list is empty.
availableEmptyTextstring · default 'No available options'
Message shown when every option has been selected.
noResultsTextstring · default 'No results'
Message shown when search has no matches in a list.
xstyleStyleXStyles
StyleX styles for the TransferList root.

Anatomy

Field label and description · required

Names and explains the selector outside the popover so placement does not change with supporting text.

Selector trigger · required

Summarizes the committed value and opens the transfer-list popover.

Search · optional

Filters the selected and available lists without changing their values and uses a uniform 12px inset.

Selected and available sections · required

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.

Row actions · required

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.

Bulk actions · optional

Optional Clear and Add all text actions that honor transfer-disabled options.

Apply and Cancel footer · optional

Rendered only for staged behavior. Apply commits a changed draft; Cancel closes without changing the committed value.

Live announcements · required

Reports add, remove, bulk, and reorder results without relying on visual position alone.

Theming

Targets

astryx-transfer-list
astryx-transfer-list-collection
astryx-transfer-list-panel

Visual props: side

astryx-transfer-list-item

Visual props: side, state