Documentação/Guias
Listas ordenáveis
Listas, barras de abas e grades que se reordenam enquanto você arrasta — makeSortableList renderiza um rascunho, makeSortableItem cria as linhas, e o drop grava a nova ordem no seu ref. Handles, uma linha de drop, trocas.
Esta página foi traduzida por IA e pode conter imprecisões. Ler o original em inglês
Carregando a demo…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import LayersPanel from './LayersPanel.vue';
// Front to back, as a design tool lists them.
const layers = ref([
{ id: 'title', name: 'Title', kind: 'text' as const, visible: true, locked: false },
{ id: 'badge', name: 'Badge', kind: 'shape' as const, visible: true, locked: false },
{ id: 'photo', name: 'Photo', kind: 'image' as const, visible: true, locked: false },
{ id: 'card', name: 'Card', kind: 'shape' as const, visible: true, locked: true },
]);
</script>
<template>
<DnDProvider>
<div class="editor">
<!-- The artboard reads `layers`: it restacks when a drop writes the new order. -->
<div class="artboard" aria-hidden="true">
<template v-for="(layer, index) in layers" :key="layer.id">
<div v-show="layer.visible" class="shape" :class="layer.id" :style="{ zIndex: layers.length - index }">
<template v-if="layer.id === 'title'">Summer sale</template>
<template v-if="layer.id === 'badge'">-30%</template>
</div>
</template>
</div>
<LayersPanel v-model="layers" />
</div>
</DnDProvider>
</template>
<style scoped>
.editor {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 24px;
}
.artboard {
position: relative;
overflow: hidden;
width: 240px;
height: 170px;
border-radius: 4px;
background: #fff;
box-shadow: 0 10px 30px -12px rgb(0 0 0 / 0.5);
}
.shape {
position: absolute;
}
.card {
inset: 18px;
border-radius: 12px;
background: #1e3a8a;
}
.photo {
top: 34px;
left: 66px;
width: 120px;
height: 84px;
border-radius: 8px;
background:
radial-gradient(circle at 75% 30%, #fde68a 0 12px, transparent 13px),
linear-gradient(170deg, #67e8f9, #0e7490);
}
.badge {
top: 20px;
left: 160px;
display: grid;
place-items: center;
width: 52px;
height: 52px;
border-radius: 50%;
background: #facc15;
color: #1e3a8a;
font: 800 13px/1 system-ui, sans-serif;
}
.title {
top: 104px;
left: 30px;
color: #fff;
font: 800 24px/1 system-ui, sans-serif;
letter-spacing: -0.02em;
text-shadow: 0 2px 6px rgb(0 0 0 / 0.35);
}
</style>Uma lista e as linhas dela
Uma lista ordenável precisa de dois componentes: a lista e a linha dela.
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import LayerRow from './LayerRow.vue';
const layers = defineModel<Layer[]>({ required: true });
const list = useTemplateRef<HTMLElement>('list');
const { items } = makeSortableList(list, { items: layers });
</script>
<template>
<ul ref="list">
<LayerRow v-for="layer in items" :key="layer.id" :layer="layer" />
</ul>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableItem } from '@vue-dnd-kit/core';
const props = defineProps<{ layer: Layer }>();
const el = useTemplateRef<HTMLElement>('el');
makeSortableItem(el, { item: () => props.layer });
</script>
<template>
<li ref="el">{{ layer.name }}</li>
</template>
A linha se registra na lista em que é renderizada. Um componente pode criar várias listas (duas colunas, uma lista dentro de outra): cada linha entra naquela cujo elemento a contém, a mais interna no caso de listas dentro de listas.
O rascunho
makeSortableList retorna items, um rascunho do seu array. Enquanto você arrasta, o
rascunho se reordena ao vivo: a linha arrastada ocupa a nova posição, e as outras saem do
caminho. O seu array não muda até o drop. Aí a biblioteca grava a nova ordem nele.
O painel de camadas acima mostra os dois: o painel renderiza items e se move enquanto você
arrasta; a prancheta lê layers, o próprio array, e só reempilha quando você solta.
- Renderize
items, não o seu array. Renderizando o seu array, nada se move até o drop. - A gravação acontece depois do
onDropda lista. NoonDrop, o seu array ainda tem a ordem antiga ee.result, a nova. layersé um ref comum (aqui umdefineModel, e a gravação emiteupdate:modelValue), então o resto do seu app vê a mudança como qualquer outra.
makeSortableList(list, {
items: layers,
onDrop: (e) => {
console.log(e.from.index, '→', e.to.index, e.result.get(e.to.list));
},
});
computed em vez de um
ref. A biblioteca não grava em uma fonte somente leitura: chame e.preventDefault() e aplique
e.result você mesmo. Veja listas controladas.Keys
As linhas são identificadas pelos dados, nunca pelo índice ou por um id automático. Por padrão,
a key é item.id, ou o próprio item para strings e números.
// LayersPanel.vue: your items have another key
makeSortableList(list, { items: layers, getKey: (layer) => layer.uuid });
// LayerRow.vue: `item` is keyed with the list's getKey…
makeSortableItem(el, { item: () => props.layer });
// …or pass the key yourself
makeSortableItem(el, { id: () => props.layer.uuid });
- Use a mesma key no
v-for::key="layer.uuid". Com:key="index"as linhas erradas são animadas e o estado delas pula de uma linha para outra. - As keys precisam ser únicas entre todas as listas que trocam itens.
- Passe
itemeidcomo getters, para que uma linha continue sincronizada quando as props dela mudam.
Handles
Por padrão, a linha inteira inicia um arraste, exceto os botões, inputs e outros controles dentro
dela: o olho e o cadeado de uma camada continuam sendo botões comuns. Passe handle para
arrastar por uma parte só:
<script setup lang="ts">
makeSortableItem(el, { item: () => props.layer, handle: '.grip' });
</script>
<template>
<li ref="el">
<button type="button" class="grip" :aria-label="`Move ${layer.name}`">⠿</button>
{{ layer.name }}
</li>
</template>
handleé um seletor dentro do item, ou um ref de elemento.- Um arraste com o mouse começa pelo handle na hora, sem os 4 px de sempre. Uma pressão que
não se move continua sendo um clique: o handle pode abrir um menu no
click. - O handle recebe os atributos do arraste (
role,tabindex,aria-*), e os arrastes por teclado começam por ele.
Faça do handle um <button type="button"> com um aria-label: ele já é focável, e os leitores de
tela anunciam o rótulo. Um <span> simples com um ícone também funciona (a biblioteca dá a ele
tabindex="0" e role="button"), mas não tem um nome significativo.
touch-action: none e ao item
activation: { touch: { distance: 4 } }.Estratégias
strategy diz como a lista está organizada, para que as linhas abram espaço do jeito certo:
strategy | Para |
|---|---|
'vertical' (padrão) | Uma coluna de linhas. |
'horizontal' | Uma única fileira: abas, chips, uma linha do tempo. |
'grid' | Grids CSS e fileiras flex que quebram linha, também com itens de tamanhos diferentes. |
Uma fileira de abas é uma lista horizontal:
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Browser from './Browser.vue';
</script>
<template>
<DnDProvider>
<Browser />
</DnDProvider>
</template>const { items } = makeSortableList(strip, {
items: tabs,
strategy: 'horizontal',
axis: 'x', // the tabs stay on their row
animateChanges: true, // the buttons open and close tabs: slide the others then too
});
O botão de fechar da aba continua funcionando dentro da aba arrastável, e um clique que termina
um arraste não seleciona uma aba. As linhas deslizam sozinhas enquanto você arrasta; animateChanges
faz o mesmo para mudanças que nenhum arraste fez, aqui os botões (veja
animações).
Uma grade ordena em duas dimensões, com blocos de qualquer tamanho:
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import PhotoGrid from './PhotoGrid.vue';
</script>
<template>
<DnDProvider>
<PhotoGrid />
</DnDProvider>
</template>const { items } = makeSortableList(grid, { items: photos, strategy: 'grid' });
.grid { display: grid; grid-template-columns: repeat(4, 76px); grid-auto-rows: 76px; grid-auto-flow: dense; }
.wide { grid-column: span 2; }
.tall { grid-row: span 2; }
grid-auto-flow: dense mantém a grade compacta quando um bloco grande se move. Sem isso, a grade
deixa buracos onde um bloco não cabe mais, e ganha uma linha.
Travar o eixo
makeSortableList(list, { items: layers, axis: 'y' });
axis: 'y' mantém o arraste no eixo vertical, 'x' no horizontal: as camadas e as abas acima
usam isso. O axis do próprio item vence o da lista. Para manter o arraste também dentro da
lista, adicione o modificador restrictToParent. Mais em restrições.
Estilizar a posição
A linha arrastada fica na lista como a posição em que vai pousar, e recebe
data-dnd-placeholder. O estilo padrão a esmaece para 40%, com especificidade zero, então
qualquer regra sua vence:
.layer[data-dnd-placeholder] {
opacity: 1;
box-shadow: inset 0 0 0 1.5px #42b883;
}
.layer[data-dnd-placeholder] > * {
visibility: hidden; /* an empty slot of the row's exact size */
}
makeSortableItem retorna o mesmo estado como refs, e mais alguns:
| Retorna | O que é |
|---|---|
isPlaceholder | Esta linha é a posição em que um item arrastado vai pousar. |
isDragging | Esta linha está sendo arrastada (todas as linhas arrastadas, em um multi-drag). |
index | O índice dela no rascunho: muda enquanto você arrasta. |
selected | Gravável, ligado à seleção do provider. |
indicator | No modo indicador, onde a linha de inserção fica. |
A lista retorna isOver e isAccepting para o seu próprio destaque, e activeKey e
activeIndex, o item arrastado e o lugar dele no rascunho desta lista. Todos os estados, com
demos ao vivo, estão em estilização.
Desligar coisas
| Opção | O que ela impede |
|---|---|
disabled em um item | Este item não pode ser arrastado. Os outros ainda passam por ele: tranque uma camada acima, depois arraste outra por cima dela. |
disabled na lista | Nenhum arraste sai dela, nenhum drop entra nela. |
sort: false na lista | Os itens dela não podem ser reordenados dentro dela, mas ainda podem sair: uma paleta, veja listas que compartilham itens. |
Todas recebem um getter ou um ref, e valem para o próximo arraste:
makeSortableItem(el, { item: () => props.layer, handle: '.grip', disabled: () => props.layer.locked });
Uma linha que precisa ficar em primeiro ou em último, não importa o que as outras façam, não é um
item: renderize-a fora da lista. A camada Background do painel fica abaixo do <ul>.
Mostrar parte dos itens
Uma caixa de busca, uma tag: filter mostra os itens para os quais retorna true, e um arraste
ordena entre eles. Os outros ficam no seu array, nos seus lugares:
Carregando a demo…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Backlog from './Backlog.vue';
import type { Task } from './TaskRow.vue';
// In priority order: the array is the backlog.
const tasks = ref<Task[]>([
{ id: 1, title: 'Login fails on Safari', tag: 'bug' },
{ id: 2, title: 'Dark mode for settings', tag: 'feature' },
{ id: 3, title: 'Document the export API', tag: 'docs' },
{ id: 4, title: 'Crash when the list is empty', tag: 'bug' },
{ id: 5, title: 'Keyboard shortcuts', tag: 'feature' },
{ id: 6, title: 'Typo in the pricing page', tag: 'docs' },
{ id: 7, title: 'Slow search with 10k rows', tag: 'bug' },
{ id: 8, title: 'Share a board by link', tag: 'feature' },
]);
</script>
<template>
<DnDProvider>
<Backlog v-model="tasks" />
</DnDProvider>
</template>const query = ref('');
const tag = ref<Tag | null>(null);
const { items } = makeSortableList(list, {
items: tasks,
filter: (task) => (!tag.value || task.tag === tag.value) && task.title.includes(query.value),
});
itemscontém os itens que passam no filtro: renderize-o como sempre. O filtro é reativo: digitar na caixa de busca atualiza a lista na hora.- O drop escreve o array inteiro. Cada item oculto fica logo depois do item visível que ele seguia; quando esse é o que se moveu, ele fica depois do mais próximo antes dele que não se moveu.
e.to.indexe oactiveIndexda lista contam os itens visíveis.e.resultcontém o array inteiro, itens ocultos incluídos.
computed que filtra o seu array e é passado como items também funciona, mas é somente
leitura: você aplicaria e.result por conta própria e calcularia para onde vão os itens ocultos.
filter faz as duas coisas.Uma linha de inserção
Com placeholder: 'indicator', a lista fica parada enquanto você arrasta, e uma linha mostra
para onde vai o drop. Você desenha a linha:
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import IssueTable from './IssueTable.vue';
</script>
<template>
<DnDProvider>
<IssueTable />
</DnDProvider>
</template>const { items, indicator } = makeSortableList(body, { items: issues, placeholder: 'indicator' });
<script setup lang="ts">
const { indicator } = makeSortableItem(el, { item: () => props.issue });
</script>
<template>
<div ref="el" class="row" :class="indicator && `drop-${indicator.side}`">…</div>
</template>
<style scoped>
.row.drop-before::before { content: ''; position: absolute; top: -1.5px; left: 6px; right: 6px; height: 2px; background: #42b883; }
.row.drop-after::before { content: ''; position: absolute; bottom: -1.5px; left: 6px; right: 6px; height: 2px; background: #42b883; }
</style>
Campo de indicator | O que contém |
|---|---|
list | A lista em que o drop entra. |
index | O índice de inserção na lista como ela está renderizada. |
key | O item ao qual a linha está presa; null em uma lista vazia (nesse caso, desenhe-a na lista). |
side | 'before' ou 'after' desse item; 'swap' em uma lista que troca de lugar. Árvores acrescentam 'inside'. |
depth, parentKey | Só em árvores: onde o drop pousa. Veja árvores. |
- O
indicatorda lista é definido enquanto a linha está naquela lista (a tabela o mostra), e oindicatorde um item enquanto a linha está presa àquele item. - A linha arrastada fica onde estava, marcada como placeholder: esmaeça-a.
- O teclado também move a linha de inserção.
- Um drop fora de qualquer alvo cancela neste modo (
dropOutside: 'cancel'), já que a lista nunca mostrou outra ordem. - O overlay cobre parte da linha embaixo dele. Deixe-o um pouco transparente, e a linha de
inserção aparece através dele:
[data-dnd-overlay] .row { opacity: 0.85 }.
Trocar de lugar em vez de deslocar
Com behavior: 'swap', o item arrastado troca de lugar com o que está embaixo dele, e os outros
ficam onde estão:
Carregando a demo…
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Rotation from './Rotation.vue';
const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
const columns = reactive([
{
id: 'primary',
title: 'Primary',
people: [
{ id: 'ava', name: 'Ava', hue: 160 },
{ id: 'ben', name: 'Ben', hue: 20 },
{ id: 'cleo', name: 'Cleo', hue: 280 },
{ id: 'dev', name: 'Dev', hue: 210 },
{ id: 'eli', name: 'Eli', hue: 45 },
],
},
{
id: 'backup',
title: 'Backup',
people: [
{ id: 'fay', name: 'Fay', hue: 330 },
{ id: 'gus', name: 'Gus', hue: 100 },
{ id: 'hana', name: 'Hana', hue: 190 },
{ id: 'ivo', name: 'Ivo', hue: 0 },
{ id: 'jun', name: 'Jun', hue: 250 },
],
},
]);
/** Live: the swap shows while you drag. On drop: nothing moves, the partner is marked. */
const placeholder = ref<'item' | 'indicator'>('item');
const last = ref('');
</script>
<template>
<DnDProvider>
<div class="schedule">
<div class="modes" role="group" aria-label="Swap preview">
<button type="button" :aria-pressed="placeholder === 'item'" @click="placeholder = 'item'">Live</button>
<button type="button" :aria-pressed="placeholder === 'indicator'" @click="placeholder = 'indicator'">On drop</button>
</div>
<div class="week">
<ol class="days">
<li v-for="day in DAYS" :key="day">{{ day }}</li>
</ol>
<Rotation v-for="column in columns" :key="column.id" :column="column" :placeholder="placeholder" @swap="last = $event" />
</div>
<p class="last">{{ last ? `Swapped ${last}` : 'Drag a person onto another' }}</p>
</div>
</DnDProvider>
</template>
<style scoped>
.schedule {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.modes {
display: flex;
padding: 3px;
border-radius: 10px;
background: color-mix(in srgb, currentColor 7%, transparent);
}
.modes button {
padding: 6px 12px;
border: 0;
border-radius: 7px;
background: none;
color: inherit;
font: inherit;
font-size: 12px;
font-weight: 500;
opacity: 0.6;
cursor: pointer;
}
.modes button[aria-pressed='true'] {
background: Canvas;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
opacity: 1;
}
.week {
display: flex;
gap: 10px;
width: 400px;
max-width: 100%;
}
.days {
display: flex;
flex-direction: column;
gap: 6px;
margin: 21px 0 0;
padding: 0;
list-style: none;
}
.days li {
height: 36px;
font: 11px/36px ui-monospace, Menlo, Consolas, monospace;
opacity: 0.55;
}
.last {
margin: 0;
font: 12px/1 ui-monospace, Menlo, Consolas, monospace;
opacity: 0.6;
}
</style>const { items } = makeSortableList(list, {
id: props.column.id,
items: toRef(props.column, 'people'),
group: 'rotation',
behavior: 'swap',
placeholder: () => props.placeholder, // 'item' (live) or 'indicator' (on drop)
onDrop: (e) => {
if (e.partner) emit('swap', `${e.items[0]?.name} ⇄ ${e.partner.item.name}`);
},
});
- Ao vivo (
placeholder: 'item', padrão): a troca aparece enquanto você arrasta. Voltar, ou sair da lista, a desfaz. - No drop (
placeholder: 'indicator'): nada se move até o drop. O indicador marca o item com o qual o drop troca,side: 'swap': contorne esse item em vez de desenhar uma linha. As setas movem a marca de item em item; de volta ao item arrastado, não há marca.
<li ref="el" :class="{ target: indicator?.side === 'swap' }">…</li>
O evento de drop tem mode: 'swap' e partner, o item com o qual houve a troca: a key dele, o
item, e o from e o to dele. Duas listas de um mesmo grupo trocam entre si quando ambas têm
behavior: 'swap', como as duas colunas. O item sob o ponteiro então vai para a outra lista,
então ele precisa ter permissão para sair (group.pull) e para entrar (group.put): veja
listas que compartilham itens. Um multi-drag ordena em vez de trocar.
Teclado
Foque uma linha, ou o handle dela, e pressione Space ou Enter para pegá-la. As setas ao longo da lista a movem uma posição; em grades, para cima e para baixo vão para a fileira seguinte, e as setas no sentido transversal à lista a levam para a próxima lista do grupo. Depois do fim de uma lista, a seta segue para a próxima lista naquela direção (listas empilhadas uma acima da outra) ou, se não houver, para um alvo de drop ali, como uma lixeira. Pressione Space ou Enter de novo para soltar, Escape para devolvê-la. Todas as demos desta página funcionam assim. Os textos para leitores de tela estão em acessibilidade.
Animações
As linhas deslizam para os novos lugares enquanto você arrasta, e a linha solta pousa na sua
posição. A própria lista cresce ou encolhe até o novo tamanho quando linhas entram e saem
(animateSize). Isso vem ligado por padrão e não precisa de código. Não envolva as linhas em
<TransitionGroup>: os movimentos seriam animados duas vezes. Também não coloque um transition
CSS no transform das linhas. Para mudanças que nenhum arraste fez (adicionar, remover,
embaralhar), ligue a opção animateChanges da lista, como as abas fazem. Veja
animações.
A lista completa de opções está na API do makeSortableList e do makeSortableItem.