Input · Transfer

Transfer

A more intuitive and efficient multiple-selection selector, which can reveal more information about options, and supports search functions. The disadvantage is that it takes up more space

Demos

How to import

<script lang="ts">
    import { Transfer } from '@fibrae/semi-ui-svelte';
</script>

Basic Usage

dataSource should have value、label、key.

总个数:100
选项名称 0
选项名称 1
选项名称 2
选项名称 3
选项名称 4
选项名称 5
选项名称 6
选项名称 7
选项名称 8
选项名称 9
选项名称 10
选项名称 11
选项名称 12
选项名称 13
选项名称 14
选项名称 15
选项名称 16
选项名称 17
选项名称 18
选项名称 19
选项名称 20
选项名称 21
选项名称 22
选项名称 23
选项名称 24
选项名称 25
选项名称 26
选项名称 27
选项名称 28
选项名称 29
选项名称 30
选项名称 31
选项名称 32
选项名称 33
选项名称 34
选项名称 35
选项名称 36
选项名称 37
选项名称 38
选项名称 39
选项名称 40
选项名称 41
选项名称 42
选项名称 43
选项名称 44
选项名称 45
选项名称 46
选项名称 47
选项名称 48
选项名称 49
选项名称 50
选项名称 51
选项名称 52
选项名称 53
选项名称 54
选项名称 55
选项名称 56
选项名称 57
选项名称 58
选项名称 59
选项名称 60
选项名称 61
选项名称 62
选项名称 63
选项名称 64
选项名称 65
选项名称 66
选项名称 67
选项名称 68
选项名称 69
选项名称 70
选项名称 71
选项名称 72
选项名称 73
选项名称 74
选项名称 75
选项名称 76
选项名称 77
选项名称 78
选项名称 79
选项名称 80
选项名称 81
选项名称 82
选项名称 83
选项名称 84
选项名称 85
选项名称 86
选项名称 87
选项名称 88
选项名称 89
选项名称 90
选项名称 91
选项名称 92
选项名称 93
选项名称 94
选项名称 95
选项名称 96
选项名称 97
选项名称 98
选项名称 99
已选个数:0
暂无内容,可从左侧勾选
Code
<script lang="ts">
    import { Transfer, type TransferDataItem, type TransferValue } from '@fibrae/semi-ui-svelte';

    const data = Array.from({ length: 100 }, (_v, i) => ({
        label: `选项名称 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    }));

    const onChange = (values: TransferValue, items: TransferDataItem[]) => console.log(values, items);
</script>

<Transfer dataSource={data} {onChange} style="width: 568px; height: 416px" />

Grouped

Set type to groupList

For grouped dataSource, the first-level child elements must have title and children attributes, structure reference <GroupItem>

Does not support multi-level nesting

总个数:13
类别A
A-1
A-2
A-3
类别B
B-1
B-2
B-3(disabled)
类别C
C-1
C-2
C-3
C-4
C-5
C-6
C-7
已选个数:1
B-3(disabled)
Code
<script lang="ts">
    import { Transfer } from '@fibrae/semi-ui-svelte';

    const dataWithGroup = [
        {
            title: '类别A',
            children: [
                { label: 'A-1', value: 1, disabled: false, key: 1 },
                { label: 'A-2', value: 2, disabled: false, key: 2 },
                { label: 'A-3', value: 3, disabled: false, key: 3 },
            ],
        },
        {
            title: '类别B',
            children: [
                { label: 'B-1', value: 4, disabled: false, key: 4 },
                { label: 'B-2', value: 5, disabled: false, key: 5 },
                { label: 'B-3(disabled)', value: 6, disabled: true, key: 6 },
            ],
        },
        {
            title: '类别C',
            children: [
                { label: 'C-1', value: 7, disabled: false, key: 7 },
                { label: 'C-2', value: 8, disabled: false, key: 8 },
                { label: 'C-3', value: 9, disabled: false, key: 9 },
                { label: 'C-4', value: 10, disabled: false, key: 10 },
                { label: 'C-5', value: 11, disabled: false, key: 11 },
                { label: 'C-6', value: 12, disabled: false, key: 12 },
                { label: 'C-7', value: 13, disabled: false, key: 13 },
            ],
        },
    ];
</script>

<Transfer dataSource={dataWithGroup} onChange={(values, items) => console.log(values, items)} style="width: 568px" type="groupList" value={[6]} />

Custom filtering logic, custom option data rendering

Use filter to customize the search logic. When it returns true, it means that the current item meets the filter rules and keeps the display of the current item in the list. If it returns false, it means it does not match, and the current item will be hidden.
When type is treeList, if you need to customize search logic, you need to set filter to true and set a custom search function through filterTreeNode of treeProps.

Using the sourceItem snippet, you can customize the rendering structure of each source data on the left.
Using the selectedItem snippet you can customize the rendering structure of each selected item on the right.
The custom fields of an item (such as abbr and color in the demo) are all on the item snippet parameter. To replace only the text of the rows, the label snippet is simpler: it applies to the source row, the selected row and the drag copy.

总个数:6
夏
夏可漫
申
申悦
文
文嘉茂
曲
曲晨一
二
曲晨二
三
曲晨三
已选个数:2
夏
夏可漫
申
申悦
Code
<script lang="ts">
    import { IconClose } from '@fibrae/semi-icons-svelte';
    import { Avatar, Button, Checkbox, Highlight, Transfer, type AvatarColor, type TransferDataItem, type TransferValue } from '@fibrae/semi-ui-svelte';

    interface Person extends TransferDataItem {
        abbr: string;
        color: AvatarColor;
    }

    let searchText = $state('');

    const customFilter = (searchValue: string, item: TransferDataItem) => String(item.value).includes(searchValue) || String(item.label).includes(searchValue);

    const data = [
        { label: '夏可漫', value: 'xiakeman@example.com', abbr: '夏', color: 'amber', area: 'US', key: 1 },
        { label: '申悦', value: 'shenyue@example.com', abbr: '申', color: 'indigo', area: 'UK', key: 2 },
        { label: '文嘉茂', value: 'wenjiamao@example.com', abbr: '文', color: 'cyan', area: 'HK', key: 3 },
        { label: '曲晨一', value: 'quchenyi@example.com', abbr: '曲', color: 'blue', area: 'India', key: 4 },
        { label: '曲晨二', value: 'quchener@example.com', abbr: '二', color: 'blue', area: 'India', key: 5 },
        { label: '曲晨三', value: 'quchensan@example.com', abbr: '三', color: 'blue', area: 'India', key: 6 },
    ] satisfies Person[];

    const inputProps = { placeholder: '搜索姓名或邮箱' };

    const onChange = (values: TransferValue, items: TransferDataItem[]) => console.log(values, items);
</script>

<Transfer dataSource={data} filter={customFilter} {inputProps} {onChange} onSearch={(value) => (searchText = value)} style="width: 568px" value={['xiakeman@example.com', 'shenyue@example.com']}>
    {#snippet sourceItem({ item, checked, onChange: onItemChange })}
        {@const person = item as Person}
        <div class="components-transfer-demo-source-item">
            <Checkbox {checked} onChange={onItemChange} style="height: 52px; align-items: center">
                <Avatar color={person.color} size="small">{person.abbr}</Avatar>
                <div class="info">
                    <div class="name">
                        <Highlight searchWords={[searchText]} sourceString={String(item.label)} />
                    </div>
                    <div class="email">
                        <Highlight searchWords={[searchText]} sourceString={String(item.value)} />
                    </div>
                </div>
            </Checkbox>
        </div>
    {/snippet}
    {#snippet selectedItem({ item, onRemove })}
        {@const person = item as Person}
        <div class="components-transfer-demo-selected-item">
            <Avatar color={person.color} size="small">{person.abbr}</Avatar>
            <div class="info">
                <div class="name">{item.label}</div>
                <div class="email">{item.value}</div>
            </div>
            <Button aria-label={`移除 ${item.label}`} class="remove" size="small" theme="borderless" type="tertiary" onclick={onRemove}>
                {#snippet icon()}<IconClose />{/snippet}
            </Button>
        </div>
    {/snippet}
</Transfer>
.components-transfer-demo-selected-item {
    .remove {
        opacity: 0;
    }
    &:hover,
    &:focus-within {
        .remove {
            opacity: 1;
        }
    }
}

.components-transfer-demo-selected-item,
.components-transfer-demo-source-item {
    height: 52px;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 12px;
    &:hover {
        background-color: var(--semi-color-fill-0);
    }
    .info {
        margin-left: 8px;
        flex-grow: 1;
    }
    .name {
        font-size: 14px;
        line-height: 20px;
    }
    .email {
        font-size: 12px;
        line-height: 16px;
        color: var(--semi-color-text-2);
    }
}

Disabled

总个数:20
选项名称 0
选项名称 1
选项名称 2
选项名称 3
选项名称 4
选项名称 5
选项名称 6
选项名称 7
选项名称 8
选项名称 9
选项名称 10
选项名称 11
选项名称 12
选项名称 13
选项名称 14
选项名称 15
选项名称 16
选项名称 17
选项名称 18
选项名称 19
已选个数:2
选项名称 2
选项名称 4
Code
<script lang="ts">
    import { Transfer, type TransferDataItem, type TransferValue } from '@fibrae/semi-ui-svelte';

    const data = Array.from({ length: 20 }, (_v, i) => ({
        label: `选项名称 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    }));

    const onChange = (values: TransferValue, items: TransferDataItem[]) => console.log(values, items);
