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
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
-
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.
-
source-labelstring'Available' - Heading for the left (available) pane. Attribute:
source-label. -
target-labelstring'Selected' - Heading for the right (selected) pane. Attribute:
target-label. -
optionsArray<{value:string,label:string,disabled?:boolean}>[] - The full universe of items. Items whose value is in
valuerender 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. -
searchablebooleanfalse - Adds a filter input to each pane that narrows that pane only, case-insensitively. Move-all respects the filter.
-
readonlybooleanfalse - Prevents moving items between panes while the lists stay focusable and filterable; the selected values still submit with the form.
-
size'sm' | 'md' | 'lg''md' - Control size.
mdis the default;smandlgscale the row height and list panels. -
formAssociatedbooleantrue -
propertiesobject{ // flag(), unlike `disabled`. The exclusion in props.js is specifically // about form-associated *platform* semantics: a `disabled` content // attribute that is merely present makes the element actually disabled // per the HTML spec, and formDisabledCallback assigns the property back, // so no converter can win. Neither of these is platform-mapped — // `required` is enforced by _computeValidity() below and `readonly` by // each component's own interaction handlers — so the stock converter buys // nothing here and costs the usual bug: `required="false"` read as true, // blocking submission of a form the author meant to leave optional. // Finding #48's shape, across all 26 form controls at once. required: flag(false), readonly: flag(false), } - Lit merges static properties up the prototype chain, so every consumer
gets these without declaring them.
requiredparticipates in constraint validation below;readonlyreflects 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
Methods
-
checkValidity()boolean - Whether the control currently satisfies its constraints, per the native
constraint-validation API. Fires
invalidon the element when it does not, and reports nothing to the user. -
reportValidity()boolean - As checkValidity(), but also shows the browser's validation message against the control when it fails.
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.