List page layout
Header, filters, table, footer. The shell every list page uses.
<script setup lang="ts">
import type { ColumnDef } from '@shared/components/common/DataTable/types'
import DataTable from '@shared/components/common/DataTable/DataTable.vue'
import ListPageLayout from '@shared/components/common/pages/ListPageLayout.vue'
import { Button } from '@shared/components/ui/button'
import { Input } from '@shared/components/ui/input'
import { Plus } from 'lucide-vue-next'
interface Order extends Record<string, unknown> {
id: string
customer: string
total: string
}
const ORDERS: Order[] = [
{ id: 'INV-2043', customer: 'Northwind', total: '$1,240' },
{ id: 'INV-2044', customer: 'Lumen Retail', total: '$486' },
{ id: 'INV-2045', customer: 'Acme Health', total: '$3,910' },
]
const columns: ColumnDef<Order>[] = [
{ key: 'id', label: 'Invoice' },
{ key: 'customer', label: 'Customer' },
{ key: 'total', label: 'Total', align: 'right' },
]
</script>
<template>
<ListPageLayout title="Invoices">
<template #description>
Every invoice raised this quarter.
</template>
<template #actions>
<Button size="sm">
<Plus class="size-4" />
New invoice
</Button>
</template>
<template #filters>
<Input placeholder="Search invoices" class="max-w-xs" />
</template>
<template #table>
<DataTable :columns="columns" :data="ORDERS" row-key="id" :toolbar="false" />
</template>
<template #footer>
<p class="text-sm text-muted-foreground">
Showing 3 of 3
</p>
</template>
</ListPageLayout>
</template>Usage
Four body slots in a fixed order: filters, table, footer, with the header built from breadcrumb, description and actions, which are forwarded to the page header.
The header slots and the body slots cannot collide, which is what lets one layout own both halves.
The filters block is spaced internally, because most pages put a stats grid above the filter control, and it sits closer to the table than the standard gap. That is deliberate: a right-aligned Filters button has to read as this table's control rather than as something floating between two sections.
API
Source: src/shared/components/common/pages/ListPageLayout.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Forwarded to the page header. |
Emits
None.
Slots
| Slot | Description |
|---|---|
breadcrumb | Forwarded to the page header. |
description | Forwarded to the page header. |
actions | Forwarded to the page header. |
filters | Stats and the filter control, above the table. |
table | The table. |
footer | Under the table. |
Every slot is optional, and an empty one renders nothing rather than an empty block.
Accessibility
- The layout adds no landmarks of its own, so the page's structure is whatever you put in the slots, with the header's
<h1>at the top. - The gap between blocks is spacing, not a separator, so a screen reader is not read a run of empty elements.