</script>

<Transfer dataSource={data} disabled {onChange} style="width: 568px; height: 416px" value={[2, 4]} />

Drag and drop sort

Set draggable to true to enable the drag sort function.

总个数:30
选项名称 0
选项名称 1
选项名称 2
选项名称 3
选项名称 4
选项名称 5
选项名称 6
选项名称 7
选项名称 8
选项名称 9
选项名称 10
选项名称 11
选项名称 12
选项名称 13
选项名称 14
选项名称 15
选项名称 16
选项名称 17
选项名称 18
选项名称 19
选项名称 20
选项名称 21
选项名称 22
选项名称 23
选项名称 24
选项名称 25
选项名称 26
选项名称 27
选项名称 28
选项名称 29
已选个数:2
选项名称 2
选项名称 4
Code
<script lang="ts">
    import { Transfer } from '@fibrae/semi-ui-svelte';

    const data = Array.from({ length: 30 }, (_, i) => ({
        label: `选项名称 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    }));
</script>

<Transfer dataSource={data} draggable onChange={(values, items) => console.log(values, items)} style="width: 568px; height: 416px" value={[2, 4]} />

Left panel pagination

When there are many options on the left, you can enable pagination through the pagination prop.

pagination accepts an object with the following properties:

  • pageSize: Number of items per page, default is 10
  • currentPage: Current page number (controlled mode)
  • defaultCurrentPage: Default current page number (uncontrolled mode)
  • onPageChange: Callback when page changes
总个数:100
选项名称 0
选项名称 1
选项名称 2
选项名称 3
选项名称 4
选项名称 5
选项名称 6
选项名称 7
选项名称 8
选项名称 9
已选个数:0
暂无内容,可从左侧勾选
Code
<script lang="ts">
    import { Transfer } from '@fibrae/semi-ui-svelte';

    const data = Array.from({ length: 100 }, (_, i) => ({
        label: `选项名称 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    }));
</script>

<Transfer dataSource={data} onChange={(values, items) => console.log(values, items)} pagination={{ pageSize: 10 }} style="width: 568px; height: 416px" />

Left panel pagination + controlled page

You can control the current page number through pagination.currentPage.

