Skip to content

The DataTable ​

DataTable is the shell: the card, the toolbar area, the table with its header and rows, the states between them and the pagination below. Page headers, tabs and side panels go around it; columns, rows and cards go into its slots.

vue
<DataTable
    :rows="rows"
    :meta="meta"
    :per-page="state.perPage"
    :col-count="colCount"
    :loading="loading"
    :error="error"
    :error-message="t('Could not load invoices')"
    floating-footer
    @retry="reload()"
    @page-change="goToPage"
    @per-page-change="setPerPage"
>
    <template #toolbar>…</template>
    <template #head>…</template>
    <InvoiceRow v-for="row in rows" :key="row.id" :row="row" />
    <template #empty>…</template>
</DataTable>

Props ​

PropDefaultPurpose
rowsrequiredThe rows of the current page.
meta{}Pagination metadata from the response.
perPage30The current page size — bind state.perPage.
perPageOptions[5, 10, 30, 50, 100, 200]Choices of the per-page select.
colCountrequiredNumber of <th> in the head, conditional ones included. Drives the colspan of skeleton and empty state.
loadingfalseShows the skeleton when there are no rows, the progress bar when there are.
errorfalseShows the error banner.
errorMessaget('Could not load data')The banner text.
skeletonRows10Rows the table skeleton draws.
skeletonCellClass, skeletonBarClass'px-6 py-3', 'h-6'Shape of the skeleton cells — match them to your rows so nothing jumps when the data lands.
mode'table'table · cards · list. See View Modes.
cardsMinWidth, cardsGap'260px', '0.75rem'The auto-fit grid in cards mode.
cardSkeletonHeight, cardSkeletonCount'240px', 8The cards skeleton.
listSkeletonHeight, listSkeletonCount'64px', 10The list skeleton.
bleedfalseDrops the card's rounding and ring for an edge-to-edge table on a phone.
fixedHeightfalseGives the card a height and scrolls the rows inside it. See Fixed Height.
floatingToolbarfalseLifts the toolbar into a floating pill once it would leave the screen. Not recommended; see Floating Bars.
floatingFooterfalseSame for the pagination.
floatingBreakpoint'(min-width: 768px)'Floating bars only above this media query.
floatTopOffset, floatBottomOffset12, 12Distance of the floating bars from the viewport edge, in pixels.

Extra classes land on the card.

Events ​

EventPayloadWire to
retry—reload()
page-changepage numbergoToPage
per-page-changethe select's value, a stringsetPerPage

Slots ​

SlotPurpose
#toolbarThe controls above the table. Rendered a second time inside the floating pill when floatingToolbar is on.
#headThe <th> elements. The <tr> is supplied.
defaultThe <tr> rows, in table mode.
#cardsThe cards, in cards mode.
#listThe rows, in list mode.
#emptyShown when a response carried no rows and there is no error.

The Three States ​

Every list has them from day one; they are not polish.

Loading. With no rows yet, a skeleton in the shape of the table — as many rows as skeletonRows, cells shaped by the skeleton classes. With rows on screen, a thin progress bar at the bottom of the card, and the rows stay put.

Empty. The #empty slot, in a full-width cell. Say why, and offer the way out — a filter matched nothing, so offer to clear it:

vue
<template #empty>
    <div class="py-12 text-center">
        <p class="text-gray-500">{{ t('No products match your search.') }}</p>
        <Button v-if="activeFilterCount || state.q" tone="secondary" size="sm" class="mt-3" @click="resetFilters(); clearSearch()">
            {{ t('Clear search') }}
        </Button>
    </div>
</template>

Error. A banner with the message and a retry button that emits retry. The toolbar stays usable, so a user can change the filter that produced the failure.

View Modes ​

mode switches what the shell renders below the toolbar:

ModeRendersSorting
table<table> with #head and the default slotSortButton in the header
cardsAn auto-fit grid of the #cards slot, cardsMinWidth per columnSortMenu in the toolbar
listA divided stack of the #list slotSortMenu in the toolbar

Toolbar, error banner and pagination are the same in every mode. Keep mode as a client-only state key and switch it from an OptionStrip in the toolbar; on a phone, useAutoMobileMode switches to list by itself.

vue
<template #cards>
    <Card v-for="row in rows" :key="row.id" :href="row.url">…</Card>
</template>

<template #list>
    <InvoiceListRow v-for="row in rows" :key="row.id" :row="row" />
</template>

Floating Bars ​

A long list scrolls its pagination off the screen. floatingFooter lifts it into a glass pill that follows the viewport once the original would leave it — while the page keeps its own scroll, and the original stays in place so nothing shifts.

floatingToolbar does the same for the toolbar. Not recommended: a second toolbar hovering over the rows reads as clutter.

A bar floats only above floatingBreakpoint, only while at least a little of the table is still on screen, and only when it has content. floatingFooter in a view that does not paginate produces nothing.

An application shell with a sticky header of its own sets the offsets once, as CSS custom properties on an ancestor, so no view has to know what floats above it:

css
.fi-main {
    --table-float-top: 76px;
    --table-float-bottom: 12px;
}

Fixed Height ​

The other answer to a long list: a table that lives in a region of a set size rather than on a page that scrolls. fixedHeight gives the card a height of its own and lets the rows scroll inside it — toolbar above, column header stuck to the top of the scroll region, pagination below, all three always on screen.

ValueEffect
trueTakes the room left below the card's top edge, so the table ends at the bottom of the window. Re-measured on resize; never below 240px.
a numberThat many pixels.
a stringUsed as is — '45vh' for two tables sharing a screen.

The floating bars switch themselves off; a table that never leaves the screen has nothing to lift. Off by default: a list normally scrolls with its page.

Released under the MIT License.