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, 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. formAssociatedbooleantruepropertiesobject{ // 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.
formvalidityvalidationMessagerequiredbooleanfalse
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 SelectMulti-value select with tag chips, inline search filtering, and keyboard navigation.
- Sortable ListDrag-and-drop reorderable list with grip handles, keyboard reordering support, and visual insertion indicators.
- CheckboxMulti-select form control with checked, indeterminate, and disabled states. For preferences, bulk selection, and consent forms where users toggle one or more independent options.
- Switch GroupGroups multiple toggle switches under a shared label with consistent sizing and disabled state. Supports vertical and horizontal layouts.