当前页码: 1
总个数:100
选项名称 0
选项名称 1
选项名称 2
选项名称 3
选项名称 4
选项名称 5
选项名称 6
选项名称 7
选项名称 8
选项名称 9
已选个数:0
暂无内容,可从左侧勾选
Code
<script lang="ts">
    import { Button, ButtonGroup, Transfer } from '@fibrae/semi-ui-svelte';

    let currentPage = $state(1);
    const data = Array.from({ length: 100 }, (_, i) => ({
        label: `选项名称 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    }));
</script>

<div>
    <ButtonGroup style="margin-bottom: 12px">
        <Button onclick={() => (currentPage = 1)}>第1页</Button>
        <Button onclick={() => (currentPage = 2)}>第2页</Button>
        <Button onclick={() => (currentPage = 5)}>第5页</Button>
        <Button onclick={() => (currentPage = 10)}>第10页</Button>
    </ButtonGroup>
    <div>当前页码: {currentPage}</div>
    <Transfer dataSource={data} onChange={(values, items) => console.log(values, items)} pagination={{ pageSize: 10, currentPage, onPageChange: (page) => (currentPage = page) }} style="width: 568px; height: 416px" />
</div>

Drag and drop + custom selected rendering

Set draggable to true to enable the drag and drop sorting function; use the selectedItem snippet to customize the rendering of the selected items on the right; You can define the trigger as any content you want and add styles. With draggable, the snippet parameter handle is a Snippet<[Snippet]>: write the trigger as a snippet (such as grip) and wrap it with {@render handle?.(grip)}. Declare grip outside the <Transfer> tag; inside it, it would be taken as a prop of Transfer. Without draggable, handle is not provided.

总个数:6
夏
夏可漫
申
申悦
文
文嘉茂
曲
曲晨一
二
曲晨二
三
曲晨三
已选个数:2
夏
夏可漫
申
申悦
Code
<script lang="ts">
    import { IconClose, IconHandle } from '@fibrae/semi-icons-svelte';
    import { Avatar, Button, Checkbox, Transfer, type AvatarColor, type TransferDataItem, type TransferValue } from '@fibrae/semi-ui-svelte';

    interface Person extends TransferDataItem {
        abbr: string;
        color: AvatarColor;
    }

    const customFilter = (searchValue: string, item: TransferDataItem) => String(item.value).includes(searchValue) || String(item.label).includes(searchValue);

    const data = [
        { label: '夏可漫', value: 'xiakeman@example.com', abbr: '夏', color: 'amber', area: 'US', key: 1 },
        { label: '申悦', value: 'shenyue@example.com', abbr: '申', color: 'indigo', area: 'UK', key: 2 },
        { label: '文嘉茂', value: 'wenjiamao@example.com', abbr: '文', color: 'cyan', area: 'HK', key: 3 },
        { label: '曲晨一', value: 'quchenyi@example.com', abbr: '曲', color: 'blue', area: 'India', key: 4 },
        { label: '曲晨二', value: 'quchener@example.com', abbr: '二', color: 'blue', area: 'India', key: 5 },
        { label: '曲晨三', value: 'quchensan@example.com', abbr: '三', color: 'blue', area: 'India', key: 6 },
    ] satisfies Person[];

    const inputProps = { placeholder: '搜索姓名或邮箱' };

    const onChange = (values: TransferValue, items: TransferDataItem[]) => console.log(values, items);
</script>

{#snippet grip()}
    <IconHandle class="semi-right-item-drag-handler" />
{/snippet}

<Transfer dataSource={data} draggable filter={customFilter} {inputProps} {onChange} style="width: 568px" value={['xiakeman@example.com', 'shenyue@example.com']}>
    {#snippet sourceItem({ item, checked, onChange: onItemChange })}
        {@const person = item as Person}
        <div class="components-transfer-demo-source-item">
            <Checkbox {checked} onChange={onItemChange} style="height: 52px; align-items: center">
                <Avatar color={person.color} size="small">{person.abbr}</Avatar>
                <div class="info">
                    <div class="name">{item.label}</div>
                    <div class="email">{item.value}</div>
                </div>
            </Checkbox>
        </div>
    {/snippet}
    {#snippet selectedItem({ item, handle, onRemove })}
        {@const person = item as Person}
        {#if handle}
            <div class="components-transfer-demo-selected-item">
                {@render handle(grip)}
                <Avatar color={person.color} size="small">{person.abbr}</Avatar>
                <div class="info">
                    <div class="name">{item.label}</div>
                    <div class="email">{item.value}</div>
                </div>
                <Button aria-label={`移除 ${item.label}`} class="remove" size="small" theme="borderless" type="tertiary" onclick={onRemove}>
                    {#snippet icon()}<IconClose />{/snippet}
                </Button>
            </div>
        {/if}
    {/snippet}
</Transfer>

Custom rendering header information in panel

The sourceHeader and selectedHeader snippets allow users to customize the header information of the left and right panels. The snippet parameter types are TransferSourceHeaderSnippetProps and TransferSelectedHeaderSnippetProps:

type TransferSourceHeaderSnippetProps = {
    num: number; // The total number of data or the number of filtered results
    showButton: boolean; // Whether to show select all/unselect all buttons
    allChecked: boolean; // Whether the current data has been selected
    onAllClick: () => void; // Function that should be called after clicking the select/unselect all button
    leafOnlyNum?: number; // Only provided when type is treeList, represents the number of leaf nodes
};

type TransferSelectedHeaderSnippetProps = {
    num: number; // The total number of selected data
    showButton: boolean; // Whether to show the clear button
    onClear: () => void; // Function that should be called after clicking the clear button
};

The example is as follows:

共 30 项
选项名称 0
选项名称 1
选项名称 2
选项名称 3
选项名称 4
选项名称 5
选项名称 6
选项名称 7
选项名称 8
选项名称 9
选项名称 10
选项名称 11
选项名称 12
选项名称 13
选项名称 14
选项名称 15
选项名称 16
选项名称 17
选项名称 18
选项名称 19
选项名称 20
选项名称 21
选项名称 22
选项名称 23
选项名称 24
选项名称 25
选项名称 26
选项名称 27
选项名称 28
选项名称 29
0 项已选
暂无内容,可从左侧勾选
Code
<script lang="ts">
    import { Button, Transfer } from '@fibrae/semi-ui-svelte';

    const data = Array.from({ length: 30 }, (_v, i) => ({
        label: `选项名称 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    }));

    const headerStyle = 'margin: 10px 0 0 10px; height: 24px; display: flex; align-items: center';
</script>

