MiHCM
primitives/@mihcm/ui·v0.21.0·stable

TransferList

Dual-pane shuttle: unassigned on the left, assigned on the right, four move buttons in the middle. Per-side filter and multi-select. Web only.

Use cases

  • Assigning a subset of N items into a bucket — roles ↔ permissions, designations ↔ job categories, modules ↔ a role template.
  • The user needs to see both pools side by side and shuttle items between them.

When NOT to use

  • For a flat multi-select with bulk actions → use CheckboxGrid.
  • For a tiny fixed list → use multiple Switch rows.
  • For a single binary choice → use Checkbox.

Platform support

Web: side-by-side dual panes with four move buttons between them. Native: vertically stacked Available/Assigned sections, each with a FlatList and filter input. Items toggle between panes via tap. Bulk "Assign All ↓" / "Remove All ↑" buttons sit between the sections.

Anatomy

┌──────────────────┬─────┬──────────────────┐
│ Available        │ ›   │ Assigned         │
│ Showing 15 of 18 │ »   │ 3 items          │
├──────────────────┼─────┼──────────────────┤
│ 🔍 Filter…       │ ‹   │ 🔍 Filter…       │
├──────────────────┼─────┼──────────────────┤
│ ☐ Engineer       │ «   │ ☐ Senior Eng     │
│ ☐ Product        │     │ ☐ Designer       │
│ ☐ Marketing      │     │ ☐ Researcher     │
│ ☐ Sales          │     │                  │
│ …                │     │                  │
└──────────────────┴─────┴──────────────────┘
        ↑              ↑                  ↑
        left pane     move buttons    right pane

Move buttons

GlyphAction
Move selected left → right
»Move all left → right
Move selected right → left
«Move all right → left

Each button disables when its action would be a no-op (no left selection → and » disabled, no right items → « disabled, etc.).

Controlled state

You control only the assignment — per-side multi-select is internal to the component:

const [assigned, setAssigned] = useState<Record<string, boolean>>({});
 
<TransferList
  items={ALL_DESIGNATIONS}
  assigned={assigned}
  onAssignedChange={setAssigned}
/>

This is intentional: per-side selection is transient UI state that doesn't belong in your form model.

Labels

The pane titles default to "Available" / "Assigned". Customise via leftLabel / rightLabel to match the domain:

<TransferList
  items={}
  assigned={}
  onAssignedChange={}
  leftLabel="Available designations"
  rightLabel="Linked to this role"
/>

Responsive and custom rendering

  • layout="responsive" stacks panes on small screens and switches to a horizontal shuttle at md.
  • layout="vertical" keeps the panes stacked for narrow drawers and mobile-first workflows. Move controls stay horizontal, but their glyphs switch to down/up arrows to match the vertical flow.
  • actionsPlacement="top" | "between" | "bottom" controls where move buttons appear.
  • allowMoveAll={false} removes bulk movement when the workflow requires deliberate assignment.
  • renderItem supports rich rows with descriptions, badges, or metadata. Keep the returned content text-readable because each row remains a button with role="option".

Composition

  • Each pane has a built-in search field. For long lists (100+ items), the search is the primary navigation.
  • Use leftLabel / rightLabel to match your domain ("Available designations" / "Linked to this role").
  • assigned is a Set<string> — the component never mutates it.
  • Pair with SectionHeader above the transfer list for page context.
  • CheckboxGrid — simpler multi-select without dual-pane semantics.
  • Select — single selection from a dropdown.
  • DataTable — tabular data with row selection.