Skip to content

Transfer ​

Dual-panel list for moving items between source and target collections.

Source10
Item 1
Item 3
Item 4
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target2
Item 2
Item 5
vue
<script setup lang="ts">
import { ref } from 'vue'
import { VxTransfer } from 'veloxui'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
  disabled: i === 3 || i === 7,
}))

const value = ref([2, 5])
</script>

<template>
  <VxTransfer v-model="value" :data="data" />
</template>

Filterable ​

Source10
Item 2
Item 3
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target2
Item 1
Item 4
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([1, 4])
</script>

<template>
  <VxTransfer v-model="value" :data="data" filterable />
</template>

Custom Titles ​

Available Cities6
Los Angeles
Houston
Phoenix
Philadelphia
San Antonio
San Diego
Selected Cities2
New York
Chicago
vue
<script setup lang="ts">
import { ref } from 'vue'

const cities = [
  { key: 'nyc', label: 'New York' },
  { key: 'la', label: 'Los Angeles' },
  { key: 'chi', label: 'Chicago' },
  { key: 'hou', label: 'Houston' },
  { key: 'phx', label: 'Phoenix' },
]
const value = ref(['nyc', 'chi'])
</script>

<template>
  <VxTransfer
    v-model="value"
    :data="cities"
    :titles="['Available Cities', 'Selected Cities']"
    filterable
  />
</template>

Field Mapping ​

Use key-field and label-field to map custom data properties without transforming your data.

Users4
Alice
Charlie
Diana
Eve
Team1
Bob
vue
<script setup lang="ts">
import { ref } from 'vue'

const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' },
  { id: 4, name: 'Diana' },
  { id: 5, name: 'Eve' },
]
const value = ref([2])
</script>

<template>
  <VxTransfer
    v-model="value"
    :data="users"
    key-field="id"
    label-field="name"
    :titles="['Users', 'Team']"
  />
</template>

Panel Width ​

Source4
Item 2
Item 4
Item 5
Item 6
Target2
Item 1
Item 3
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 6 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([1, 3])
</script>

<template>
  <!-- Number = pixels -->
  <VxTransfer v-model="value" :data="data" :panel-width="300" />

  <!-- String = any CSS value -->
  <VxTransfer v-model="value" :data="data" panel-width="50%" />
</template>

Sort ​

Items in both panels are sorted using the provided comparator function.

Sorted A-Z10
Item 10
Item 11
Item 12
Item 2
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Sorted A-Z2
Item 1
Item 3
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([3, 1])
</script>

<template>
  <VxTransfer
    v-model="value"
    :data="data"
    :sort-method="(a, b) => a.label.localeCompare(b.label)"
    :titles="['Sorted A-Z', 'Sorted A-Z']"
  />
</template>

Move All ​

Show move-all buttons (double arrows) to transfer all enabled items at once.

Source12
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target0
No data
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
  disabled: i === 3 || i === 7,
}))
const value = ref([])
</script>

<template>
  <VxTransfer v-model="value" :data="data" show-move-all />
</template>

Button Texts ​

Source12
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target0
No data
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([])
</script>

<template>
  <VxTransfer v-model="value" :data="data" :button-texts="['Add', 'Remove']" />
</template>

Custom Button Slots ​

Each slot receives move, disabled, and checkedCount.

Source7
Item 1
Item 2
Item 3
Item 5
Item 6
Item 7
Item 8
Target1
Item 4
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 8 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([4])
</script>

<template>
  <VxTransfer v-model="value" :data="data">
    <template #to-target-button="{ move, disabled, checkedCount }">
      <button :disabled="disabled" @click="move">
        Add {{ checkedCount || '' }} →
      </button>
    </template>
    <template #to-source-button="{ move, disabled, checkedCount }">
      <button :disabled="disabled" @click="move">
        ← Remove {{ checkedCount || '' }}
      </button>
    </template>
  </VxTransfer>
</template>

Custom Item Slot ​

Source11
Item 1#1
Item 2#2
Item 4#4
Item 5#5
Item 6#6
Item 7#7
Item 8#8
Item 9#9
Item 10#10
Item 11#11
Item 12#12
Target1
Item 3#3
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([3])
</script>

