# Transfer list

> Moves options between an available list and a chosen list, and submits the chosen ones with the form.

## Anatomy

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](/lists.html.md) row with a `<label>` around a [checkbox](/checkboxes.html.md) 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.

-    Olives
-    Peppers
-    Onions
-    Basil
-    Pineapple
-    Spinach

-    Mushrooms

Submit Reset

Submitted:

nothing yet

```
<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 |
