Transfer List
Accessible, unstyled dual-listbox transfer list with typed items, search, and bulk moves.
|
|
| Package |
@vizejs/ui/transfer-list |
| Maturity |
stable |
| Own the source |
vize lib pull transfer-list |
| Requires |
collection, composite-navigation, context, controllable-state, id, typeahead |
| Aliases |
transfer list, dual listbox, shuttle, list builder |
| Covers |
Ant Design Transfer, PrimeVue PickList, MUI Transfer List |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { TransferList, TransferListRoot, TransferListAction, TransferListEmpty, TransferListItem, TransferListPanel, TransferListSearch } from "@vizejs/ui/transfer-list";
Or copy the source into your project with vize lib pull transfer-list (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Dual listbox that moves team members between "Available" and "Assigned" with search and bulk moves. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import {
TransferList,
TransferListAction,
TransferListEmpty,
TransferListItem,
TransferListPanel,
TransferListSearch,
} from "@vizejs/ui/transfer-list";
const people = ["Ada", "Alan", "Grace", "Katherine", "Linus", "Margaret"];
const assigned = ref<readonly string[]>(["Grace"]);
const availableId = useId();
const assignedId = useId();
</script>
<template>
<TransferList
v-slot="{ visibleSource, visibleTarget }"
v-model="assigned"
:items="people"
name="assignees"
>
<div>
<span :id="availableId">Available</span>
<TransferListSearch side="source" aria-label="Search available people" />
<TransferListPanel side="source" :aria-labelledby="availableId">
<TransferListItem v-for="person in visibleSource" :key="person" :value="person">
{{ person }}
</TransferListItem>
</TransferListPanel>
<TransferListEmpty side="source">Nobody left to assign</TransferListEmpty>
</div>
<div>
<TransferListAction action="move-selected-to-target">></TransferListAction>
<TransferListAction action="move-all-to-target">>></TransferListAction>
<TransferListAction action="move-selected-to-source"><</TransferListAction>
<TransferListAction action="move-all-to-source"><<</TransferListAction>
</div>
<div>
<span :id="assignedId">Assigned</span>
<TransferListPanel side="target" :aria-labelledby="assignedId">
<TransferListItem v-for="person in visibleTarget" :key="person" :value="person">
{{ person }}
</TransferListItem>
</TransferListPanel>
<TransferListEmpty side="target">Nobody assigned yet</TransferListEmpty>
</div>
</TransferList>
</template>
API
TransferList<T>
Source: src/families/selection/transfer-list/transfer-list-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
items (required) |
readonly T[] |
required |
Every item, in canonical order; the source panel shows items not in the target. |
modelValue |
readonly T[] |
undefined |
Controlled target (right-hand) values. undefined selects uncontrolled behavior. |
defaultValue |
readonly T[] |
[] |
Initial target values for uncontrolled use and the value restored by reset. |
by |
TransferListBy<T> |
undefined |
Compare values by a property key or with a custom equality function. |
itemText |
(item: T) => string |
undefined |
Human-readable text used by search and typeahead. |
itemDisabled |
(item: T) => boolean |
undefined |
Keep individual items in place. |
filter |
TransferListFilter<T> |
undefined |
Search filter. Defaults to an accent- and case-insensitive "contains" match. |
sourceQuery |
string |
undefined |
Controlled source search text. |
targetQuery |
string |
undefined |
Controlled target search text. |
max |
number |
Infinity |
Maximum number of target values. |
orderMode |
TransferListOrderMode |
"append" |
append adds moved items at the end; source-order keeps items order. |
disabled |
boolean |
false |
Disable every panel, item, and action. |
name |
string |
undefined |
Form field name; each target value submits one hidden input. |
form |
string |
undefined |
Id of the owning form when rendered outside it. |
formValue |
(item: T) => string |
undefined |
Serialize a value for form submission. |
Events
| Event |
Payload |
Description |
update:modelValue |
[value: readonly T[]] |
Fired when the target list requests a new controlled value. |
change |
[ value: readonly T[], previous: readonly T[], moved: readonly T[], direction: TransferListDirection, nativeEvent: Event | null, ] |
Fired after a move: next target, previous target, moved items, direction, and the triggering event. |
update:sourceQuery |
[query: string] |
Fired when the source search text requests a new controlled value. |
update:targetQuery |
[query: string] |
Fired when the target search text requests a new controlled value. |
Slots
| Slot |
Slot props |
Description |
default |
TransferListSlotState<T> |
Panels, search fields, and action buttons. Receives both sides and capacity. |
Exposed
| Member |
Type |
Description |
moveToSource |
— |
|
moveToTarget |
— |
|
reset |
— |
|
target |
— |
|
TransferListRoot<T>
Source: src/families/selection/transfer-list/transfer-list-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
items (required) |
readonly T[] |
required |
Every item, in canonical order; the source panel shows items not in the target. |
modelValue |
readonly T[] |
undefined |
Controlled target (right-hand) values. undefined selects uncontrolled behavior. |
defaultValue |
readonly T[] |
[] |
Initial target values for uncontrolled use and the value restored by reset. |
by |
TransferListBy<T> |
undefined |
Compare values by a property key or with a custom equality function. |
itemText |
(item: T) => string |
undefined |
Human-readable text used by search and typeahead. |
itemDisabled |
(item: T) => boolean |
undefined |
Keep individual items in place. |
filter |
TransferListFilter<T> |
undefined |
Search filter. Defaults to an accent- and case-insensitive "contains" match. |
sourceQuery |
string |
undefined |
Controlled source search text. |
targetQuery |
string |
undefined |
Controlled target search text. |
max |
number |
Infinity |
Maximum number of target values. |
orderMode |
TransferListOrderMode |
"append" |
append adds moved items at the end; source-order keeps items order. |
disabled |
boolean |
false |
Disable every panel, item, and action. |
name |
string |
undefined |
Form field name; each target value submits one hidden input. |
form |
string |
undefined |
Id of the owning form when rendered outside it. |
formValue |
(item: T) => string |
undefined |
Serialize a value for form submission. |
Events
| Event |
Payload |
Description |
update:modelValue |
[value: readonly T[]] |
Fired when the target list requests a new controlled value. |
change |
[ value: readonly T[], previous: readonly T[], moved: readonly T[], direction: TransferListDirection, nativeEvent: Event | null, ] |
Fired after a move: next target, previous target, moved items, direction, and the triggering event. |
update:sourceQuery |
[query: string] |
Fired when the source search text requests a new controlled value. |
update:targetQuery |
[query: string] |
Fired when the target search text requests a new controlled value. |
Slots
| Slot |
Slot props |
Description |
default |
TransferListSlotState<T> |
Panels, search fields, and action buttons. Receives both sides and capacity. |
Exposed
| Member |
Type |
Description |
target |
readonly T[] |
Current target values. |
moveToTarget |
() => readonly T[] |
Move checked items to the target. |
moveToSource |
() => readonly T[] |
Move checked items back to the source. |
reset |
() => boolean |
Restore defaultValue. |
TransferListAction
Source: src/families/selection/transfer-list/transfer-list-action.vue
Props
| Prop |
Type |
Default |
Description |
action (required) |
TransferListAction |
required |
Move checked or all visible items in one direction. |
ariaLabel |
string |
undefined |
Accessible name; defaults to an English description of the action. |
Slots
| Slot |
Slot props |
Description |
default |
{ readonly enabled: boolean } |
Button content such as an arrow icon. Receives availability. |
TransferListEmpty
Source: src/families/selection/transfer-list/transfer-list-empty.vue
Props
| Prop |
Type |
Default |
Description |
side (required) |
TransferListSide |
required |
Panel whose emptiness this message reports. |
Slots
| Slot |
Slot props |
Description |
default |
{ readonly query: string; readonly filtered: boolean } |
Message shown while the panel has no visible items. Receives the search text. |
TransferListItem<T>
Source: src/families/selection/transfer-list/transfer-list-item.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned option id. null and undefined select a deterministic fallback. |
value (required) |
T |
required |
Item value from the panel's items. |
textValue |
string |
undefined |
Typeahead text; defaults to the root itemText. |
Slots
| Slot |
Slot props |
Description |
default |
TransferListItemSlotState<T> |
Item content. Receives checked, active, and disabled state. |
Exposed
| Member |
Type |
Description |
checked |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
TransferListPanel
Source: src/families/selection/transfer-list/transfer-list-panel.vue
Props
| Prop |
Type |
Default |
Description |
side (required) |
TransferListSide |
required |
Which list this panel shows: available (source) or chosen (target) items. |
ariaLabel |
string |
undefined |
Accessible name, e.g. "Available" or "Selected". |
ariaLabelledby |
string |
undefined |
Space-separated ids of a visible panel heading. |
Slots
| Slot |
Slot props |
Description |
default |
TransferListPanelSlotState<unknown> |
Render one TransferListItem per visible item. Values are untyped here; the root slot exposes the same lists typed as T. |
TransferListSearch
Source: src/families/selection/transfer-list/transfer-list-search.vue
Props
| Prop |
Type |
Default |
Description |
side (required) |
TransferListSide |
required |
Panel this field filters. |
placeholder |
string |
undefined |
Hint text shown while empty. |
ariaLabel |
string |
undefined |
Accessible name, e.g. "Search available". |
Behavior
Contract
TransferList is a headless, strongly typed dual listbox. transfer-list-root.vue
is generic over the item type T: items holds every item, v-model holds the
target (right-hand) values as readonly T[], and the source panel shows the
rest. transfer-list-panel.vue renders one multiselectable role="listbox"
per side with active-descendant navigation and typeahead from the shared
collection registry and composite navigation. transfer-list-item.vue
renders role="option" where aria-selected means "checked for moving".
transfer-list-search.vue filters one panel, transfer-list-action.vue moves
checked or all visible items, and transfer-list-empty.vue reports an empty
panel. Named lists submit one hidden input per target value. No CSS ships.
Normative Behavior
| # |
State |
Input |
Outcome |
Proven by |
| T1 |
any |
render |
two multiselectable listboxes split items by the target model; hidden inputs mirror the target |
renders two multiselect listboxes that split items by the target model |
| T2 |
source |
click items / "Add selected" |
toggles checked state (never for disabled items), moves checked items, emits change(value, previous, moved, direction, event) |
clicking checks items and the selected action moves them, emitting the move |
| T3 |
source-order |
move / "Add all" / "Remove all" |
keeps canonical order, skips disabled items, disables the action when nothing can move |
source-order mode keeps canonical order and move-all skips disabled items |
| T4 |
max |
"Add all" |
moves only up to max, publishes data-full, disables further adds |
max caps the target and disables moving further items |
| T5 |
search |
type / Escape / ArrowDown |
filters one panel accent-insensitively, move-all moves only visible items, empty state distinguishes filtering, Escape clears, ArrowDown enters the panel |
search filters one panel accent-insensitively and drives the empty state |
| T6 |
panel focused |
arrows / Home / Space / Shift+Arrow / Ctrl+A / Enter |
navigates skipping disabled items, toggles, extends checks, checks all visible, moves checked (or the active item); the highlight recovers to a neighbour |
keyboard: navigation, Space toggles, Shift+Arrow extends, Ctrl+A, Enter moves |
| T7 |
any |
double-click / printable key |
moves one item; typeahead highlights by text |
double-click moves an item and typeahead finds items by text |
| T8 |
controlled |
move |
emits and waits for the parent |
controlled target waits for the parent |
| T9 |
disabled |
any |
panels leave the tab order; items, actions, and search are inert |
disabled transfer lists block every interaction |
| T10 |
any |
exposed API |
moveToTarget, moveToSource, reset share state |
exposed methods move checked items |
| T11 |
no provider |
mount a part |
throws VIZE_UI_CONTEXT_MISSING |
parts require a TransferList provider |
| T12 |
pure helper |
transferItems |
respects max, order modes, and direction |
transferItems respects max, order modes, and direction |
SSR
Panel ids derive from the root id; markup is byte-identical across isolated
requests and hydrates without warnings (renders byte-identical TransferList markup across isolated SSR requests, hydrates TransferList without mismatches or node replacement).