<template>
  <VxTransfer v-model="value" :data="data">
    <template #item="{ item }">
      <span>{{ item.label }}</span>
      <span style="margin-left: auto; font-size: 11px; opacity: 0.5">
        #{{ item.key }}
      </span>
    </template>
  </VxTransfer>
</template>

Customize panel headers and footers. Slots receive { checked, total, query }.

Pick items (6)
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Target0
No data
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 6 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([])
</script>

<template>
  <VxTransfer v-model="value" :data="data">
    <template #source-header="{ total }">
      <div class="vx-transfer__header" style="background: var(--muted);">
        <span style="font-size: 13px; font-weight: 600;">
          Pick items ({{ total }})
        </span>
      </div>
    </template>
    <template #target-footer="{ total }">
      <div class="vx-transfer__footer">
        <span class="vx-transfer__footer-info">
          {{ total }} selected
        </span>
      </div>
    </template>
  </VxTransfer>
</template>

Drag and Drop ​

Drag items between panels. Enable with draggable. The target panel highlights when dragging over it.

Source10
Item 1
Item 3
Item 4
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target2
Item 2
Item 5
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
  disabled: i === 3 || i === 7,
}))
const value = ref([2, 5])
</script>

<template>
  <VxTransfer v-model="value" :data="data" draggable />
</template>

Keyboard Navigation ​

Click a panel body to focus it, then use Arrow Up/Down to navigate items and Space or Enter to toggle selection. The focused item gets an outline ring.

Source12
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target0
No data
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([])
</script>

<template>
  <!-- Click inside a panel, then use Arrow keys + Space/Enter -->
  <VxTransfer v-model="value" :data="data" />
</template>

Scroll Pagination ​

Set page-size to load items incrementally as the user scrolls. Only the visible page renders — more items appear automatically when scrolling near the bottom.

All Records (100)95
Record 1
Record 2
Record 3
Record 4
Record 6
Record 7
Record 8
Record 9
Record 11
Record 12
Record 13
Record 14
Record 15
Record 16
Record 17
Selected5
Record 5
Record 10
Record 20
Record 30
Record 40
vue
<script setup lang="ts">
import { ref } from 'vue'

const bigData = Array.from({ length: 100 }, (_, i) => ({
  key: i + 1,
  label: `Record ${i + 1}`,
  disabled: i === 9,
}))
const value = ref([5, 10, 20, 30, 40])
</script>

<template>
  <VxTransfer
    v-model="value"
    :data="bigData"
    :page-size="15"
    filterable
    :titles="['All Records (100)', 'Selected']"
  />
</template>

Virtual Scroll ​

For massive datasets (1000+ items), enable virtual-scroll with item-height for smooth 60fps scrolling. Only visible rows are rendered in the DOM.

5,000 Items4997
Virtual item 1
Virtual item 2
Virtual item 3
Virtual item 4
Virtual item 5
Virtual item 6
Virtual item 7
Virtual item 8
Virtual item 9
Virtual item 10
Virtual item 11
Virtual item 12
Virtual item 13
Virtual item 14
Virtual item 15
Virtual item 16
Virtual item 17
Virtual item 18
Virtual item 19
Selected3
Virtual item 100
Virtual item 200
Virtual item 500
vue
<script setup lang="ts">
import { ref } from 'vue'

const hugeData = Array.from({ length: 5000 }, (_, i) => ({
  key: i + 1,
  label: `Virtual item ${i + 1}`,
}))
const value = ref([100, 200, 500])
</script>

<template>
  <VxTransfer
    v-model="value"
    :data="hugeData"
    virtual-scroll
    :item-height="36"
    filterable
    :titles="['5,000 Items', 'Selected']"
  />
</template>

Remote Data (fetchOptions) ​

Load source data from an API with infinite scroll pagination. Pass an async fetchOptions(query, page) function that returns TransferItem[]. Scroll to the bottom to load the next page. Search input is debounced and triggers a fresh fetch.

Comments (remote)0
No data
Selected0
No data
vue
<script setup lang="ts">
import { ref } from 'vue'

