Vanilla JS — Transfer between two lists
Reorder items within one list or transfer them to another. Both areas share one scope and group="tasks", with unique areaId values. Item IDs remain stable during transfer.
Try it
Section titled “Try it”Drag the left handle into the empty list on the right. The done count increases and the item leaves the first list. Reset returns all items to their original order. Transfer in this example uses pointer interaction.
Done: 0 / 3
---const { lang = 'ko' } = Astro.props;const ko = lang === 'ko';const items = [{ id: 'plan', label: ko ? '문서 설계' : 'Design the docs' }, { id: 'build', label: ko ? '예제 구현' : 'Build an example' }, { id: 'review', label: ko ? '코드 검토' : 'Review the code' }];---<vanilla-transfer class="demo" data-demo="sortable-vanilla" data-language={lang}> <div class="demo-toolbar"><span>{ko ? '손잡이를 잡고 다른 목록으로 이동하세요.' : 'Drag a handle into the other list.'}</span><button data-reset>{ko ? '초기화' : 'Reset'}</button></div> <div class="transfer-board"> <section class="transfer-column"><div class="list-heading">{ko ? '진행 예정' : 'To do'}<span data-count="todo">3</span></div> <div class="task-list" data-list="todo" aria-label={ko ? '진행 예정' : 'To do'}> {items.map(item => <div class="task" data-task={item.id} data-sortable-id={item.id}><button class="task-handle" aria-label={`${ko ? '드래그' : 'Drag'}: ${item.label}`}>⠿</button><span>{item.label}</span></div>)} </div> </section> <section class="transfer-column"><div class="list-heading">{ko ? '완료' : 'Done'}<span data-count="done">0</span></div><div class="task-list" data-list="done" aria-label={ko ? '완료' : 'Done'}></div></section> </div> <p role="status" class="demo-status">{ko ? '완료' : 'Done'}: 0 / 3</p></vanilla-transfer><script> import { createSortable, type Sortable } from 'comins-sortable'; class VanillaTransfer extends HTMLElement { instance?: Sortable; unregister?: () => void; cleanup?: () => void; connectedCallback() { const todo = this.querySelector<HTMLElement>('[data-list="todo"]')!; const done = this.querySelector<HTMLElement>('[data-list="done"]')!; const initial = [...todo.children]; const count = (list: HTMLElement) => list.querySelectorAll(':scope > [data-sortable-id]:not([data-comins-sortable-placeholder])').length; const update = () => { this.querySelector('[data-count="todo"]')!.textContent = String(count(todo)); this.querySelector('[data-count="done"]')!.textContent = String(count(done)); this.querySelector('[role="status"]')!.textContent = `${this.dataset.language === 'ko' ? '완료' : 'Done'}: ${count(done)} / 3`; }; this.instance = createSortable(todo, { areaId: 'todo', group: 'tasks', item: ':scope > [data-sortable-id]', handle: '.task-handle', onChange: update, onAfterDrag: update }); this.unregister = this.instance.registerArea(done, { areaId: 'done', group: 'tasks', item: ':scope > [data-sortable-id]', handle: '.task-handle' }); const reset = () => { this.instance?.cancel(); for (const item of initial) todo.append(item); this.instance?.refreshArea('todo'); this.instance?.refreshArea('done'); update(); }; const button = this.querySelector('[data-reset]')!; button.addEventListener('click', reset); this.cleanup = () => button.removeEventListener('click', reset); } disconnectedCallback() { this.cleanup?.(); this.unregister?.(); this.instance?.destroy(); } } if (!customElements.get('vanilla-transfer')) customElements.define('vanilla-transfer', VanillaTransfer);</script>Vanilla JS state contract
Section titled “Vanilla JS state contract”Create the first list with createSortable from the package root, then register the second through the same instance’s registerArea. The adapter applies DOM movement before onChange. If your app owns a separate data model, update it in that callback. This example uses the DOM as its source of truth. Release registration and destroy the instance on disposal.
Handles use touch-action: none and user-select: none to avoid competing touch scroll and text selection during a drag.
A temporary placeholder can still exist during onChange. Count actual item selectors instead of children.length. Refresh again in onAfterDrag to reflect cancellation or rollback.
Search terms
Section titled “Search terms”Transfer, move between lists, drag and drop, reorder, shared scope.