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 single 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.
Usage
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>
<!-- 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>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
- On drop:
reorderevent emits{ from, to }with the source and target values - Your code handles the actual array reorder
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 |
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" - 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.
Usage Guide
Setup
ts
import { VibeList, VibeListItem, VibeListGroup, VibeListDivider, VibeListEmpty } from "@vibe-labs/design-vue-lists";
import "@vibe-labs/design-components-lists";VibeList
Settings navigation list
vue
<script setup lang="ts">
import { VibeList, VibeListItem, VibeListGroup, VibeListDivider } from "@vibe-labs/design-vue-lists";
import { useRouter } from "vue-router";
const router = useRouter();
</script>
<template>
<VibeList variant="outlined" hoverable interactive divided>
<VibeListGroup label="Account">
<VibeListItem label="Profile" description="Edit your public profile" @click="router.push('/settings/profile')" />
<VibeListItem label="Notifications" description="Email, push, and in-app alerts" @click="router.push('/settings/notifications')" />
</VibeListGroup>
<VibeListDivider />
<VibeListGroup label="Security">
<VibeListItem label="Password" description="Change your password" @click="router.push('/settings/password')" />
<VibeListItem label="Two-factor authentication" @click="router.push('/settings/2fa')" />
</VibeListGroup>
</VibeList>
</template>Single-select list
vue
<script setup lang="ts">
import { VibeList, VibeListItem } from "@vibe-labs/design-vue-lists";
import { ref } from "vue";
const selectedGenre = ref("electronic");
const genres = [
{ id: "electronic", label: "Electronic" },
{ id: "jazz", label: "Jazz" },
{ id: "hip-hop", label: "Hip Hop" },
{ id: "classical", label: "Classical" },
];
</script>
<template>
<VibeList
interactive
hoverable
selection-mode="single"
v-model:selected="selectedGenre"
>
<VibeListItem
v-for="genre in genres"
:key="genre.id"
:value="genre.id"
:label="genre.label"
/>
</VibeList>
</template>Reorderable playlist
vue
<script setup lang="ts">
import { VibeList, VibeListItem } from "@vibe-labs/design-vue-lists";
import { VibeBadge } from "@vibe-labs/design-vue-badges";
import { ref } from "vue";
import type { ListReorderEvent } from "@vibe-labs/design-vue-lists";
const tracks = ref([
{ id: "t1", title: "Acid Rain", duration: "6:42" },
{ id: "t2", title: "Dub Horizon", duration: "8:15" },
{ id: "t3", title: "System Error", duration: "5:30" },
]);
function onReorder({ from, to }: ListReorderEvent) {
const fromIdx = tracks.value.findIndex((t) => t.id === from);
const toIdx = tracks.value.findIndex((t) => t.id === to);
const [moved] = tracks.value.splice(fromIdx, 1);
tracks.value.splice(toIdx, 0, moved);
}
</script>
<template>
<VibeList reorderable divided @reorder="onReorder">
<VibeListItem
v-for="track in tracks"
:key="track.id"
:value="track.id"
:label="track.title"
>
<template #trail>
<span>{{ track.duration }}</span>
</template>
</VibeListItem>
</VibeList>
</template>Common Patterns
List with empty state
vue
<script setup lang="ts">
import { VibeList, VibeListItem, VibeListEmpty } from "@vibe-labs/design-vue-lists";
import { computed } from "vue";
const props = defineProps<{ items: Array<{ id: string; label: string }> }>();
</script>
<template>
<VibeList>
<VibeListEmpty v-if="props.items.length === 0" message="No items found" />
<VibeListItem
v-for="item in props.items"
:key="item.id"
:value="item.id"
:label="item.label"
/>
</VibeList>
</template>Multi-select with trail badges
vue
<script setup lang="ts">
import { VibeList, VibeListItem } from "@vibe-labs/design-vue-lists";
import { VibeBadge } from "@vibe-labs/design-vue-badges";
import { ref } from "vue";
const selected = ref<string[]>([]);
const artists = [
{ id: "a1", name: "Burial", genre: "UK Garage" },
{ id: "a2", name: "Four Tet", genre: "Electronica" },
{ id: "a3", name: "Aphex Twin", genre: "Ambient" },
];
</script>
<template>
<VibeList interactive hoverable selection-mode="multi" v-model:selected="selected">
<VibeListItem
v-for="artist in artists"
:key="artist.id"
:value="artist.id"
:label="artist.name"
>
<template #trail>
<VibeBadge :label="artist.genre" auto-color size="sm" />
</template>
</VibeListItem>
</VibeList>
<p>{{ selected.length }} selected</p>
</template>