<Transfer dataSource={data} style="width: 568px; height: 416px">
    {#snippet sourceHeader({ num, showButton, allChecked, onAllClick })}
        <div style={headerStyle}>
            <span>共 {num} 项</span>
            {#if showButton}
                <Button size="small" theme="borderless" type="tertiary" onclick={onAllClick}>
                    {allChecked ? '取消全选' : '全选'}
                </Button>
            {/if}
        </div>
    {/snippet}
    {#snippet selectedHeader({ num, showButton, onClear })}
        <div style={headerStyle}>
            <span>{num} 项已选</span>
            {#if showButton}
                <Button size="small" theme="borderless" type="tertiary" onclick={onClear}>清空</Button>
            {/if}
        </div>
    {/snippet}
</Transfer>

Fully custom rendering

Semi provides the sourcePanel and selectedPanel snippets, allowing you to completely customize the rendering structure of the left and right panels. With this function, you can directly reuse the logic capabilities inside the Transfer to implement the Transfer component with a highly customized style structure
The parameter type of sourcePanel is TransferSourcePanelSnippetProps, from which you can get data to render your Panel structure

interface TransferSourcePanelSnippetProps {
    value: Array<string | number>; // value of all selected items
    loading: boolean | undefined; // Whether loading
    noMatch: boolean; // Whether there is no matching item that matches the current search value
    filterData: Array<Item>; // items that match the current search value
    sourceData: Array<Item>; // All items
    propsDataSource: Array<Item> | Array<GroupItem> | Array<TreeItem>; // The dataSource of Transfer
    allChecked: boolean; // Whether to select all
    showNumber: number; // Filter the number of results
    inputValue: string; // the value of the input search box
    selectedItems: Map<string | number, Item>; // collection of all selected items, the key of the Map is item.key
    onSearch: (searchString: string) => void; // The function that should be called when the search box changes, the input parameter is the search value
    onAllClick: () => void; // The function that should be called when all the buttons on the left are clicked
    onSelectOrRemove: (item: Item) => void; // The function that should be called when selecting or deleting a single option, the input should be the current operation item
    onSelect: (values: Array<string | number>) => void; // controlled batch selection, the input is the values of the selected items
}

The parameter type of selectedPanel is TransferSelectedPanelSnippetProps

interface TransferSelectedPanelSnippetProps {
    length: number; // number of selected options
    selectedData: Array<Item>; // All selected items collection
    onClear: () => void; // The callback function that should be called when clicking to clear
    onRemove: (item: Item) => void; // The function that should be called when deleting a single option
    onSortEnd: (payload: { oldIndex: number; newIndex: number }) => void; // The function that should be called when reordering the results
}
门店列表
待选门店: 100

海底捞门店 0

海底捞门店 1

海底捞门店 2

海底捞门店 3

海底捞门店 4

海底捞门店 5

海底捞门店 6

海底捞门店 7

海底捞门店 8

海底捞门店 9

海底捞门店 10

海底捞门店 11

海底捞门店 12

海底捞门店 13

海底捞门店 14

海底捞门店 15

海底捞门店 16

海底捞门店 17

海底捞门店 18

海底捞门店 19

海底捞门店 20

海底捞门店 21

海底捞门店 22

海底捞门店 23

海底捞门店 24

海底捞门店 25

海底捞门店 26

海底捞门店 27

海底捞门店 28

海底捞门店 29

海底捞门店 30

海底捞门店 31

海底捞门店 32

海底捞门店 33

海底捞门店 34

海底捞门店 35

海底捞门店 36

海底捞门店 37

海底捞门店 38

海底捞门店 39

海底捞门店 40

海底捞门店 41

海底捞门店 42

海底捞门店 43

海底捞门店 44

海底捞门店 45

海底捞门店 46

海底捞门店 47

海底捞门店 48

海底捞门店 49

海底捞门店 50

海底捞门店 51

海底捞门店 52

海底捞门店 53

海底捞门店 54

海底捞门店 55

海底捞门店 56

海底捞门店 57

海底捞门店 58

海底捞门店 59

海底捞门店 60

海底捞门店 61

海底捞门店 62

海底捞门店 63

海底捞门店 64

海底捞门店 65

海底捞门店 66

海底捞门店 67

海底捞门店 68

海底捞门店 69

海底捞门店 70

海底捞门店 71

海底捞门店 72

海底捞门店 73

海底捞门店 74

海底捞门店 75

海底捞门店 76

海底捞门店 77

海底捞门店 78

海底捞门店 79

海底捞门店 80

海底捞门店 81

海底捞门店 82

海底捞门店 83

海底捞门店 84

海底捞门店 85

海底捞门店 86

海底捞门店 87

海底捞门店 88

海底捞门店 89

海底捞门店 90

海底捞门店 91

海底捞门店 92

海底捞门店 93

海底捞门店 94

海底捞门店 95

海底捞门店 96

海底捞门店 97

海底捞门店 98

海底捞门店 99

已选同步门店: 0
暂无数据,请从左侧筛选
Code
<script lang="ts">
    import { IconSearch } from '@fibrae/semi-icons-svelte';
    import { Button, Input, Spin, Transfer, type TransferValue } from '@fibrae/semi-ui-svelte';

    const dataSource = Array.from({ length: 100 }, (_v, i) => ({
        label: `海底捞门店 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    }));

    const onChange = (values: TransferValue) => console.log(values);
</script>

<Transfer class="component-transfer-demo-custom-panel" {dataSource} {onChange}>
    {#snippet sourcePanel({ loading, noMatch, filterData, selectedItems, allChecked, onAllClick, inputValue, onSearch, onSelectOrRemove })}
        <section class="source-panel">
            <div class="panel-header sp-font">门店列表</div>
            <div class="panel-main">
                <Input aria-label="搜索门店" onChange={(value) => onSearch(value)} showClear style="width: 454px; margin: 12px 14px">
                    {#snippet prefix()}<IconSearch />{/snippet}
                </Input>
                <div class="panel-controls sp-font">
                    <span>待选门店: {filterData.length}</span>
                    <Button size="small" theme="borderless" onclick={onAllClick}>
                        {allChecked ? '取消全选' : '全选'}
                    </Button>
                </div>
                <div class="panel-list">
                    {#if loading}
                        <Spin />
                    {:else if noMatch}
                        <div class="empty sp-font">
                            {inputValue ? '无搜索结果' : '暂无内容'}
                        </div>
                    {:else}
                        {#each filterData as item (item.key)}
                            <div class="semi-transfer-item panel-item">
                                <p>{item.label}</p>
                                <Button class="panel-item-remove" size="small" theme="borderless" type="primary" onclick={() => onSelectOrRemove(item)}>
                                    {selectedItems.has(item.key) ? '删除' : '添加'}
                                </Button>
                            </div>
                        {/each}
                    {/if}
                </div>
            </div>
        </section>
    {/snippet}
    {#snippet selectedPanel({ selectedData, onClear, onRemove })}
        <section class="selected-panel">
            <div class="panel-header sp-font">
                <div>已选同步门店: {selectedData.length}</div>
                <Button size="small" theme="borderless" type="primary" onclick={onClear}>清空</Button>
            </div>
            <div class="panel-main">
                {#if !selectedData.length}
                    <div class="empty sp-font">暂无数据,请从左侧筛选</div>
                {:else}
                    {#each selectedData as item (item.key)}
                        <div class="semi-transfer-item panel-item">
                            <p>{item.label}</p>
                            <Button class="panel-item-remove" size="small" theme="borderless" type="primary" onclick={() => onRemove(item)}>删除</Button>
                        </div>
                    {/each}
                {/if}
            </div>
        </section>
    {/snippet}
</Transfer>
.component-transfer-demo-custom-panel {
    .sp-font {
        color: var(--semi-grey-9);
        font-size: 12px;
        font-weight: 500;
        line-height: 20px;
    }

    .empty {
        width: 100%;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .panel-item {
        flex-shrink: 0;
        height: 56px;
        border-radius: 4px;
        padding: 8px 12px;
        flex-wrap: wrap;
        background-color: rgba(22, 24, 35, 0.03);

        &-main {
            flex-grow: 1;
        }

        p {
            margin: 0 12px;
            flex-basis: 100%;
        }

        .panel-item-remove {
            cursor: pointer;
            color: var(--semi-color-primary);
        }
    }

    .panel-header {
        padding: 10px 12px;
        border: 1px solid rgba(22, 24, 35, 0.16);
        border-radius: 4px 4px 0 0;
        height: 38px;
        box-sizing: border-box;
        background-color: var(--semi-color-tertiary-light-default);
        display: flex;
        align-items: center;
        justify-content: space-between;

        .clear {
            cursor: pointer;
            color: var(--semi-color-primary);
        }
    }

    .source-panel {
        display: flex;
        flex-direction: column;
        width: 482px;
        height: 353px;

        .panel-main {
            border: 1px solid var(--semi-color-border);
            border-top: none;

            .panel-list {
                display: flex;
                flex-wrap: wrap;
                row-gap: 8px;
                column-gap: 8px;
                overflow-y: auto;
                height: 214px;
                margin-left: 12px;
                margin-right: 12px;
                padding-bottom: 8px;
            }
        }

        .panel-controls {
            margin: 10px 12px;
            font-size: 12px;
            line-height: 20px;

            .semi-button {
                margin-left: 8px;
                font-size: 12px;
            }
        }

        .panel-item {
            width: 176px;
        }

        margin-right: 16px;
    }

    .selected-panel {
        width: 200px;
        height: 353px;

        .panel-main {
            display: flex;
            flex-direction: column;
            overflow-y: auto;
            padding: 12px;
            border: 1px solid var(--semi-color-border);
            border-top: none;
            height: 323px;
            box-sizing: border-box;
            row-gap: 8px;
        }
    }
}

Fully custom rendering, drag and drop sorting

In a completely custom rendering scene, since the rendering of the drag area has also been completely taken over by you, you do not need to declare draggable. But you need to implement the drag and drop logic yourself, You can use the drag-and-drop tool library dnd-kit to quickly realize the function.

Besides, To support drag sorting, you need to call onSortEnd with oldIndex and newIndex as the input parameters after the drag sorting is over

Example using dnd-kit. The core dependencies that need to be used are @dnd-kit/svelte and @dnd-kit/dom. DragDropProvider provides the drag and drop context, and every selected item gets a Sortable exported from @dnd-kit/dom/sortable. Its parameters are as follows

1. Function: Obtain the necessary information during the drag and drop process through the unique id and the current index
2. Core input parameters:
    - id: unique identifier, which can be a number or a string
    - index: the current position of the item in the list; keep it up to date when the list changes
    - element: the draggable dom node
    - handle: optional, the drag handle element; the whole node is draggable when omitted
3. The second argument is the DragDropManager instance; call destroy on every Sortable when the component unmounts

The example creates the Sortable of each row with {@attach}, destroys it in the cleanup function, and keeps the selected values in bind:value. The order shown during a drag is driven by the example's own preview state, so the OptimisticSortingPlugin is removed: it moves DOM nodes itself, which collides with the keyed each of Svelte.

In onDragOver of DragDropProvider the preview order is adjusted from source and target; in onDragEnd the positions before and after the drag are worked out from the id of event.operation.source, then onSortEnd from the parameters of selectedPanel is called; event.canceled being true means the drag was canceled.

门店列表
待选门店: 100

海底捞门店 0

海底捞门店 1

海底捞门店 2

海底捞门店 3

海底捞门店 4

海底捞门店 5

海底捞门店 6

海底捞门店 7

海底捞门店 8

海底捞门店 9

海底捞门店 10

海底捞门店 11

海底捞门店 12

海底捞门店 13

海底捞门店 14

海底捞门店 15

海底捞门店 16

海底捞门店 17

海底捞门店 18

海底捞门店 19

海底捞门店 20

海底捞门店 21

海底捞门店 22

海底捞门店 23

海底捞门店 24

海底捞门店 25

海底捞门店 26

海底捞门店 27

海底捞门店 28

海底捞门店 29

海底捞门店 30

海底捞门店 31

海底捞门店 32

海底捞门店 33

海底捞门店 34

海底捞门店 35

海底捞门店 36

海底捞门店 37

海底捞门店 38

海底捞门店 39

海底捞门店 40

海底捞门店 41

海底捞门店 42

海底捞门店 43

海底捞门店 44

海底捞门店 45

海底捞门店 46

海底捞门店 47

海底捞门店 48

海底捞门店 49

海底捞门店 50

海底捞门店 51

海底捞门店 52

海底捞门店 53

海底捞门店 54

海底捞门店 55

海底捞门店 56

海底捞门店 57

海底捞门店 58

海底捞门店 59

海底捞门店 60

海底捞门店 61

海底捞门店 62

海底捞门店 63

海底捞门店 64

海底捞门店 65

海底捞门店 66

海底捞门店 67

海底捞门店 68

海底捞门店 69

海底捞门店 70

海底捞门店 71

海底捞门店 72

海底捞门店 73

海底捞门店 74

海底捞门店 75

海底捞门店 76

海底捞门店 77

海底捞门店 78

海底捞门店 79

海底捞门店 80

海底捞门店 81

海底捞门店 82

海底捞门店 83

海底捞门店 84

海底捞门店 85

海底捞门店 86

海底捞门店 87

海底捞门店 88

海底捞门店 89

海底捞门店 90

海底捞门店 91

海底捞门店 92

海底捞门店 93

海底捞门店 94

海底捞门店 95

海底捞门店 96

海底捞门店 97

海底捞门店 98

海底捞门店 99

已选同步门店: 2

海底捞门店 2

海底捞门店 4

Code
<script lang="ts">
    import { untrack, type ComponentProps } from 'svelte';
    import { DragDropManager } from '@dnd-kit/dom';
    import { isSortable, OptimisticSortingPlugin, Sortable } from '@dnd-kit/dom/sortable';
    import { DragDropProvider } from '@dnd-kit/svelte';
    import { IconHandle, IconSearch } from '@fibrae/semi-icons-svelte';
    import { Button, Input, Spin, Transfer, type TransferDataItem, type TransferOnSortEnd, type TransferValue } from '@fibrae/semi-ui-svelte';

    type ProviderProps = ComponentProps<typeof DragDropProvider>;

    const dataSource = Array.from({ length: 100 }, (_v, i) => ({
        label: `海底捞门店 ${i}`,
        value: i,
        disabled: false,
        key: `key-${i}`,
    }));

    let value = $state<TransferValue>([2, 4]);
    // The order shown while dragging; cleared on drop, so Transfer's value stays the only truth
    let preview = $state.raw<TransferDataItem[] | null>(null);

    // The default plugins keep the Accessibility one, which gives each handle a button role and Tab stop for keyboard sorting
    const manager = new DragDropManager();
    // OptimisticSortingPlugin moves DOM nodes itself, which collides with Svelte's keyed each; `preview` drives the order instead
    const sortablePlugins: ConstructorParameters<typeof Sortable>[0]['plugins'] = (defaults) => defaults.filter((plugin) => plugin !== OptimisticSortingPlugin);

    // Only the handle drags; it is found inside the row once the row's children are rendered
    const sortable = (id: string | number, position: () => number) => (row: HTMLElement) => {
        const item = new Sortable({ id, index: untrack(position), element: row, handle: row.querySelector<HTMLElement>('[data-sort-handle]') ?? undefined, plugins: sortablePlugins }, manager);
        $effect(() => {
            item.index = position();
        });
        return () => item.destroy();
    };

    const onDragOver = (selectedData: TransferDataItem[]): ProviderProps['onDragOver'] => {
        return ({ operation: { source, target } }) => {
            if (!isSortable(source) || !isSortable(target) || source.id === target.id) return;
            const next = [...(preview ?? selectedData)];
            const from = next.findIndex((item) => item.key === source.id);
            const to = next.findIndex((item) => item.key === target.id);
            if (from < 0 || to < 0) return;
            next.splice(to, 0, ...next.splice(from, 1));
            preview = next;
        };
    };

    const onDragEnd = (selectedData: TransferDataItem[], onSortEnd: TransferOnSortEnd): ProviderProps['onDragEnd'] => {
        return ({ canceled, operation: { source } }) => {
            if (!canceled && preview && source) {
                const oldIndex = selectedData.findIndex((item) => item.key === source.id);
                const newIndex = preview.findIndex((item) => item.key === source.id);
                if (oldIndex !== newIndex) onSortEnd({ oldIndex, newIndex });
            }
            preview = null;
        };
    };

    const onChange = (next: TransferValue) => console.log(next);
</script>

<Transfer bind:value class="component-transfer-demo-custom-panel" {dataSource} {onChange}>
    {#snippet sourcePanel({ loading, noMatch, filterData, selectedItems, allChecked, onAllClick, inputValue, onSearch, onSelectOrRemove })}
        <section class="source-panel">
            <div class="panel-header sp-font">门店列表</div>
            <div class="panel-main">
                <Input aria-label="搜索门店" onChange={(text) => onSearch(text)} showClear style="width: 454px; margin: 12px 14px">
                    {#snippet prefix()}<IconSearch />{/snippet}
                </Input>
                <div class="panel-controls sp-font">
                    <span>待选门店: {filterData.length}</span>
                    <Button size="small" theme="borderless" onclick={onAllClick}>
                        {allChecked ? '取消全选' : '全选'}
                    </Button>
                </div>
                <div class="panel-list">
                    {#if loading}
                        <Spin />
                    {:else if noMatch}
                        <div class="empty sp-font">
                            {inputValue ? '无搜索结果' : '暂无内容'}
                        </div>
                    {:else}
                        {#each filterData as item (item.key)}
                            <div class="semi-transfer-item panel-item">
                                <div class="panel-item-main" style="flex-grow: 1">
                                    <p style="margin: 0 12px">{item.label}</p>
                                    <Button class="panel-item-remove" size="small" theme="borderless" type="primary" onclick={() => onSelectOrRemove(item)}>
                                        {selectedItems.has(item.key) ? '删除' : '添加'}
                                    </Button>
                                </div>
                            </div>
                        {/each}
                    {/if}
                </div>
            </div>
        </section>
    {/snippet}
    {#snippet selectedPanel({ selectedData, onClear, onRemove, onSortEnd })}
        {@const ordered = preview ?? selectedData}
        <section class="selected-panel">
            <div class="panel-header sp-font">
                <div>已选同步门店: {selectedData.length}</div>
                <Button size="small" theme="borderless" type="primary" onclick={onClear}>清空</Button>
            </div>
            <div class="panel-main" style="display: block">
                <DragDropProvider {manager} onDragEnd={onDragEnd(selectedData, onSortEnd)} onDragOver={onDragOver(selectedData)}>
                    <div style="overflow: auto; display: flex; flex-direction: column; row-gap: 8px">
                        <!-- Keyed by key: dnd-kit reorders the rows mid-drag, and the keyed each moves them with their data -->
                        {#each ordered as item, position (item.key)}
                            <div {@attach sortable(item.key, () => position)}>
                                <div class="semi-transfer-item panel-item">
                                    <span aria-label={String(item.label)} data-sort-handle style="line-height: 0">
                                        <IconHandle class="pane-item-drag-handler" />
                                    </span>
                                    <div class="panel-item-main" style="flex-grow: 1">
                                        <p style="margin: 0 12px">{item.label}</p>
                                        <Button class="panel-item-remove" size="small" theme="borderless" type="primary" onclick={() => onRemove(item)}>删除</Button>
                                    </div>
                                </div>
                            </div>
                        {/each}
                    </div>
                </DragDropProvider>
            </div>
        </section>
    {/snippet}
</Transfer>

Tree Transfer

The input type is treeList, and the Tree component is used as a custom rendering list.

The properties of the default tree can be overridden by treeProps(TreeProps). The default properties of the tree on the left are

{
    multiple:true,
    disableStrictly:true,
    leafOnly:true,
    filterTreeNode:true,
    defaultExpandAll:true,
}

The Tree's own search box is hidden, and the search box of Transfer drives the filtering. Of the Tree snippets only label is forwarded, through the label snippet of Transfer, and it applies to both the tree rows on the left and the selected items on the right. A leaf node writes children: [].

总个数:12
  • Asia
  • China
  • Beijing
  • Shanghai
  • Chengdu
  • Japan
  • Osaka
  • North America
  • United States
  • Canada
  • Mexico
  • Cuba
已选个数:1
Shanghai
Code
<script lang="ts">
    import { Transfer, type TransferTreeItem, type TransferValue } from '@fibrae/semi-ui-svelte';

    const treeData: TransferTreeItem[] = [
        {
            label: 'Asia',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: 'China',
                    value: 'China',
                    key: '0-0',
                    children: [
                        { label: 'Beijing', value: 'Beijing', key: '0-0-0', children: [] },
                        { label: 'Shanghai', value: 'Shanghai', key: '0-0-1', children: [] },
                        { label: 'Chengdu', value: 'Chengdu', key: '0-0-2', children: [] },
                    ],
                },
                {
                    label: 'Japan',
                    value: 'Japan',
                    key: '0-1',
                    children: [{ label: 'Osaka', value: 'Osaka', key: '0-1-0', children: [] }],
                },
            ],
        },
        {
            label: 'North America',
            value: 'North America',
            key: '1',
            children: [
                { label: 'United States', value: 'United States', key: '1-0', children: [] },
                { label: 'Canada', value: 'Canada', key: '1-1', children: [] },
                { label: 'Mexico', value: 'Mexico', disabled: true, key: '1-2', children: [] },
                { label: 'Cuba', value: 'Cuba', key: '1-3', children: [] },
            ],
        },
    ];

    let value = $state<TransferValue>(['Shanghai']);
</script>

<div style="margin: 10px; padding: 10px; width: 600px">
    <Transfer bind:value dataSource={treeData} type="treeList" />
</div>

Tree Transfer with Custom Header Showing Leaf Node Count

When type is treeList, the TransferSourceHeaderSnippetProps parameter of sourceHeader provides an additional leafOnlyNum field, which represents the number of leaf nodes. This is useful in scenarios like file selection, where you can display only the file count instead of the total including folders.

文件总数: 7 个文件 (总节点数:10)
  • 文件夹 1
  • 文件 1-1
  • 文件 1-2
  • 子文件夹 1
  • 文件 1-1-1
  • 文件 1-1-2
  • 文件夹 2
  • 文件 2-1
  • 文件 2-2
  • 独立文件
已选个数:0
暂无内容,可从左侧勾选
Code
<script lang="ts">
    import { Button, Transfer, type TransferValue } from '@fibrae/semi-ui-svelte';

    const treeData = [
        {
            label: '文件夹 1',
            value: 'folder1',
            key: 'folder1',
            children: [
                { label: '文件 1-1', value: 'file1-1', key: 'file1-1', children: [] },
                { label: '文件 1-2', value: 'file1-2', key: 'file1-2', children: [] },
                {
                    label: '子文件夹 1',
                    value: 'subfolder1',
                    key: 'subfolder1',
                    children: [
                        { label: '文件 1-1-1', value: 'file1-1-1', key: 'file1-1-1', children: [] },
                        { label: '文件 1-1-2', value: 'file1-1-2', key: 'file1-1-2', children: [] },
                    ],
                },
            ],
        },
        {
            label: '文件夹 2',
            value: 'folder2',
            key: 'folder2',
            children: [
                { label: '文件 2-1', value: 'file2-1', key: 'file2-1', children: [] },
                { label: '文件 2-2', value: 'file2-2', key: 'file2-2', children: [] },
            ],
        },
        { label: '独立文件', value: 'file3', key: 'file3', children: [] },
    ];

    let value = $state<TransferValue>([]);
</script>

<Transfer bind:value dataSource={treeData} style="width: 600px" type="treeList">
    {#snippet sourceHeader({ num, leafOnlyNum, showButton, allChecked, onAllClick })}
        <div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--semi-color-border)">
            <span>
                <strong>文件总数:</strong>
                {leafOnlyNum !== undefined ? leafOnlyNum : num} 个文件
                {#if leafOnlyNum !== undefined}
                    <span style="color: var(--semi-color-text-2); margin-inline-start: 8px; font-size: 12px"> (总节点数:{num}) </span>
                {/if}
            </span>
            {#if showButton}
                <Button size="small" theme="borderless" type="tertiary" onclick={onAllClick}>
                    {allChecked ? '取消全选' : '全选'}
                </Button>
            {/if}
        </div>
    {/snippet}
</Transfer>

Accessibility

ARIA

  • The container of the search box is a native search element (the search landmark), named from the locale
  • When the root element has aria-label or aria-labelledby and no role, it gets role group
  • Add role list to the option list on the left and listitem to its options; in groupList every group is a listitem of the outer list, and its title names the nested list that holds the options
  • Add role list to the selected list on the right, add role listitem to the selected item; custom rendered items on either side are wrapped in an element with role listitem

Keyboard

  • The default filter ignores case
  • When disabled, the two lists are Tab stops so the keyboard can scroll their content
  • With draggable, the drag handle is a native button with a name: Tab focuses the handle, Space picks up, the arrow keys move, Space drops; the sort is announced to screen readers. Mouse dragging is unchanged
  • Until the dnd-kit needed for sorting has loaded, the handle is a placeholder with aria-disabled="true"

API Reference

Transfer

Props

PropertyDescriptionTypeDefault
dataSourceData source: an array of items for list, an array of groups for groupList, or tree data with children for treeList (a leaf writes children: []). The tree of a treeList only updates when a new array is passed, changes made in place are not seen.TransferDataSource[]
disabledWhether the whole transfer is disabled.booleanfalse
draggableEnables drag sorting of the selected items on the right. After a sort, value and onChange carry the new order.booleanfalse
emptyLeftShown when the left list has no data. The locale text appears when it is not provided; false hides the empty state.Snippet | false-
emptyRightShown when nothing is selected. The locale text appears when it is not provided; false hides the empty state.Snippet | false-
emptySearchShown when the search matches nothing. The locale text appears when it is not provided; false hides the empty state.Snippet | false-
filterFiltering logic. false hides the search box; a function replaces the default case-insensitive label match and returns true to keep an item. For a custom search in treeList, set it to true and pass the matching function as filterTreeNode in treeProps.Typeboolean | ((searchValue: string, item: TransferDataItem) => boolean)-
inputPropsProps passed to the search Input, see Input for the options. Transfer owns its value and onChange; to search from outside, call the search method.TransferInputProps-
labelContent of an item, used by the source row (the tree row in treeList), the selected row and the drag copy; the label of the item by default.TypeSnippet<[TransferLabelSnippetProps]>-
loadingWhether the options on the left are loading.boolean-
onBlurCalled once when the focus moves from inside the Transfer to outside it, with the FocusEvent; moving between the search box, checkboxes, items, remove buttons and drag handles does not call it, and a window blur (empty relatedTarget) does.Type(event: FocusEvent) => void-
onChangeCalled when the selection changes, including after a drag sort, with the array of values and the array of their items. This is the callback of Transfer, not the native change of the root element.Type(values: TransferValue, items: TransferDataItem[]) => void-
onDeselectCalled when an item is unchecked, with that item.Type(item: TransferDataItem) => void-
onSearchCalled when the text of the search box changes, with the search value. A search driven by the search method does not call it.Type(searchValue: string) => void-
onSelectCalled when an item is checked, with that item.Type(item: TransferDataItem) => void-
paginationPaging of the left panel, for list and groupList only. onPageChange lives inside the object.TransferPagination-
selectedHeaderReplaces the header of the right panel.TypeSnippet<[TransferSelectedHeaderSnippetProps]>-
selectedItemReplaces one selected item on the right. With draggable, handle is a Snippet<[Snippet]>: write the drag icon as a snippet and wrap it with {@render handle?.(grip)}. With treeList and showPath, fullPath holds the whole path.TypeSnippet<[TransferSelectedItemSnippetProps]>-
selectedPanelReplaces the whole right panel. Call onSortEnd after a drag sort you implemented has finished.TypeSnippet<[TransferSelectedPanelSnippetProps]>-
showPathtreeList only: whether the selected items on the right show their path from the root.booleanfalse
sourceHeaderReplaces the header of the left panel. leafOnlyNum is provided for treeList only.TypeSnippet<[TransferSourceHeaderSnippetProps]>-
sourceItemReplaces one item on the left. index is the position among the shown items after filtering and paging, not the position in dataSource.TypeSnippet<[TransferSourceItemSnippetProps]>-
sourcePanelReplaces the whole left panel, reusing the filtering and selection logic of Transfer.TypeSnippet<[TransferSourcePanelSnippetProps]>-
treePropstreeList only: props passed to the Tree on the left, overriding the defaults multiple, disableStrictly, leafOnly, filterTreeNode and defaultExpandAll. Transfer decides the selection and the tree data, so value, onChange, keyMaps, treeData, treeDataSimpleJson and onChangeWithObject are ignored; of the Tree snippets only label is forwarded.TransferTreeProps-
typeTransfer type: list, groupList or treeList.TransferType"list"
valueArray of the values of the selected items, two-way bindable with bind:value. A plain value is the initial selection, which Transfer then maintains itself; bind:value={get, set} makes it fully controlled. Its order is the order on the right.TransferValue-
virtualizeVirtualizes the selected list on the right, only with the default right panel and draggable off. A numeric height applies as is; a string such as 100% is measured from the remaining panel height.TransferVirtualize-

Methods

MethodDescriptionType
searchSearches as if the value were typed into the search box; onSearch is not called.Type(input: string) => void

class, style, aria-* and other undeclared attributes land on the root element of Transfer; with aria-label or aria-labelledby and no role, the root element gets role="group". value can be bound with bind:value, or given a plain value as the initial selection; there is no defaultValue. Pass the props of the search box through inputProps, and those of the Tree on the left of a treeList through treeProps.

The types of the interfaces below are exported from @fibrae/semi-ui-svelte as: TransferDataItem for Item, TransferGroupItem for GroupItem, TransferTreeItem for TreeItem, TransferVirtualize for VirtualizeProps and TransferPagination for PaginationProps.

Item Interface

An item can carry any custom fields; they appear unchanged on item in the snippet parameters and callback payloads.

propsdescriptiondata typedefault
disabledWhether to disablebooleanfalse
keyRequired, unique identification of each option, no repetition is allowedstring | number
labelOption display text, matched by the default filter; use the label snippet for other contentstring
fullPathWhen type="treeList" and showPath is true, returns the full path node array from the root node to the current nodeArray<Item>
valueThe value represented by the optionstring | number

GroupItem Interface

A group is not an item itself, it only has the following properties

propsdescriptiondata typedefault
childrenElements of the grouparray<Item>
titleGroup Namestring

TreeItem Interface

TreeItem inherits all the properties of Item

propsdescriptiondata typedefault
childrenChildren Itemsarray<TreeItem>

VirtualizeProps Interface

propsdescriptiondata typedefault
heightHeight of the virtualized list. If it is a number, it takes effect directly. If it is a string (e.g. 100%), it adapts to remaining heightnumber | string
widthWidth of the virtualized listnumber | string
itemSizeFixed height of each item rownumber

PaginationProps Interface

propsdescriptiondata typedefault
currentPageCurrent page number (controlled mode)number
defaultCurrentPageDefault current page number (uncontrolled mode)number1
pageSizeNumber of items per pagenumber10
onPageChangeCallback when page changes(currentPage: number) => void

Methods

Transfer exposes search through export function; get the instance with bind:this and call it. Its type is TransferHandle.

<script lang="ts">
    import { Transfer, type TransferHandle } from '@fibrae/semi-ui-svelte';

    let transfer = $state<TransferHandle>();
</script>

<Transfer bind:this={transfer} />
<button onclick={() => transfer?.search('Option 1')}>Search</button>

Design Tokens

These are the component's internal Sass variables and cannot be overridden. To customize styles, override the --semi-* CSS variables (see the Design Token page) or write unlayered CSS rules.
Sass variableValueUsage
$transition_duration_transfer_item-bgvar(--semi-transition_duration-none)穿梭框条目-背景色-动画持续时间
$transition_function_transfer_item-bgvar(--semi-transition_function-easeIn)穿梭框条目-背景色-过渡曲线
$transition_delay_transfer_item-bgvar(--semi-transition_delay-none)穿梭框条目-背景色-延迟时间
$color-transfer-bgvar(--semi-color-bg-1)穿梭框背景颜色 - 默认
$color-transfer-bordervar(--semi-color-border)穿梭框描边颜色 - 默认
$color-transfer_header-textvar(--semi-color-text-2)穿梭框 header 文字颜色
$color-transfer_empty-textvar(--semi-color-text-2)穿梭框空状态文字颜色
$color-transfer_group_title-textvar(--semi-color-text-2)穿梭框分组标题文字颜色
$color-transfer_disabled-textvar(--semi-color-disabled-text)穿梭框禁用文本文字颜色
$color-transfer_close_icon-iconvar(--semi-color-text-2)穿梭框关闭按钮颜色
$color-transfer_item-textvar(--semi-color-text-1)穿梭框常规文字颜色
$color-transfer_selected_item-textvar(--semi-color-text-0)穿梭框选中条目文字颜色
$color-transfer_item-bg-hovervar(--semi-color-fill-0)穿梭框条目背景色 - 悬浮
$color-transfer_item-bg-activevar(--semi-color-fill-1)穿梭框条目背景色 - 按下
$width-transfer-border1px穿梭框描边宽度
$width-transfer-minWidth402px穿梭框整体最小宽度
$height-transfer400px穿梭框高度
$height-transfer_header24px穿梭框 header 高度
$height-transfer_item-minHeight36px穿梭框条目最小高度
$width-transfer_item_close-icon$width-icon-small穿梭框选中条目删除按钮宽度
$width-transfer_left50%穿梭框左侧面板宽度
$width-transfer_right50%穿梭框左侧面板宽度
$height-transfer_left_empty36px穿梭框空状态高度
$width-transfer_left-border$border-thickness-control穿梭框双侧面板分割线宽度
$height-transfer_right_header32px穿梭框 header 高度
$height-transfer_right_empty100%穿梭框右侧空状态宽度
$width-transfer_empty100%穿梭框整体空状态宽度
$height-transfer_group_title28px穿梭框分组标题高度
$spacing-transfer_header-marginTop12px穿梭框 header 顶部外边距
$spacing-transfer_header-marginRight12px穿梭框 header 右侧外边距
$spacing-transfer_header-marginBottom8px穿梭框 header 底部外边距
$spacing-transfer_header-marginLeft12px穿梭框 header 左侧外边距
$spacing-transfer_header_all-marginLeft$spacing-base穿梭框全选按钮左侧外边距
$spacing-transfer_item-paddingTop$spacing-tight穿梭框条目顶部内边距
$spacing-transfer_item-paddingRight9px穿梭框条目右侧内边距
$spacing-transfer_item-paddingBottom$spacing-tight穿梭框条目底部内边距
$spacing-transfer_item-paddingLeft$spacing-base-tight穿梭框条目左侧内边距
$spacing-transfer_right_header-marginTop$spacing-base-tight穿梭框右侧面板 header 顶部外边距
$spacing-transfer_right_header-marginBottom0穿梭框右侧面板 header 底部外边距
$spacing-transfer_right_item_drag_handler-marginRight4px穿梭框拖动图标右侧外边距
$spacing-transfer_filter-marginTop12px穿梭框搜索顶部外边距
$spacing-transfer_filter-marginRight12px穿梭框搜索右侧外边距
$spacing-transfer_filter-marginBottom0穿梭框搜索底部外边距
$spacing-transfer_filter-marginLeft12px穿梭框搜索左侧外边距
$spacing-transfer_group_title-paddingLeft$spacing-base-tight穿梭框分组标题左侧内边距
$radius-transfervar(--semi-border-radius-medium)穿梭框圆角
$font-transfer_header_all-fontWeight600穿梭框字重