Transfer List
Dual-listbox for moving items between an available and a selected pane, ideal for permissions and settings UIs.
<arc-transfer-list> Overview
>
TransferList presents the full universe of options split across two panes — everything not yet chosen on the left ("Available") and the current value on the right ("Selected"). Users mark items with a checkbox-style highlight, then move them across with the center controls, or move a single item instantly with a double-click or the Enter key. Pane titles are customisable via `sourceLabel` and `targetLabel`, and each pane header shows a live "checked of total" count.
Options are supplied as an array of `{ value, label, disabled? }` objects and the component's `value` is the array of values currently in the Selected pane, kept in options order. With the `searchable` flag each pane gains its own case-insensitive filter input that narrows only that pane, and the move-all buttons respect the active filter. Disabled options render dimmed and can never be moved.
The component is form-associated: give it a `name` and it submits one form entry per selected value, participates in `form.reset()`, and honours `<fieldset disabled>`. Both listboxes follow the WAI-ARIA multi-select listbox pattern — one tab stop each with a roving tabindex, arrow-key navigation, Space to toggle, and Ctrl+A to check every visible item — and moves are announced through a polite live region.Guidelines
When to use
- Use for medium-sized sets (roughly 5-100 items) where users assign a subset, such as role permissions or report columns
- Enable `searchable` whenever a pane can hold more than a dozen items
- Keep option labels short — one line each — so they do not truncate in narrow panes
- Override `sourceLabel`/`targetLabel` with domain terms ("All permissions" / "Granted") for clearer context
- Listen to `arc-change` to persist the selection; the detail carries the full value array after every move
When not to use
- Do not use for a handful of options — a checkbox group or multi-select is lighter
- Do not use for thousands of items without server-side narrowing; all options render in the panes
- Do not repurpose the checked highlight as the selection itself — only items in the right pane are the value
- Do not disable options without conveying elsewhere why they cannot be moved
- Avoid placing two transfer lists side by side; each already spans two panes and needs the width
Features
- Two labeled panes with live "checked of total" counts and customisable titles
- Checkbox-style multi-highlight: mark any number of items, then transfer them in one action
- Center controls to move checked items or all (filtered) items in either direction, auto-disabled when inapplicable
- Double-click or Enter moves a single item across instantly
- Optional per-pane case-insensitive filtering via the `searchable` flag
- Full APG listbox keyboard support: roving tabindex, ArrowUp/Down, Home/End, Space to check, Ctrl+A to check all visible items
- Focus stays in the same pane on the nearest remaining item after a move; moves are announced via a polite live region
- Form-associated: submits one entry per selected value under `name` and supports form reset
- Disabled options render dimmed and are excluded from every move operation
- Responsive: panes stack vertically and controls rotate horizontal below ~560px container width
Preview
Available
0 of 0
No items
Granted
0 of 0
No items
Usage
This component requires JavaScript. No pure HTML/CSS version is available — use the Web Component directly or a framework wrapper.
<arc-transfer-list
id="permissions"
name="permissions"
searchable
source-label="Available"
target-label="Granted"
></arc-transfer-list>
<script>
const tl = document.getElementById('permissions');
tl.options = [
{ value: 'read', label: 'Read content' },
{ value: 'write', label: 'Write content' },
{ value: 'admin', label: 'Administer', disabled: true },
];
tl.value = ['read'];
tl.addEventListener('arc-change', (e) => {
console.log(e.detail.value);
});
</script> import { TransferList } from '@arclux/arc-ui-react';
const options = [
{ value: 'read', label: 'Read content' },
{ value: 'write', label: 'Write content' },
{ value: 'admin', label: 'Administer', disabled: true },
];
<TransferList
options={options}
value={['read']}
searchable
sourceLabel="Available"
targetLabel="Granted"
onArcChange={(e) => console.log(e.detail.value)}
/> <script setup>
import { TransferList } from '@arclux/arc-ui-vue';
const options = [
{ value: 'read', label: 'Read content' },
{ value: 'write', label: 'Write content' },
];
</script>
<template>
<TransferList
:options="options"
:value="['read']"
searchable
source-label="Available"
target-label="Granted"
@arc-change="(e) => console.log(e.detail.value)"
/>
</template> <script>
import { TransferList } from '@arclux/arc-ui-svelte';
const options = [
{ value: 'read', label: 'Read content' },
{ value: 'write', label: 'Write content' },
];
</script>
<TransferList
{options}
value={['read']}
searchable
source-label="Available"
target-label="Granted"
on:arc-change={(e) => console.log(e.detail.value)}
/> import { Component } from '@angular/core';
import { TransferList } from '@arclux/arc-ui-angular';
@Component({
imports: [TransferList],
template: `
<arc-transfer-list
[options]="options"
[value]="['read']"
searchable
sourceLabel="Available"
targetLabel="Granted"
(arcChange)="onChange($event)"
></arc-transfer-list>
`,
})
export class PermissionsComponent {
options = [
{ value: 'read', label: 'Read content' },
{ value: 'write', label: 'Write content' },
];
onChange(e: CustomEvent) {
console.log(e.detail.value);
}
} API
-
size'sm' | 'md' | 'lg''md' - Control size. `md` is the default; `sm` and `lg` scale the row height and list panels.
-
optionsArray<{value:string,label:string,disabled?:boolean}>[] - The full universe of items. Items whose value is in `value` render in the Selected pane; the rest render in Available.
-
valuestring[][] - Values currently in the Selected pane, kept in options order. Updated after every move and emitted via `arc-change`.
-
namestring'' - Form field name. When set, the component submits one form entry per selected value.
-
disabledbooleanfalse - Disables the whole control, preventing interaction and reducing opacity.
-
searchablebooleanfalse - Adds a filter input to each pane that narrows that pane only, case-insensitively. Move-all respects the filter.
-
source-labelstring'Available' - Heading for the left (available) pane. Attribute: `source-label`.
-
target-labelstring'Selected' - Heading for the right (selected) pane. Attribute: `target-label`.
-
readonlybooleanfalse - Prevents moving items between panes while the lists stay focusable and filterable; the selected values still submit with the form.
-
formAssociatedbooleantrue -
propertiesobject{ required: { type: Boolean, reflect: true }, readonly: { type: Boolean, reflect: true }, } - Lit merges static properties up the prototype chain, so every consumer gets these without declaring them. `required` participates in constraint validation below; `readonly` reflects for styling and is enforced by each component's interaction handlers (the mixin can't know which gestures mutate state).
-
autoValidatesbooleantrue - Components that run their own constraint-validation logic (pattern checks, range checks) opt out of the automatic required sync by overriding this to false, and own the whole validity flag set instead.
-
form -
validity -
validationMessage -
requiredbooleanfalse
Events
-
arc-changedetail: { value: string[] } - Fired after every move with `{ value }` -- the current array of selected values.
See Also
- Multi Select Multi-value select with tag chips, inline search filtering, and keyboard navigation.
- Sortable List Drag-and-drop reorderable list with grip handles, keyboard reordering support, and visual insertion indicators.
- Checkbox Multi-select form control supporting checked, indeterminate, and disabled states. Ideal for preferences, bulk-selection patterns, and consent forms where users need to toggle one or more independent options.
- Switch Group Groups multiple toggle switches under a shared label with consistent sizing and disabled state. Supports vertical and horizontal layouts.