async function fetchComments(query: string, page: number) {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/comments?_limit=10&_page=${page}`
  )
  const data = await res.json()
  return data.map((c: any) => ({ key: c.id, label: c.name }))
}

const value = ref([])
</script>

<template>
  <VxTransfer
    v-model="value"
    :fetch-options="fetchComments"
    filterable
    :titles="['Comments (remote)', 'Selected']"
  />
</template>

Loading ​

External loading state — shows a spinner at the bottom of the source panel.

Source3
Item 2
Item 3
Item 4
Target1
Item 1
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 4 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([1])
const loading = ref(true)
</script>

<template>
  <VxTransfer v-model="value" :data="data" :loading="loading" />
</template>

Target Order ​

By default items keep their original data order. Use target-order="push" to append items in the order they were selected.

Source11
Item 1
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target1
Item 2
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([2])
</script>

<template>
  <VxTransfer v-model="value" :data="data" target-order="push" />
</template>

Button Visibility ​

Hide one or both move buttons.

Source4
Item 2
Item 4
Item 5
Item 6
Target2
Item 1
Item 3
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 6 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([1, 3])
</script>

<template>
  <!-- Only allow adding to target (no remove button) -->
  <VxTransfer v-model="value" :data="data" :show-to-source-button="false" />

  <!-- Only allow removing from target (no add button) -->
  <VxTransfer v-model="value" :data="data" :show-to-target-button="false" />
</template>

Custom Empty State ​

Use source-empty and target-empty slots to customize what's shown when a panel has no items.

Source0
📭
Nothing here yet
Add items to get started
Target0
🎯
No selections
Move items from the left
vue
<template>
  <VxTransfer :data="[]">
    <template #source-empty>
      <div style="text-align: center; padding: 20px;">
        <div style="font-size: 24px;">📭</div>
        <div>Nothing here yet</div>
        <div style="opacity: 0.7;">Add items to get started</div>
      </div>
    </template>
    <template #target-empty>
      <div style="text-align: center; padding: 20px;">
        <div style="font-size: 24px;">🎯</div>
        <div>No selections</div>
        <div style="opacity: 0.7;">Move items from the left</div>
      </div>
    </template>
  </VxTransfer>
</template>

Disabled ​

Source6
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Target0
No data
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 6 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([])
</script>

<template>
  <VxTransfer v-model="value" :data="data" disabled />
</template>

Props ​

PropTypeDefaultDescription
modelValue(string | number)[][]Keys in target list (v-model)
dataTransferItem[][]Full data source
titles[string, string]['Source', 'Target']Panel titles
filterablebooleanfalseShow search input
filterPlaceholderstring'Search...'Search placeholder
filterMethod(query, item) => boolean—Custom filter function
disabledbooleanfalseDisable all interaction
loadingbooleanfalseExternal loading state
targetOrder'original' | 'push''original'Order of target items
buttonTexts[string?, string?][]Labels for move buttons
showToTargetButtonbooleantrueShow move-to-target button
showToSourceButtonbooleantrueShow move-to-source button
showMoveAllbooleanfalseShow move-all buttons
pageSizenumber0Items per page (0 = show all)
scrollDistancenumber30Pixels from bottom to trigger load
fetchOptions(query, page) => Promise<TransferItem[]>—Async remote source
debouncenumber300Debounce for remote search (ms)
keyFieldstring'key'Property name for item key
labelFieldstring'label'Property name for item label
panelWidthnumber | string240Panel width (px or CSS value)
sortMethod(a, b) => number—Sort comparator for items
draggablebooleanfalseEnable drag and drop between panels
virtualScrollbooleanfalseEnable virtual scrolling for large lists
itemHeightnumber36Row height in px (for virtual scroll)

Events ​

EventTypeDescription
update:modelValue(value)Target keys changed
change(value, direction, movedKeys)Items moved
load-more(page)Next remote page requested

Slots ​

SlotPropsDescription
item{ item }Custom item rendering
source-header{ checked, total, query }Custom source panel header
target-header{ checked, total, query }Custom target panel header
source-footer{ checked, total }Custom source panel footer
target-footer{ checked, total }Custom target panel footer
source-empty—Source empty state
target-empty—Target empty state
to-target-button{ move, disabled, checkedCount }Custom move-to-target button
to-source-button{ move, disabled, checkedCount }Custom move-to-source button

Exposed Methods ​

MethodDescription
moveRight()Move checked items to target
moveLeft()Move checked items to source
moveAllRight()Move all enabled items to target
moveAllLeft()Move all enabled items to source

TransferItem Interface ​

ts
interface TransferItem {
  key: string | number
  label: string
  disabled?: boolean
  [k: string]: any
}