---
title: Transfer
description: A control that moves items between two lists.
category: Data
status: beta
since: 0.3.4
tags: [transfer, data, permission]
syncStatus: reviewed
verified: true
---

## Usage

### Filterable Transfer
`filterable` shows a filter input on both sides; `emptyText` localizes the empty state.
:::TuffDemoWrapper{demo="TransferTransferDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import { ref } from 'vue'

  const selected = ref<Array<string | number>>(['release'])
  const data = [
    { key: 'release', label: 'Release channel' },
    { key: 'beta', label: 'Beta channel' },
    { key: 'snapshot', label: 'Snapshot channel' },
    { key: 'internal', label: 'Internal rollout', disabled: true },
  ]
  </script>

  <template>
    <TxTransfer
      v-model="selected"
      :data="data"
      :titles="['Available channels', 'Selected']"
      filterable
      filter-placeholder="Filter channels"
      empty-text="No items"
      add-aria-label="Add selected channels"
      remove-aria-label="Remove selected channels"
    />
  </template>
---
:::

### Ranked Target List
`orderable` shows ranks and move up / move down in the target panel, in `modelValue` order; `maxHeight` keeps long lists scrolling inside the panel.
:::TuffDemoWrapper{demo="TransferOrderableDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import { ref } from 'vue'

  const selected = ref<Array<string | number>>(['gpt-1', 'gpt-3', 'gpt-2'])
  const data = [
    { key: 'gpt-1', label: 'Primary · high quality' },
    { key: 'gpt-2', label: 'Fallback · balanced' },
    { key: 'gpt-3', label: 'Fast · low latency' },
    { key: 'gpt-4', label: 'Long context' },
  ]
  </script>

  <template>
    <TxTransfer
      v-model="selected"
      :data="data"
      :titles="['Available models', 'Call order']"
      :empty-text="['No more models', 'Nothing ranked yet — add a model first']"
      :max-height="260"
      filterable
      orderable
      target-order="push"
      move-up-aria-label="Move up"
      move-down-aria-label="Move down"
    />
  </template>
---
:::

### Permission Resource Grants
`TxTransfer` only moves resources under host control; `TxTree`, `TxTreeSelect`, and `TxTimeline` handle the scope, owner team, and audit trail.
:::TuffDemoWrapper{demo="ComponentsPermissionOrchestrationDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import { ref } from 'vue'

  const assignedKeys = ref<Array<string | number>>(['read-docs', 'publish-notes'])
  const resources = [
    { key: 'read-docs', label: 'Read docs' },
    { key: 'publish-notes', label: 'Publish notes' },
    { key: 'manage-assets', label: 'Manage assets' },
    { key: 'rotate-keys', label: 'Rotate keys', disabled: true },
    { key: 'view-billing', label: 'View billing' },
  ]
  </script>

  <template>
    <TxTransfer
      v-model="assignedKeys"
      :data="resources"
      :titles="['Assignable resources', 'Granted']"
      filterable
      filter-placeholder="Filter resources"
      empty-text="No resources"
      add-aria-label="Add selected resources"
      remove-aria-label="Remove selected resources"
    />
  </template>
---
:::

### Best Practices

- Keep `modelValue` controlled by the host and store only item keys; keep every `data[].key` unique and stable.
- Keep disabled rows in `data` rather than filtering them out, so unavailable resources don't look missing.
- Use `targetOrder="push"` when selection order matters; turn on `orderable` when users rank the target by hand (priorities, fallback chains).
- Inside a dialog or drawer, set `maxHeight` to fit the container (e.g. `min(56dvh, 520px)`); lower `minHeight` in compact spots.
- In a localized UI, override `titles`, `emptyText`, and every `*AriaLabel` prop.

## API Reference

### Props

::TuffPropsTable
---
rows:
  - name: modelValue
    type: "Array<string | number>"
    default: "[]"
    description: Keys shown in the target list.
  - name: data
    type: TransferItem[]
    default: "[]"
    description: All items, each with key, label, and optional disabled.
  - name: titles
    type: "[string, string]"
    default: "['Source', 'Target']"
    description: Source and target panel titles.
  - name: filterable
    type: boolean
    default: false
    description: Shows a filter input on both sides.
  - name: filterPlaceholder
    type: string
    default: "''"
    description: Placeholder of the filter input.
  - name: emptyText
    type: "string | [string, string]"
    default: "'No data'"
    description: Text for an empty panel; a tuple gives each side its own.
  - name: maxHeight
    type: "string | number"
    default: "'320px'"
    description: Panel height cap, px for numbers; writes --tx-transfer-max-height.
  - name: minHeight
    type: "string | number"
    default: "'240px'"
    description: Panel height floor, px for numbers; writes --tx-transfer-min-height.
  - name: addAriaLabel
    type: string
    default: "'Move selected items to target'"
    description: Accessible name of the add button.
  - name: removeAriaLabel
    type: string
    default: "'Move selected items to source'"
    description: Accessible name of the remove button.
  - name: selectAllAriaLabel
    type: string
    default: "'Select all'"
    description: Accessible name of each select-all box, followed by the panel title.
  - name: moveUpAriaLabel
    type: string
    default: "'Move item up'"
    description: Accessible name of the move-up button, followed by the row label.
  - name: moveDownAriaLabel
    type: string
    default: "'Move item down'"
    description: Accessible name of the move-down button, followed by the row label.
  - name: targetOrder
    type: "'original' | 'push'"
    default: "'original'"
    description: Orders the target by original data order or append order; ignored with `orderable`.
  - name: orderable
    type: boolean
    default: false
    description: Shows ranks and move up/down in the target panel; order follows modelValue.
---
::

### Events

| Event | Payload | Description |
|------|------|------|
| `update:modelValue` | `Array<string \| number>` | Selected keys updated. |
| `change` | `Array<string \| number>` | Fires together with `update:modelValue`. |

## Overview

- Each panel's select-all box covers only visible, enabled rows; a partial selection reports `aria-checked="mixed"`.
- Rows move by checking them and pressing a middle button, or by double-clicking; double-clicking a disabled row does nothing.
- A middle button turns primary and shows the pending count once rows are checked; with none checked it is disabled.
- With `orderable`, ranks and first/last checks come from the unfiltered `modelValue`, so filtering never shifts them.

## Technologies

- Panels read `--tx-transfer-max-height` (default `320px`) and `--tx-transfer-min-height` (default `240px`); the props only write those variables, and the cap keeps scrolling inside the list.
- Source: `packages/tuffex/packages/components/src/transfer/`.

<TuffDocSourceLink label="View source" />
