Toolbar & Filters
The toolbar is the #toolbar slot of DataTable, and the package ships every control a list toolbar tends to need. This page describes each one and the order they belong in, so every list in the application reads the same way.
The Canonical Order
<template #toolbar>
<SearchInput … />
<Combobox … /> <!-- two to four standard filters -->
<div class="h-5 w-px bg-gray-200 dark:bg-white/10" role="separator" aria-orientation="vertical"></div>
<OptionStrip variant="pills" multiple … /> <!-- quick filters, optional -->
<OptionStrip … /> <!-- option strips, optional -->
<ViewProfileMenu class="ml-auto" … /> <!-- opens the right-hand cluster -->
<SortMenu … />
<ColumnPicker v-if="state.mode === 'table'" … />
<IconButton :label="t('Filters')" size="lg" @click="toggleFilters"> <!-- with a FilterPanel -->
<IconFilter />
<span v-if="activeFilterCount" class="badge">{{ activeFilterCount }}</span>
</IconButton>
<OptionStrip v-model="state.mode" variant="segmented" … /> <!-- view mode, last -->
</template>- Search is always there, first. No list turns it off.
- Standard filters — the two to four almost every reader reaches for — sit beside it as
Comboboxes. - Quick filters follow behind a pipe divider: one
OptionStrip variant="pills" multiple, a pill per filter. Each pill switches on and off on its own, and the strip never has an "All" option — no pill set already means all. - Option strips — any other one-of-many choice — come next. The view mode is not one of them.
- Advanced filters — date ranges, thresholds, cross-cutting flags — live in the
FilterPanel, opened by the filter toggle with theactiveFilterCountbadge. - The right-hand cluster — views, sort, columns, filter toggle, view mode — lands in the same spot in every table, so a reader's muscle memory carries over. The view mode closes it.
- A list opens unfiltered. Dropdown and quick filters start empty; another default is the exception.
Every control is 36px tall (size="lg" on the icon buttons), so the row reads as one line.
SearchInput
<SearchInput
ref="search"
:model-value="state.q"
:placeholder="t('Search name, number or customer…')"
:clear-label="t('Clear search')"
@update:model-value="onSearchInput"
@clear="clearSearch()"
/>| Prop | Default | Purpose |
|---|---|---|
modelValue | '' | Bind state.q. |
placeholder | '' | Say what can be searched. |
clearLabel | t('Clear search') | Accessible label of the × button, which appears once there is text. |
Events: update:modelValue → onSearchInput, clear → clearSearch. The ref exposes focus() and selectAll(). Enter selects the whole term, so a barcode scanner's next scan overwrites the previous one instead of appending.
Combobox
The base package's searchable select, re-exported here because it is the standard filter control. Its full reference is under the base package's Selects. The three things that matter in a toolbar:
<Combobox
:model-value="state.brand"
:options="props.brands"
:empty-value="0"
:placeholder="t('Brand')"
:all-label="t('All brands')"
variant="filter"
@update:model-value="(value) => setFilter('brand', value)"
/>emptyValuemust match the filter's default inDEFAULTS. It defaults to0; pass""for a string filter, or clearing writes0into the state and the URL.variant="filter"tints the trigger while a value is set, so an active filter is visible at a glance. The defaultfieldvariant is for forms and must not be used here.- Long lists pass
fetchOptionsandselectedLabel, so the options come from the server per search and a deep-linked value outside the preloaded page still shows its name.
SortButton
Sorting from the column header, in table mode. Renders the label with an arrow that rotates for ascending and dims when the column is not the active sort.
<th><SortButton field="total" :label="t('Total')" :sort="state.sort" :dir="state.dir" @sort="setSort" /></th>
<th><SortButton field="weight" :label="t('Weight')" icon="o-scale" :sort="state.sort" :dir="state.dir" @sort="setSort" /></th>| Prop | Default | Purpose |
|---|---|---|
field | required | The sort key sent to the server. |
label | required | The column name — shown, or used as tooltip when icon or short is set. |
icon | '' | An icon name from your set, for a narrow column. |
short | '' | An abbreviation, for a narrow column. |
sort, dir | '', 'desc' | The current state. |
Event: sort(field) → setSort.
SortMenu
Sorting from the toolbar, in every mode — the only way to sort in cards and list mode, where there is no header.
<SortMenu :options="sortOptions" :sort="state.sort" :dir="state.dir" :label="t('Sort by')" @sort="setSort" />| Prop | Default | Purpose |
|---|---|---|
options | required | { field, label }[]. |
sort, dir | '', 'desc' | The current state. The active option shows its direction. |
label | t('Sort by') | Accessible label of the button. |
Picking an option closes the menu; picking the active one again flips the direction.
ColumnPicker
Lets the reader choose which columns to show. The visible keys are client-only state, kept in the URL and saved as a per-user preference.
<ColumnPicker
:columns="COLUMN_REGISTRY"
:visible="columns.keys.value"
:changed="columns.changed.value"
:label="t('Columns')"
:reset-label="t('Reset columns')"
@update="columns.set"
@reset="columns.reset"
/>| Prop | Default | Purpose |
|---|---|---|
columns | required | { key, label, default?, locked?, span? }[]. A locked column cannot be hidden. |
visible | required | The keys currently shown. |
changed | false | Shows the count badge on the button and the reset row at the bottom. |
label, resetLabel | translated |
Events: update(keys) — in the order the columns were registered — and reset.
columns comes from useColumnPicker, which also keeps colCount right: pass columns.span.value.
FilterPanel
A panel beside the table for the filters that would make the toolbar too dense. It docks on the right while there is room and floats over the table otherwise; whether it is open is remembered per browser.
<script setup>
const { root, rootStyle, headerStyle, filtersOpen, panelRendered, docked, toggleFilters } =
useFilterPanelDock('invoices.filters', { panelWidth: 320 });
</script>
<template>
<div ref="root" :style="rootStyle" class="mx-auto w-full">
<div class="flex items-start gap-4">
<DataTable class="min-w-0 flex-1" …>
<template #toolbar>
…
<IconButton :label="t('Filters')" size="lg" :tone="filtersOpen ? 'active' : 'quiet'" @click="toggleFilters"><IconFilter /></IconButton>
</template>
</DataTable>
<FilterPanel v-if="panelRendered" v-show="filtersOpen" :title="t('Filters')"
:class="docked ? 'w-80 shrink-0' : 'fixed right-4 top-20 z-10 w-80'" @close="toggleFilters">
…
<template #footer>
<Button tone="secondary" size="sm" :disabled="!activeFilterCount" @click="resetFilters">{{ t('Reset filters') }}</Button>
</template>
</FilterPanel>
</div>
</div>
</template>useFilterPanelDock(storageKey, options) wraps useViewWidth(): it returns the same root and rootStyle, and widens the view by the panel's width while the panel is docked, so the table keeps its size instead of shrinking.
| Option | Default | Purpose |
|---|---|---|
baseWidth | 1536 | The view's base width. |
panelWidth | 320 | The panel's width; the view widens by this plus a gap. |
enabled | null | A ref that disables docking — on a phone, for instance. |
| Returns | Meaning |
|---|---|
filtersOpen | Whether the panel is open. Persisted under storageKey in localStorage. |
panelRendered | Turns true on first open and stays, so the panel keeps its state when hidden. |
docked | Whether there is room to dock. |
headerStyle | A maxWidth for the page header, so it stays aligned with the table while the panel docks. |
toggleFilters() |
FilterPanel itself takes a title and emits close. Its slots: default for the scrolling body, #tabs above it, #footer below with a divider.
Icons
The toolbar icons the package uses are exported as components, so a list can add the same glyphs without redrawing them:
IconSearch, IconFilter, IconColumns, IconSort, IconViews, IconStar, IconModeTable, IconModeCards, IconModeList, IconChevronRight. Each is a bare <svg> that takes its size and colour from the caller's classes.