Appearance
@vibe-labs/design-vue-lists
Vue 3 list components for the Vibe Design System. Compound component architecture with selection, drag-and-drop reorder, keyboard navigation, and full ARIA support.
Installation
ts
import { VibeList, VibeListItem, VibeListGroup, VibeListDivider, VibeListEmpty } from "@vibe-labs/design-vue-lists";Requires the CSS layer from @vibe-labs/design-components-lists.
Components
VibeList
Root container that provides shared context to all list children via inject.
Usage
vue
<!-- Basic -->
<VibeList>
<VibeListItem label="Notifications" description="Manage your notification preferences" />
<VibeListItem label="Security" description="Password, 2FA, and sessions" />
<VibeListItem label="Billing" description="Plans, invoices, and payment methods" />
</VibeList>
<!-- Interactive with selection -->
<VibeList interactive hoverable selection-mode="single" v-model:selected="selectedItem">
<VibeListItem value="a" label="Option A" />
<VibeListItem value="b" label="Option B" />
<VibeListItem value="c" label="Option C" />
</VibeList>
<!-- Multi-select -->
<VibeList interactive hoverable selection-mode="multi" v-model:selected="selectedItems">
<VibeListItem value="one" label="Item 1" />
<VibeListItem value="two" label="Item 2" />
<VibeListItem value="three" label="Item 3" />
</VibeList>
<!-- Reorderable -->
<VibeList reorderable @reorder="onReorder">
<VibeListItem v-for="item in items" :key="item.id" :value="item.id" :label="item.name" />
</VibeList>
<!-- Variants -->
<VibeList variant="elevated">...</VibeList>
<VibeList variant="outlined">...</VibeList>
<VibeList variant="ghost" flush>...</VibeList>
<!-- Density -->
<VibeList size="sm">...</VibeList>
<VibeList size="lg" divided>...</VibeList>Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | ListVariant | "default" | Visual style |
size | ListSize | "md" | Item density |
divided | boolean | false | Auto borders between items |
striped | boolean | false | Alternating backgrounds |
hoverable | boolean | false | Hover highlight on items |
interactive | boolean | false | Pointer cursor + active press |
flush | boolean | false | Strip all container chrome |
reorderable | boolean | false | Enable drag-and-drop reorder |
selectionMode | ListSelectionMode | "none" | Selection behaviour |
selected | string | string[] | — | Selected value(s) (v-model:selected) |
ariaLabel | string | — | Accessible label |
tag | string | "div" | Container HTML element |
Events
| Event | Payload | Description |
|---|---|---|
update:selected | string | string[] | Selection changed |
item-click | string, MouseEvent | Item clicked (with value) |
reorder | ListReorderEvent | Item dropped onto new target |
VibeListItem
Individual list row with structured slots.
vue
<!-- Simple -->
<VibeListItem label="Settings" />
<!-- With description -->
<VibeListItem label="Notifications" description="Email, push, and in-app alerts" />
<!-- Full slots -->
<VibeListItem value="doc-1">
<template #icon><FileIcon /></template>
Report Q3.pdf
<template #description>2.4 MB · Modified today</template>
<template #trail>
<VibeBadge label="New" variant="accent-subtle" />
</template>
</VibeListItem>
<!-- Custom drag handle -->
<VibeListItem value="item-1">
<template #handle><GripIcon /></template>
Draggable item
</VibeListItem>
<!-- Disabled -->
<VibeListItem label="Locked" disabled />Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Identifier (required for selection / reorder) |
label | string | — | Label text (alternative to default slot) |
description | string | — | Description text (alternative to slot) |
selected | boolean | — | Override selected state |
disabled | boolean | false | Disabled state |
Slots
| Slot | Description |
|---|---|
default | Label content (inside .list-item-label) |
icon | Leading icon |
description | Secondary text below label |
trail | Trailing content (badges, actions, metadata) |
handle | Custom drag handle (default: 6-dot grip SVG) |
Events
| Event | Payload | Description |
|---|---|---|
click | MouseEvent | Item clicked |
VibeListGroup
Section wrapper with label heading.
vue
<VibeList>
<VibeListGroup label="General">
<VibeListItem label="Profile" />
<VibeListItem label="Language" />
</VibeListGroup>
<VibeListGroup label="Danger Zone">
<VibeListItem label="Delete account" />
</VibeListGroup>
</VibeList>Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Group heading (alternative to label slot) |
Slots
| Slot | Description |
|---|---|
default | Group items |
label | Custom heading |
VibeListDivider
Visual separator between items.
vue
<VibeList>
<VibeListItem label="Above" />
<VibeListDivider />
<VibeListItem label="Below" />
</VibeList>VibeListEmpty
Empty state displayed when the list has no items.
vue
<VibeList>
<VibeListEmpty v-if="items.length === 0" message="No results found" />
<VibeListItem v-for="item in items" :key="item.id" :label="item.name" />
</VibeList>
<!-- Custom content -->
<VibeListEmpty>
<EmptyIllustration />
<p>Nothing here yet</p>
</VibeListEmpty>Props
| Prop | Type | Default | Description |
|---|---|---|---|
message | string | — | Text (alternative to default slot) |
Selection Modes
| Mode | Behaviour |
|---|---|
"none" (default) | No selection. Items can still be interactive/clickable. |
"single" | One item at a time. Re-selecting the same item deselects. |
"multi" | Toggle items independently. Multiple can be selected. |
When selection is enabled, the container uses role="listbox" and items use role="option" with aria-selected.
Drag and Drop Reorder
When reorderable is set:
- A drag handle appears on each item that has a
valueprop - Items become draggable via HTML5 Drag and Drop
- During drag: source item gets
data-dragging, target getsdata-drag-over - On drop:
reorderevent emits{ from, to }with the source and target values - Your code handles the actual array reorder — the list just reports the intent
vue
<script setup>
function onReorder({ from, to }) {
const fromIdx = items.value.findIndex((i) => i.id === from);
const toIdx = items.value.findIndex((i) => i.id === to);
const [moved] = items.value.splice(fromIdx, 1);
items.value.splice(toIdx, 0, moved);
}
</script>
<VibeList reorderable @reorder="onReorder">
<VibeListItem v-for="item in items" :key="item.id" :value="item.id" :label="item.name" />
</VibeList>Keyboard Navigation
| Key | Action |
|---|---|
| ArrowDown | Focus next item (wraps) |
| ArrowUp | Focus previous item (wraps) |
| Home | Focus first item |
| End | Focus last item |
| Space | Toggle selection (when selection enabled) |
| Enter | Activate / toggle selection |
Keyboard navigation is active when interactive is set or selectionMode is not "none".
ARIA
- Default list:
role="list"on container - Selectable list:
role="listbox"on container,role="option"on items,aria-selectedtracks state - Multi-select:
aria-multiselectable="true"on container - Disabled items:
aria-disabled="true" - Reorderable items:
draggable="true"on items with value - Groups:
role="group"on group wrapper - Dividers:
role="separator" - Empty state:
role="status"
Dependencies
| Package | Purpose |
|---|---|
@vibe-labs/design-components-lists | CSS tokens + generated styles |
Build
bash
npm run buildBuilt with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.