Transfer

Two lists and the buttons between them, for moving items from a source set into a selected one.

Accessibility

Transfer is built from real elements, so the rows and the buttons work with the keyboard. Names and announcements are left to you.

What it renders

Each panel body is a real <ul> with one <li> per row, so a screen reader announces a list and counts it. The rest has no role, and the only ARIA attribute is the oneWay remove button's label.
  • The two lists are not named. A panel's title is plain text in the header, not a heading, and nothing ties the list to it. The English locale leaves both titles empty, so an unconfigured Transfer reads as two anonymous lists.
  • Pass titles for sighted readers. An aria-label on Transfer names the whole control, not either list. To name each list, draw it yourself in a children body.
  • The counts in each header (“3/12 items”) are plain text. They are correct, and nothing announces them. See below.
  • The two search inputs have no label. Their name is the placeholder, which is the same on both sides, so a reader hears “Search here” twice. Pass showSearch a different placeholder per use.

Reaching a row

A row is an <li> with a checkbox and a text span inside. The checkbox is the only part the keyboard reaches.
  • Each row's checkbox is its tab stop. Space toggles it, and clicking the row text does the same for the mouse.
  • Every row is a tab stop, so a panel of forty rows is forty stops before the move buttons. Use pagination, or a searchable Select, well before the list gets that long.
  • A checkbox is named by the text in its row. A render that returns an icon and no words leaves it unnamed, so keep a word in the row.
  • Under oneWay, rows on the right have a real remove button named from the locale's “Remove”, the same on every row. Set locale.remove to something clearer (“Remove from assigned”).
  • A disabled item disables its checkbox or remove button, which takes the row out of the tab order. Say why in the row's text.

The header checkbox, and the menu beside it

The select-all checkbox is a real checkbox, and its partial state is announced as partially checked.
  • It has no label, and no prop gives it one. You can change the count line beside it with selectAllLabels (a node or a function per panel), so make it say which panel it counts.
  • It disappears in two cases: with pagination, and on the right panel under oneWay.
  • The selection menu opens on hover only. Its trigger is an icon with no name that cannot take focus, so “Select all data” and the other entries are out of keyboard reach.
  • Pass selectionsIcon a Button with an aria-label to give the menu a trigger the keyboard can reach and a screen reader can name.

Naming the two buttons

The move buttons are real buttons, disabled when their side has nothing selected. The kit's default buttons show only an arrow, so they have no name.
  • Two strings in actions name them. Each string becomes the button's text and its name, and the arrow icons go.
  • Or pass your own Buttons with an aria-label. “Add to assigned” and “Remove from assigned” say more than “Move right”, which is only a direction and flips in RTL.
  • Do not rely on titles to explain them. Nothing links the titles to the buttons, so a reader on a button may not have heard them.
  • You cannot enable a button that Transfer disabled, even with disabled={false}.

Announcing a move

Nothing announces that items moved. The counts change and the rows appear on the other side, but focus stays on the button, and a screen reader reads none of it.
  • Announce it yourself. onChange gives you the keys that moved and the direction, which is what to write into a polite live region: “3 items added to Assigned, 12 remaining”.
  • Say the counts, not only the verb, so the reader does not have to count two lists.
  • Announce when a panel empties, too. Its rows are replaced by the empty state, and a reader tabbing through them runs out.
  • Do not move focus to the other panel to make the change audible. The button is where the reader is working.

Contrast and motion

Every color comes from the theme, so contrast depends on the theme. The kit's default theme is checked. A custom seed is not.
  • A selected row has a fill behind its text. Check the text against that fill, not the panel, in both modes.
  • A disabled row is only grayed, which is one more reason to write the reason in the row.
  • status="error" only changes the panel borders. Pair it with a message.
  • The fades here turn off with token: { motion: false } on a ThemeScope. Put any motion you add behind motion-safe:.