Skip to content

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 ​

vue
<template #toolbar>
    <SearchInput … />
    <Combobox … />                       <!-- two to four standard filters -->
    <OptionStrip … />                    <!-- quick filters, optional -->

    <ViewProfileMenu class="ml-auto" … />  <!-- opens the right-hand cluster -->
    <SortMenu v-if="state.mode !== 'table'" … />
    <ColumnPicker v-if="state.mode === 'table'" … />
    <IconButton :label="t('Filters')" size="lg" @click="toggleFilters">
        <IconFilter />
        <span v-if="activeFilterCount" class="badge">{{ activeFilterCount }}</span>
    </IconButton>
    <OptionStrip v-model="state.mode" variant="segmented" … />
</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.
  • Advanced filters — date ranges, thresholds, cross-cutting flags — live in the FilterPanel, opened by the filter toggle with the activeFilterCount badge.
  • 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.

Every control is 36px tall (size="lg" on the icon buttons), so the row reads as one line.

SearchInput ​

vue
<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()"
/>
PropDefaultPurpose
modelValue''Bind state.q.
placeholder''Say what can be searched.
clearLabelt('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:

vue
<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)"
/>
  • emptyValue must match the filter's default in DEFAULTS. It defaults to 0; pass "" for a string filter, or clearing writes 0 into 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 default field variant is for forms and must not be used here.
  • Long lists pass fetchOptions and selectedLabel, 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.

vue
<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>
PropDefaultPurpose
fieldrequiredThe sort key sent to the server.
labelrequiredThe 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, for cards and list mode where there is no header.

vue
<SortMenu :options="sortOptions" :sort="state.sort" :dir="state.dir" :label="t('Sort by')" @sort="setSort" />
PropDefaultPurpose
optionsrequired{ field, label }[].
sort, dir'', 'desc'The current state. The active option shows its direction.
labelt('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.

vue
<ColumnPicker
    :columns="COLUMNS"
    :visible="visibleColumnKeys"
    :changed="state.cols !== DEFAULTS.cols"
    :label="t('Columns')"
    :reset-label="t('Reset columns')"
    @update="setColumns"
    @reset="setColumns(DEFAULT_COLUMN_KEYS)"
/>
PropDefaultPurpose
columnsrequired{ key, label, locked? }[]. A locked column cannot be hidden.
visiblerequiredThe keys currently shown.
changedfalseShows the count badge on the button and the reset row at the bottom.
label, resetLabeltranslated

Events: update(keys) — in the order the columns were registered — and reset.

colCount on the DataTable must follow the visible columns, conditional ones included. Derive it from the same list:

js
const visibleColumns = computed(() => COLUMNS.filter((column) => visibleColumnKeys.value.includes(column.key)));
const colCount = computed(() => visibleColumns.value.reduce((sum, column) => sum + (column.span ?? 1), 0));

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.

vue
<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.

OptionDefaultPurpose
baseWidth1536The view's base width.
panelWidth320The panel's width; the view widens by this plus a gap.
enablednullA ref that disables docking — on a phone, for instance.
ReturnsMeaning
filtersOpenWhether the panel is open. Persisted under storageKey in localStorage.
panelRenderedTurns true on first open and stays, so the panel keeps its state when hidden.
dockedWhether there is room to dock.
headerStyleA 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.

Released under the MIT License.