List
Composition-based list primitives under the frappe-ui/list subpath. Every list surface is one column grid: a feed row is just the default column template, a table is an explicit one. The family owns geometry — columns, dividers, hover surfaces, selection and sort chrome — and nothing readable: cell contents (typography, avatars, badges, unread emphasis) are entirely app-authored.
Not to be confused with the config-driven ListView in the main entry — that stays untouched; import from frappe-ui/list for this family.
Feed mode
The default template (auto, minmax(0,1fr), auto) fits leading media, content, and a right-aligned trailing cell. Rows with to render as RouterLinks, rows with a click listener as buttons. selectable reveals the animated checkbox column and switches row click from navigate to toggle; selected values surface through v-model:selection. When a ListHeader is present, a select-all checkbox appears in it automatically — checked when every row is selected, mixed when only some are, and toggling all rows on or off. It reasons over the full ListRows items, so it covers virtualized rows too. ListRows resolves each row's identity once and exposes it as the scoped value prop. The identity defaults to the item's name/id; pass row-key (a field name or (item, index) => key) when the row should use a different field.
Dividers default to inset: they start at the content column (the text edge) by construction, never render above the first row, and hide around a hovered row so the rounded hover surface floats free.
<script setup lang="ts">
import { ref } from 'vue'
import { Avatar, Badge, Button } from 'frappe-ui'
import { List, ListRow, ListCell } from 'frappe-ui/list'
const discussions = [
{
name: '1',
title: 'Weekly sync notes',
author: 'Rosa Diaz',
comment: 'Sounds good, let us ship it on Monday',
time: '2 h',
comments: 4,
unread: true,
},
{
name: '2',
title: 'Redesigning the onboarding flow',
author: 'Jake Peralta',
comment: 'I added the new mockups to the page',
time: '5 h',
comments: 12,
unread: false,
},
{
name: '3',
title: 'Q3 hiring plan',
author: 'Amy Santiago',
comment: 'Two backend roles and one designer',
time: '1 d',
comments: 7,
unread: true,
},
{
name: '4',
title: 'Incident review: search downtime',
author: 'Terry Jeffords',
comment: 'Root cause was the index rebuild',
time: '2 d',
comments: 9,
unread: false,
},
{
name: '5',
title: 'Docs sprint retrospective',
author: 'Raymond Holt',
comment: 'Velocity was acceptable.',
time: '4 d',
comments: 3,
unread: false,
},
]
const selectable = ref(false)
const selection = ref<string[]>([])
function toggleSelectMode() {
selectable.value = !selectable.value
selection.value = []
}
</script>
<template>
<div class="w-full">
<div class="mb-2 flex h-7 items-center justify-end gap-3">
<span v-if="selection.length" class="text-sm text-ink-gray-5">
{{ selection.length }} selected
</span>
<Button @click="toggleSelectMode">
{{ selectable ? 'Done' : 'Select' }}
</Button>
</div>
<List :selectable="selectable" v-model:selection="selection" :row-height="60">
<ListRow
v-for="discussion in discussions"
:key="discussion.name"
:value="discussion.name"
@click="() => {}"
>
<ListCell>
<Avatar :label="discussion.author" size="2xl" />
</ListCell>
<ListCell>
<div class="min-w-0">
<div
class="truncate text-base text-ink-gray-8"
:class="discussion.unread && 'font-semibold'"
>
{{ discussion.title }}
</div>
<div class="mt-1 truncate text-base text-ink-gray-5">
{{ discussion.author }}: {{ discussion.comment }}
</div>
</div>
</ListCell>
<ListCell class="justify-end">
<div class="flex flex-col items-end gap-1">
<span class="text-sm text-ink-gray-5">{{ discussion.time }}</span>
<Badge>{{ discussion.comments }}</Badge>
</div>
</ListCell>
</ListRow>
</List>
</div>
</template>Active row
A master–detail list (a mail inbox, a file browser) tracks one open row. Bind v-model:active to a row value and the List owns the rest: it highlights that row and hides the dividers hugging it — above and below — so its rounded surface floats free, like a hovered row but persistent. Clicking a row sets active; unlike selectable, activation is additive, so the row's own @click and to navigation still run. It's single-select and independent of the multi-select checkbox selection — and works in feed or column mode.
<List v-model:active="openId">
<ListRows :items="threads" v-slot="{ value }">
<ListRow :value="value">…</ListRow>
</ListRows>
</List>Column mode
Pass explicit columns (deterministic track sizes — auto tracks size independently per row) and a ListHeader. Header and rows share one --list-columns template, so they can never drift.
ListHeaderCell is a plain label with optional #prefix / #suffix adornments. Sortable columns use ListHeaderCellSort instead — a controlled sort button: you hand it the active direction (asc / desc / null) and update your own sort state in its click handler. Your code owns the state, toggle rules, direction glyphs (via the scoped #suffix="{ direction }" slot), and whether ordering happens client-side or through useList orderBy. The cell keeps only the behavioral chrome: a real button, aria-sort, the tooltip, and revealing an inactive column's suffix on hover. Both variants render the same data-slot="list-header-cell" geometry, so mixing them in one header is seamless.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Avatar, Button } from 'frappe-ui'
import {
List,
ListRow,
ListCell,
ListHeader,
ListHeaderCell,
ListHeaderCellSort,
ListRows,
} from 'frappe-ui/list'
const members = [
{ name: 'Rosa Diaz', email: '[email protected]', role: 'Admin', since: '2021-06' },
{ name: 'Jake Peralta', email: '[email protected]', role: 'Member', since: '2022-01' },
{ name: 'Amy Santiago', email: '[email protected]', role: 'Admin', since: '2020-11' },
{ name: 'Terry Jeffords', email: '[email protected]', role: 'Member', since: '2023-03' },
{ name: 'Raymond Holt', email: '[email protected]', role: 'Guest', since: '2024-08' },
]
// Sort state, toggle rules, comparator, and direction icons are all app
// code — the header cells only render the chrome for whatever `direction`
// you hand them.
type Field = 'name' | 'role' | 'since'
function sortIcon(direction: 'asc' | 'desc' | null) {
if (!direction) return 'lucide-arrow-up-down'
return direction === 'asc' ? 'lucide-arrow-up' : 'lucide-arrow-down'
}
const sortField = ref<Field>('name')
const sortDirection = ref<'asc' | 'desc'>('asc')
function toggleSort(field: Field, firstDirection: 'asc' | 'desc' = 'asc') {
if (sortField.value === field) {
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'
} else {
sortField.value = field
sortDirection.value = firstDirection
}
}
function directionFor(field: Field) {
return sortField.value === field ? sortDirection.value : null
}
const sortedMembers = computed(() => {
const factor = sortDirection.value === 'desc' ? -1 : 1
return [...members].sort(
(a, b) => factor * a[sortField.value].localeCompare(b[sortField.value]),
)
})
</script>
<template>
<List class="w-full" :columns="['minmax(0,1fr)', '7rem', '8rem', '3rem']" :row-height="56">
<ListHeader>
<ListHeaderCellSort :direction="directionFor('name')" @click="toggleSort('name')">
Member
<template #suffix="{ direction }">
<span class="block size-3.5" :class="sortIcon(direction)" />
</template>
</ListHeaderCellSort>
<ListHeaderCellSort :direction="directionFor('role')" @click="toggleSort('role')">
Role
<template #suffix="{ direction }">
<span class="block size-3.5" :class="sortIcon(direction)" />
</template>
</ListHeaderCellSort>
<ListHeaderCellSort
:direction="directionFor('since')"
class="justify-end"
@click="toggleSort('since', 'desc')"
>
Member since
<template #suffix="{ direction }">
<span class="block size-3.5" :class="sortIcon(direction)" />
</template>
</ListHeaderCellSort>
<ListHeaderCell />
</ListHeader>
<ListRows :items="sortedMembers" v-slot="{ item: member }">
<ListRow>
<ListCell>
<Avatar :label="member.name" size="xl" />
<div class="ml-3 min-w-0">
<div class="truncate text-base text-ink-gray-8">{{ member.name }}</div>
<div class="mt-0.5 truncate text-sm text-ink-gray-5">{{ member.email }}</div>
</div>
</ListCell>
<ListCell>
<span class="text-base text-ink-gray-7">{{ member.role }}</span>
</ListCell>
<ListCell class="justify-end">
<span class="text-base text-ink-gray-6">{{ member.since }}</span>
</ListCell>
<ListCell class="justify-end">
<Button variant="ghost" icon="lucide-trash-2" label="Remove member" />
</ListCell>
</ListRow>
</ListRows>
</List>
</template>Virtual rows
ListRows iterates items through its scoped slot; with virtual, only rows near the viewport mount. The scoped slot receives { item, index, value }, where value is the string row identity used by select-all and active-row state. The scroll container is the nearest scrollable ancestor — the list windows against an app-owned scroll area (a settings body, the page) and keeps its scrollbar. itemHeight defaults to the List's rowHeight. The underlying composable, useVirtualRows, is exported for exotic cases.
Styling hooks
--list-columns and --list-gap (default 0.5rem) are public hooks on the List — override them with plain classes, responsively if needed. People-style lists collapse to a feed on mobile with no dedicated API:
<List
:columns="['minmax(8rem,1fr)', '5.5rem', '5.5rem']"
class="max-sm:[--list-columns:auto_minmax(0,1fr)_auto]"
>with max-sm:hidden on the numeric cells and the ListHeader.
For --list-gap and --list-row-padding-x (the content inset shared by interactive rows — default 0.75rem — and the column header, so setting it on the List aligns both), the frappe-ui Tailwind preset ships spacing-scale utilities — list-gap-* and list-row-px-* — so the usual authoring form is max-sm:list-gap-3 sm:list-gap-4 rather than raw [--list-gap:0.75rem] properties. Both forms hit the same CSS vars.
Cells (and plain header cells) are flex containers with items-center — align content with justify utilities (class="justify-end" for numeric columns), responsively if needed. For sortable numeric headers, use <ListHeaderCellSort align="end"> so the sort glyph moves to the leading side and the label stays flush with the column edge.
Slots for CSS targeting: data-slot="list | list-header | list-header-cell | list-row | list-cell | list-row-checkbox | list-divider". State: data-state="selected" (checkbox selection), data-active (+ aria-current, the v-model:active row) and data-interactive on rows, data-sort on the active header cell.
Accessibility follows header presence: role="list" / "listitem" without a ListHeader, table / row / columnheader / cell (plus aria-sort) with one.
API Reference
List
Show types
import type { RouteLocationRaw } from 'vue-router'
export type ListDivider = 'inset' | 'full' | 'none'
export type ListSortDirection = 'asc' | 'desc'
export interface ListProps {
/**
* Grid track sizes, written to the `--list-columns` CSS var shared by the
* header and every row. Defaults to the feed template
* `['auto', 'minmax(0,1fr)', 'auto']` (leading media, content, trailing).
* Table-style lists must pass deterministic track sizes — `auto` tracks
* size independently per row.
*/
columns?: string[]
/**
* Divider treatment between rows: `inset` starts at the content column
* (the text edge), `full` spans all columns. Defaults to `inset` with the
* default feed template, `full` when `columns` is set.
*/
divider?: ListDivider
/**
* Reveals the animated checkbox column and switches row click from
* navigate to toggle. Selected values surface via `v-model:selection`.
*/
selectable?: boolean
// Two more models live on List but aren't plain props (so they're not in
// this interface): `v-model:selection` (string[], the checkbox set) and
// `v-model:active` (string, the single open/highlighted row — the List
// styles it and hides the dividers hugging it). See List.vue.
/**
* Fixed row height in px (sets `--list-row-height`). Required for
* virtualization; without it rows size to their content. Responsive
* heights are non-virtual — set them with classes on the rows instead.
*/
rowHeight?: number
}
export interface ListRowProps {
/**
* Renders the row as a RouterLink. Without `to`, a row with a click
* listener renders as a button; otherwise a plain div.
*/
to?: RouteLocationRaw
/**
* Row key — the `selection` key when `selectable` and the `v-model:active`
* key. Required whenever the list uses either.
*/
value?: string
/** Fired when the row is activated, unless selection mode claims the click. */
onClick?: (event: MouseEvent) => void
}
export interface ListHeaderCellSortProps {
/**
* Active sort direction for this column, `null`/omitted when inactive.
* The cell is controlled — sort state and toggle rules are app-owned:
* update whatever drives your ordering in the `click` handler.
*/
direction?: ListSortDirection | null
/**
* Horizontal alignment of the header content. `'end'` right-aligns the cell
* (for numeric/right-aligned columns) *and* moves the sort glyph to the
* leading side, so the label stays flush with the column's right edge and
* lines up with the values below. Defaults to `'start'`.
*/
align?: 'start' | 'end'
}
export interface ListVirtualOptions {
/** Row height in px. Defaults to the List's `rowHeight`. */
itemHeight?: number
/** Rows rendered beyond the visible window on each side. */
overscan?: number
}Grid track sizes, written to the `--list-columns` CSS var shared by the header and every row. Defaults to the feed template `['auto', 'minmax(0,1fr)', 'auto']` (leading media, content, trailing). Table-style lists must pass deterministic track sizes — `auto` tracks size independently per row.
Divider treatment between rows: `inset` starts at the content column (the text edge), `full` spans all columns. Defaults to `inset` with the default feed template, `full` when `columns` is set.
Reveals the animated checkbox column and switches row click from navigate to toggle. Selected values surface via `v-model:selection`.
Fixed row height in px (sets `--list-row-height`). Required for virtualization; without it rows size to their content. Responsive heights are non-virtual — set them with classes on the rows instead.
| Slot | Payload |
|---|---|
default | {} |
| Event | Payload |
|---|---|
update:selection | [value: string[]] Fired when the selection changes. |
update:active | [value: string | undefined] Fired when the active changes. |
Fired when the selection changes.
Fired when the active changes.
ListRow
Show types
import type { RouteLocationRaw } from 'vue-router'
export type ListDivider = 'inset' | 'full' | 'none'
export type ListSortDirection = 'asc' | 'desc'
export interface ListProps {
/**
* Grid track sizes, written to the `--list-columns` CSS var shared by the
* header and every row. Defaults to the feed template
* `['auto', 'minmax(0,1fr)', 'auto']` (leading media, content, trailing).
* Table-style lists must pass deterministic track sizes — `auto` tracks
* size independently per row.
*/
columns?: string[]
/**
* Divider treatment between rows: `inset` starts at the content column
* (the text edge), `full` spans all columns. Defaults to `inset` with the
* default feed template, `full` when `columns` is set.
*/
divider?: ListDivider
/**
* Reveals the animated checkbox column and switches row click from
* navigate to toggle. Selected values surface via `v-model:selection`.
*/
selectable?: boolean
// Two more models live on List but aren't plain props (so they're not in
// this interface): `v-model:selection` (string[], the checkbox set) and
// `v-model:active` (string, the single open/highlighted row — the List
// styles it and hides the dividers hugging it). See List.vue.
/**
* Fixed row height in px (sets `--list-row-height`). Required for
* virtualization; without it rows size to their content. Responsive
* heights are non-virtual — set them with classes on the rows instead.
*/
rowHeight?: number
}
export interface ListRowProps {
/**
* Renders the row as a RouterLink. Without `to`, a row with a click
* listener renders as a button; otherwise a plain div.
*/
to?: RouteLocationRaw
/**
* Row key — the `selection` key when `selectable` and the `v-model:active`
* key. Required whenever the list uses either.
*/
value?: string
/** Fired when the row is activated, unless selection mode claims the click. */
onClick?: (event: MouseEvent) => void
}
export interface ListHeaderCellSortProps {
/**
* Active sort direction for this column, `null`/omitted when inactive.
* The cell is controlled — sort state and toggle rules are app-owned:
* update whatever drives your ordering in the `click` handler.
*/
direction?: ListSortDirection | null
/**
* Horizontal alignment of the header content. `'end'` right-aligns the cell
* (for numeric/right-aligned columns) *and* moves the sort glyph to the
* leading side, so the label stays flush with the column's right edge and
* lines up with the values below. Defaults to `'start'`.
*/
align?: 'start' | 'end'
}
export interface ListVirtualOptions {
/** Row height in px. Defaults to the List's `rowHeight`. */
itemHeight?: number
/** Rows rendered beyond the visible window on each side. */
overscan?: number
}Renders the row as a RouterLink. Without `to`, a row with a click listener renders as a button; otherwise a plain div.
Row key — the `selection` key when `selectable` and the `v-model:active` key. Required whenever the list uses either.
Fired when the row is activated, unless selection mode claims the click.
| Slot | Payload |
|---|---|
default | {} |
ListCell
| Slot | Payload |
|---|---|
default | {} |
ListHeader
| Slot | Payload |
|---|---|
default | {} |
ListHeaderCell
| Slot | Payload |
|---|---|
default | — Column label. |
prefix | — Leading adornment, rendered before the label. |
suffix | — Trailing adornment, rendered after the label. |
Column label.
Leading adornment, rendered before the label.
Trailing adornment, rendered after the label.
ListHeaderCellSort
Show types
import type { RouteLocationRaw } from 'vue-router'
export type ListDivider = 'inset' | 'full' | 'none'
export type ListSortDirection = 'asc' | 'desc'
export interface ListProps {
/**
* Grid track sizes, written to the `--list-columns` CSS var shared by the
* header and every row. Defaults to the feed template
* `['auto', 'minmax(0,1fr)', 'auto']` (leading media, content, trailing).
* Table-style lists must pass deterministic track sizes — `auto` tracks
* size independently per row.
*/
columns?: string[]
/**
* Divider treatment between rows: `inset` starts at the content column
* (the text edge), `full` spans all columns. Defaults to `inset` with the
* default feed template, `full` when `columns` is set.
*/
divider?: ListDivider
/**
* Reveals the animated checkbox column and switches row click from
* navigate to toggle. Selected values surface via `v-model:selection`.
*/
selectable?: boolean
// Two more models live on List but aren't plain props (so they're not in
// this interface): `v-model:selection` (string[], the checkbox set) and
// `v-model:active` (string, the single open/highlighted row — the List
// styles it and hides the dividers hugging it). See List.vue.
/**
* Fixed row height in px (sets `--list-row-height`). Required for
* virtualization; without it rows size to their content. Responsive
* heights are non-virtual — set them with classes on the rows instead.
*/
rowHeight?: number
}
export interface ListRowProps {
/**
* Renders the row as a RouterLink. Without `to`, a row with a click
* listener renders as a button; otherwise a plain div.
*/
to?: RouteLocationRaw
/**
* Row key — the `selection` key when `selectable` and the `v-model:active`
* key. Required whenever the list uses either.
*/
value?: string
/** Fired when the row is activated, unless selection mode claims the click. */
onClick?: (event: MouseEvent) => void
}
export interface ListHeaderCellSortProps {
/**
* Active sort direction for this column, `null`/omitted when inactive.
* The cell is controlled — sort state and toggle rules are app-owned:
* update whatever drives your ordering in the `click` handler.
*/
direction?: ListSortDirection | null
/**
* Horizontal alignment of the header content. `'end'` right-aligns the cell
* (for numeric/right-aligned columns) *and* moves the sort glyph to the
* leading side, so the label stays flush with the column's right edge and
* lines up with the values below. Defaults to `'start'`.
*/
align?: 'start' | 'end'
}
export interface ListVirtualOptions {
/** Row height in px. Defaults to the List's `rowHeight`. */
itemHeight?: number
/** Rows rendered beyond the visible window on each side. */
overscan?: number
}Active sort direction for this column, `null`/omitted when inactive. The cell is controlled — sort state and toggle rules are app-owned: update whatever drives your ordering in the `click` handler.
Horizontal alignment of the header content. `'end'` right-aligns the cell (for numeric/right-aligned columns) *and* moves the sort glyph to the leading side, so the label stays flush with the column's right edge and lines up with the values below. Defaults to `'start'`.
| Slot | Payload |
|---|---|
default | — Column label. |
prefix | { direction: "desc" | "asc" | null; } Leading adornment, rendered before the label. |
suffix | { direction: "desc" | "asc" | null; } Sort glyph. Optional — the cell renders a built-in arrow from `direction` by default. Provide this to override (e.g. a custom lucide span). The cell owns the reveal: an inactive column's glyph shows on hover. |
Column label.
Leading adornment, rendered before the label.
Sort glyph. Optional — the cell renders a built-in arrow from `direction` by default. Provide this to override (e.g. a custom lucide span). The cell owns the reveal: an inactive column's glyph shows on hover.
| Event | Payload |
|---|---|
click | [event: MouseEvent] |
ListRows
Show types
import type { RouteLocationRaw } from 'vue-router'
export type ListDivider = 'inset' | 'full' | 'none'
export type ListSortDirection = 'asc' | 'desc'
export interface ListProps {
/**
* Grid track sizes, written to the `--list-columns` CSS var shared by the
* header and every row. Defaults to the feed template
* `['auto', 'minmax(0,1fr)', 'auto']` (leading media, content, trailing).
* Table-style lists must pass deterministic track sizes — `auto` tracks
* size independently per row.
*/
columns?: string[]
/**
* Divider treatment between rows: `inset` starts at the content column
* (the text edge), `full` spans all columns. Defaults to `inset` with the
* default feed template, `full` when `columns` is set.
*/
divider?: ListDivider
/**
* Reveals the animated checkbox column and switches row click from
* navigate to toggle. Selected values surface via `v-model:selection`.
*/
selectable?: boolean
// Two more models live on List but aren't plain props (so they're not in
// this interface): `v-model:selection` (string[], the checkbox set) and
// `v-model:active` (string, the single open/highlighted row — the List
// styles it and hides the dividers hugging it). See List.vue.
/**
* Fixed row height in px (sets `--list-row-height`). Required for
* virtualization; without it rows size to their content. Responsive
* heights are non-virtual — set them with classes on the rows instead.
*/
rowHeight?: number
}
export interface ListRowProps {
/**
* Renders the row as a RouterLink. Without `to`, a row with a click
* listener renders as a button; otherwise a plain div.
*/
to?: RouteLocationRaw
/**
* Row key — the `selection` key when `selectable` and the `v-model:active`
* key. Required whenever the list uses either.
*/
value?: string
/** Fired when the row is activated, unless selection mode claims the click. */
onClick?: (event: MouseEvent) => void
}
export interface ListHeaderCellSortProps {
/**
* Active sort direction for this column, `null`/omitted when inactive.
* The cell is controlled — sort state and toggle rules are app-owned:
* update whatever drives your ordering in the `click` handler.
*/
direction?: ListSortDirection | null
/**
* Horizontal alignment of the header content. `'end'` right-aligns the cell
* (for numeric/right-aligned columns) *and* moves the sort glyph to the
* leading side, so the label stays flush with the column's right edge and
* lines up with the values below. Defaults to `'start'`.
*/
align?: 'start' | 'end'
}
export interface ListVirtualOptions {
/** Row height in px. Defaults to the List's `rowHeight`. */
itemHeight?: number
/** Rows rendered beyond the visible window on each side. */
overscan?: number
}Items to iterate — one default-slot render per item.
How to derive a row's identity. A string reads that property off the item; a function computes it. Drives the render `:key`, the header select-all universe, and the scoped `value` slot prop. Defaults to the item's `name`, then `id`, then the index.
Window the rows (vueuse useVirtualList) so only rows near the viewport mount. `itemHeight` defaults to the List's `rowHeight`; the scroll container is the nearest scrollable ancestor.
| Slot | Payload |
|---|---|
default | { item: T; index: number; value: string; } |
ListGroup
Show types
import type { RouteLocationRaw } from 'vue-router'
export type ListDivider = 'inset' | 'full' | 'none'
export type ListSortDirection = 'asc' | 'desc'
export interface ListProps {
/**
* Grid track sizes, written to the `--list-columns` CSS var shared by the
* header and every row. Defaults to the feed template
* `['auto', 'minmax(0,1fr)', 'auto']` (leading media, content, trailing).
* Table-style lists must pass deterministic track sizes — `auto` tracks
* size independently per row.
*/
columns?: string[]
/**
* Divider treatment between rows: `inset` starts at the content column
* (the text edge), `full` spans all columns. Defaults to `inset` with the
* default feed template, `full` when `columns` is set.
*/
divider?: ListDivider
/**
* Reveals the animated checkbox column and switches row click from
* navigate to toggle. Selected values surface via `v-model:selection`.
*/
selectable?: boolean
// Two more models live on List but aren't plain props (so they're not in
// this interface): `v-model:selection` (string[], the checkbox set) and
// `v-model:active` (string, the single open/highlighted row — the List
// styles it and hides the dividers hugging it). See List.vue.
/**
* Fixed row height in px (sets `--list-row-height`). Required for
* virtualization; without it rows size to their content. Responsive
* heights are non-virtual — set them with classes on the rows instead.
*/
rowHeight?: number
}
export interface ListRowProps {
/**
* Renders the row as a RouterLink. Without `to`, a row with a click
* listener renders as a button; otherwise a plain div.
*/
to?: RouteLocationRaw
/**
* Row key — the `selection` key when `selectable` and the `v-model:active`
* key. Required whenever the list uses either.
*/
value?: string
/** Fired when the row is activated, unless selection mode claims the click. */
onClick?: (event: MouseEvent) => void
}
export interface ListHeaderCellSortProps {
/**
* Active sort direction for this column, `null`/omitted when inactive.
* The cell is controlled — sort state and toggle rules are app-owned:
* update whatever drives your ordering in the `click` handler.
*/
direction?: ListSortDirection | null
/**
* Horizontal alignment of the header content. `'end'` right-aligns the cell
* (for numeric/right-aligned columns) *and* moves the sort glyph to the
* leading side, so the label stays flush with the column's right edge and
* lines up with the values below. Defaults to `'start'`.
*/
align?: 'start' | 'end'
}
export interface ListVirtualOptions {
/** Row height in px. Defaults to the List's `rowHeight`. */
itemHeight?: number
/** Rows rendered beyond the visible window on each side. */
overscan?: number
}Section label shown in the group header. Overridden by the #header slot.
Pin the group header to the top of the scroll container while its rows scroll under it. Off by default.
| Slot | Payload |
|---|---|
default | — The group's rows — `<ListRow>` elements. |
header | — Replaces the header content (the label). |
The group's rows — `<ListRow>` elements.
Replaces the header content (the label).