Table toolbar
Keep search, filters and actions together above the table they affect.
| Invoice | Client | Issued | Status |
|---|---|---|---|
| Northwind Traders | Jul 14, 2026 | Paid | |
| Blue Yonder Airlines | Aug 03, 2026 | Due | |
| Contoso | Aug 21, 2026 | Overdue | |
| Fabrikam | Sep 02, 2026 | Paid | |
| Tailspin Toys | Sep 18, 2026 | Due |
<script setup lang="ts">import { computed, ref } from 'vue'import { Plus, Settings2 } from '@lucide/vue'import type { DateRangeValue, TableColumn } from 'grani'import { UiCard, UiTableToolbar, UiDateRange, UiIconButton, UiButton, UiTable, UiCopyButton, UiDateCell, UiBadge } from 'grani' interface Invoice { id: number number: string client: string status: 'paid' | 'due' | 'overdue' issued: string} const ROWS: Invoice[] = [ { id: 1, number: 'INV-1041', client: 'Northwind Traders', status: 'paid', issued: '2026-07-14' }, { id: 2, number: 'INV-1042', client: 'Blue Yonder Airlines', status: 'due', issued: '2026-08-03', }, { id: 3, number: 'INV-1043', client: 'Contoso', status: 'overdue', issued: '2026-08-21' }, { id: 4, number: 'INV-1044', client: 'Fabrikam', status: 'paid', issued: '2026-09-02' }, { id: 5, number: 'INV-1045', client: 'Tailspin Toys', status: 'due', issued: '2026-09-18' },] const columns: TableColumn<Invoice>[] = [ { key: 'number', label: 'Invoice', width: '120px' }, { key: 'client', label: 'Client', truncate: true }, { key: 'issued', label: 'Issued', width: '130px' }, { key: 'status', label: 'Status', width: '110px' },] const TONE = { paid: 'success', due: 'neutral', overdue: 'danger' } as const const LABEL = { paid: 'Paid', due: 'Due', overdue: 'Overdue' } as const // Each tab keeps its own search: typing in one does not change the others.const query = ref('') const period = ref<DateRangeValue>({ from: null, to: null, preset: 'all' }) const byClient = (q: string) => (r: Invoice) => !q || r.client.toLowerCase().includes(q.toLowerCase()) const rows = computed(() => ROWS.filter( (r) => (!period.value.from || r.issued >= period.value.from) && (!period.value.to || r.issued <= period.value.to) && byClient(query.value)(r), ),)</script> <template> <UiCard :padded="false" class="w-full"> <UiTableToolbar v-model:search="query" search-placeholder="Search by client" class="border-b border-border" > <template #start="{ size }"> <UiDateRange v-model="period" ariaLabel="Issued" :size="size" /> </template> <template #end="{ size }"> <UiIconButton :icon="Settings2" ariaLabel="Categories" :size="size" /> <UiButton :size="size"><Plus />New invoice</UiButton> </template> </UiTableToolbar> <UiTable :columns="columns" :items="rows" :filtered="Boolean(query || period.from || period.to)" > <template #cell.number="{ value }"> <UiCopyButton size="xs" flush :value="String(value)" :label="String(value)" /> </template> <template #cell.issued="{ value }"> <UiDateCell :value="String(value)" /> </template> <template #cell.status="{ value }"> <UiBadge :tone="TONE[value as Invoice['status']]" size="sm"> {{ LABEL[value as Invoice['status']] }} </UiBadge> </template> </UiTable> </UiCard></template>
Also called list toolbar, search and filters bar, table header bar.
Usage
<UiTableToolbar v-model:search="search" /> With GraniResolver in the Vite config, a template needs no import: set it up once.
API
| Name · type | Default | Description |
|---|---|---|
| search*string | The search text (v-model:search); the field is always there. Sent as typed, spaces included, after a 250 ms pause or at Enter. | |
| searchPlaceholderstring | What the search looks in; the field's placeholder and its name. | |
| columnsTableColumns<T> | The columns menu: the state from useTableColumns. The button stands first in the right cluster; without it the columns are fixed. | |
| sizexs | sm | md | Step of the control ladder for the row: xs for a dense row, sm (36 px), md for a comfortable one; unset, the app's size (createGrani({ size })). The slots get it as { size }. |
Anything else lands on the root element: native attributes, aria-*, data-testid.
When to use another component
| When you need | Take |
|---|---|
| One block of table, toolbar, pages and states | Data table |
| A search outside a list | Input type="search" |
| A second columns menu | Never in #end; the toolbar draws it from columns |