NbReorderList is a vertical list whose rows can be dragged into a new order, with a drop indicator and a keyboard path that does the same job. The model editor's field list, a block zone and moving media between folders all want this.
<script setup>
const fields = ref([
{ id: 'title', label: 'Title' },
{ id: 'slug', label: 'Slug' },
{ id: 'body', label: 'Body' },
])
</script>
<template>
<NbReorderList
v-model="fields"
item-key="id"
label="Fields"
v-slot="{ item }"
>
{{ item.label }}
</NbReorderList>
</template>Keyboard
The keyboard path is the half that gets skipped when a reorder list is written by hand, and it is most of why this is a component rather than a recipe.
| Key | Does |
|---|---|
| Tab | Moves to the next row |
| Space / Enter | Picks the row up, or drops it |
| ↑ / ↓ | Moves the held row, or moves focus when nothing is held |
| Esc | Cancels the pick-up |
Arrow keys browse until a row is picked up, and only then move it. Reordering directly on arrow keys looks simpler but leaves a screen reader user unable to read the list without rearranging it.
Every move is announced through a live region, because a keyboard user does not see the row travel.
Handles
By default a row drags from anywhere, which suits a row that is just a label. Set handle when rows contain their own controls, so dragging does not fight the buttons inside them.
Controlled, for orders that can fail
v-model writes the new order back immediately, which is right when the order lives in the client. When it lives on a server and the move can be rejected, bind :model-value instead and apply the change yourself once it is saved.
<NbReorderList
:model-value="fields"
item-key="id"
@reorder="onReorder"
v-slot="{ item }"
>
{{ item.label }}
</NbReorderList>
<script setup>
async function onReorder({ from, to }) {
const next = [...fields.value]
next.splice(to, 0, ...next.splice(from, 1))
const saved = await api.saveOrder(next)
if (saved) fields.value = next // otherwise the list stays as it was
}
</script>Keys
itemKey needs to identify a row in a way that survives reordering, so it cannot be the index: the index is the thing that changes. Pass a property name, or a function for a compound key.
Notes on the interaction
- The dragged row stays put and dims rather than following the pointer. A row moving under the cursor competes with the drop indicator, and the indicator is the part that says what will actually happen.
- Pointer events, not HTML5 drag and drop. Drag events cannot be styled consistently, do not fire on touch without a polyfill, and give no control over where the drop indicator sits.
- A held row is dropped if focus leaves it. A row still held after the user has tabbed away is a trap with nothing to cancel it.
- The gap between rows opens while dragging, from 4px to 12px, so the drop indicator is a bar sitting in a space rather than a hairline pinched between two borders. The indicator is centred in that gap, which is why the gap is a token the component owns rather than a margin on the rows.
Styling
Rows are squared, like the rest of the library. The gap is --nb-reorder-list-gap; set it on the list to change the rhythm, and the drop indicator follows automatically because it is positioned against that same value.