Skip to content

Transfer

WxTransfer is two lists and a pair of arrows: everything there is on the left, everything chosen on the right. It is the shape for choosing from a set you also need to see — permissions, roles, the columns of a report.

Two lists and what moves between them
Available0/7
Granted0/2
Granted: posts.read, media.upload
In a panel too narrow for two columns
Available0/2
Selected0/1
The container decides, not the window — this one is 360px wide inside a page that is not.

Usage

vue
<script setup lang="ts">
import { ref } from 'vue'
import { WxTransfer, type TransferItem } from '@webx-ui/core'

const permissions: TransferItem[] = [
  { value: 'posts.read', label: 'Read posts' },
  {
    value: 'posts.write',
    label: 'Write posts',
    description: 'Create and edit, without publishing',
  },
  { value: 'settings.write', label: 'Change settings', disabled: true },
]

const granted = ref<string[]>(['posts.read'])
</script>

<template>
  <wx-transfer
    v-model="granted"
    :items="permissions"
    :titles="['Available', 'Granted']"
    searchable
  />
</template>

items is everything, both panels together. v-model is the right-hand panel — the values that were chosen — so saving is the model as it stands, and the left panel is simply everything else.

The right panel is in the model's order

The model is an array, and the order in it is the order it will be saved in. So the right panel shows it in that order rather than the catalogue's: a panel that showed something else would be quietly lying about what is about to be sent.

When to reach for it, and when not

It earns its width when the reader needs to see what they did not choose — a list of forty permissions where the point is which ones are missing. When they do not, a WxSelect with multiple, or a Find button over a SortableList, says the same thing in a quarter of the room.

Where the chosen order matters and is dragged rather than picked, that is SortableList — two of them sharing a group pass rows between themselves.

Moving

  • Tick and press an arrow. The arrow is off until there is something for it to move.
  • The checkbox in a heading ticks what the search has left showing, and nothing behind it — a select-all that quietly took forty hidden rows with it would be a trap.
  • Double-click a row to move that one.
  • disabled on an item pins it to the side it is on, from either direction.

Every move is announced in a live region: 3 moved to Granted.

Narrow

Side by side is the point of the component, so when there is no longer room for it the panels stack and the arrows turn to point up and down. That is decided by the panel's width, not the window's — a transfer in a 380px drawer on a wide screen is narrow, and the window has nothing to say about it.

Props

PropTypeDefaultDescription
modelValue(string | number)[][]The values on the right
itemsTransferItem[][]Everything, both panels together
titles[string, string]['Available', 'Selected']Headings of the two panels
searchablebooleanfalseA search field over each panel
searchPlaceholderstring'Search'Placeholder for both
heightnumber | string260Height of a list
emptyTextstring'Nothing here'Shown in a panel with nothing in it
size'sm' | 'md' | 'lg''md'Size of the controls
disabledbooleanfalseNothing moves
toRightLabelstring'Move to the right'Accessible name for the first arrow
toLeftLabelstring'Move to the left'Accessible name for the second

TransferItem: { value, label?, description?, disabled? }label falls back to the value.

Events: update:modelValue; change ({ values, to }, where to is 'left' or 'right').

Slots: item ({ item, side }) — one row; empty ({ side }).

Accessibility

Each row is a real checkbox with a real label, so a keyboard reaches them by tabbing and ticks them with space; the arrows are buttons with names, since an arrow glyph is not one. The heading checkbox carries the indeterminate state when only some of the shown rows are ticked, and each move is announced.

A double-click is a shortcut, never the only way to do something.

Released under the MIT License.