A transfer list lets people pick several options by moving them from an available list to a chosen list, and the form submits the chosen ones.
A .transfer-list holds a .list.transfer-available, a
.transfer-actions group of four buttons, and a
.list.transfer-chosen. Each row is a
list row with a <label> around a
checkbox that has a value and no
name. Name each list with aria-label or
aria-labelledby, and give the container a data-name.
Check some rows below and press a button to move them.
<form>
<div class="transfer-list" data-name="toppings">
<ul class="list transfer-available" aria-label="Available toppings">
<li><label><input type="checkbox" value="olives"> Olives</label></li>
<li><label><input type="checkbox" value="peppers"> Peppers</label></li>
</ul>
<div class="transfer-actions">
<button type="button" class="icon-button" data-transfer="add" aria-label="Add checked toppings">
<span class="material-symbols" aria-hidden="true">chevron_right</span>
</button>
<button type="button" class="icon-button" data-transfer="add-all" aria-label="Add all toppings">
<span class="material-symbols" aria-hidden="true">keyboard_double_arrow_right</span>
</button>
<button type="button" class="icon-button" data-transfer="remove" aria-label="Remove checked toppings">
<span class="material-symbols" aria-hidden="true">chevron_left</span>
</button>
<button type="button" class="icon-button" data-transfer="remove-all" aria-label="Remove all toppings">
<span class="material-symbols" aria-hidden="true">keyboard_double_arrow_left</span>
</button>
</div>
<ul class="list transfer-chosen" aria-label="Chosen toppings">
<li><label><input type="checkbox" value="mushrooms"> Mushrooms</label></li>
</ul>
</div>
</form>
Moving rows
data-transfer="add" moves the checked rows of the available list
to the end of the chosen list, and remove moves the checked rows of
the chosen list back. add-all and remove-all move every
row. Moved rows arrive unchecked, and a row whose checkbox is disabled stays
where it is. The chosen list keeps the order rows arrived in, and a row moved
back returns to the place it had before the first move.
The rows are native checkboxes, so Tab reaches each one and Space checks it.
There is no listbox role or arrow-key model to learn. Focus stays on the button
after a move, and a button pressed with nothing to move does nothing. Give every
button type="button", or it submits the form as well; a semantics
rule flags one without it.
The lists sit side by side and follow the page direction. In a right-to-left page the icons flip so they keep pointing at the list they move rows to. Below the medium window size the lists stack, with the buttons in a row between them.
The submitted value
When the form builds its data, each row of the chosen list adds its checkbox
value under the container's data-name, in the chosen list's order.
The checkboxes carry no name, because a checked box only means the
row is selected to move. A form reset puts every row back where it was before
the first move.
A list inside a <fieldset disabled> submits nothing, as the
fieldset's own fields do. A chosen row whose own checkbox is disabled cannot be
moved, but it still submits.
Events and announcements
After each move the container fires a bubbling
expressive:transfer event whose detail is
{ rows, from, to }: the moved rows in their new order, then the
two lists. A polite live region after the container announces the move.
Expressive.TransferList.i18n.moved holds its text,
Moved {count} to {list} by default, where
{list} is the destination list's name.
Expressive.TransferList.i18n.moved = '{count} moved to {list}';
document.addEventListener('expressive:transfer', (event) => {
const { rows, to } = event.detail;
console.log(rows.length, 'rows moved to', to.getAttribute('aria-label'));
});
The root bundle starts the behavior on load. With the modular entry, call
TransferList.Init() once. The listeners are on the document, so a
transfer list added later needs no call.
Limits
The script writes the values, so a page with the script blocked submits nothing
for the list. The list must sit inside its form. One that reaches a form
through a form attribute, or sits in a form inside a shadow root,
adds no entries and ignores resets. There is no required check, so
validate the chosen list on submit when it must not be empty.
Tokens
Override these on the .transfer-list.
| Token | Default |
|---|---|
--md-comp-transfer-list-gap | 16px |
--md-comp-transfer-list-max-block-size | 20rem; a list scrolls past it |
--md-comp-transfer-list-shape | the medium corner |