Customization
Empty states, style slot overrides, and state persistence.
Empty, loading and error states
Custom Empty State
<tbody>, so it must be table-row markup.Name | Role | Department | Location | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
No results found Try adjusting your filters or search term. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Table {items} {columns}>
{#snippet emptyState()}
<tr>
<td colspan="99" class="py-12 text-center">
<div class="text-4xl mb-3">🔍</div>
<p class="text-lg font-semibold">No results found</p>
<p class="text-sm text-text-secondary mt-1">
Try adjusting your filters or search term.
</p>
</td>
</tr>
{/snippet}
</Table>On mobile the card list shows the plain noDataText instead, since <tr> and <td> cannot live in a <div>. loadingState and errorState behave the same way.
Style slot overrides
Style Slot Overrides
slotClasses to add your own classes to specific rendering slots, or unstyled to strip all variant classes.<Table
{items}
{columns}
slotClasses={{
container: 'my-custom-container',
row: 'hover:bg-primary-subtle',
headerRow: 'bg-surface-elevated',
cell: 'px-6',
filterBar: 'mb-6'
}}
/>Seventeen slots, one per element the table renders, are listed with what each one wraps
under TableSlotClasses. Two are narrower than their names suggest: cell and headerCell cover the cells of your columns and not
the table's own — the selection checkbox, the expand chevron, the group spacer keep their
fixed widths, so padding you add here cannot deform them. Reach those through row, or restyle everything with unstyled.
What the table remembers
State Persistence
<script>
import { Table, createTableView, bindViewToStorage } from '@urbicon-ui/table';
// The view — which rows the reader is looking at.
const view = createTableView({ defaults: { pageSize: 25 } });
const saved = bindViewToStorage(view, { key: 'team-roster' });
</script>
<!-- The preferences — how the table looks. -->
<Table {items} {columns} {view} prefs={{ storage: 'team-roster', persistSelection: true }} />The view — search, sort, page, page size, filters, grouping — decides which rows a reader
sees. It lives on a view object you own, and bindViewToStorage gives that object a memory. The
preferences — hidden columns, column order, summaries, and opt-in selection — decide how the
table looks; they belong to the table and travel through prefs. The two channels write separate entries and
only share the key here because one name is easier to remember.
Narrowing What Is Stored
axes narrows what is remembered — the default is every setting but the page number.<script>
const view = createTableView();
const saved = bindViewToStorage(view, {
key: 'team-roster',
axes: ['sort', 'pageSize'], // remember the ordering, forget search and filters
storage: sessionStorage, // tab-scoped; localStorage is the default
debounceMs: 500
});
// saved.flush() — write the pending change now, e.g. before a programmatic
// navigation; the teardown drops what is still pending.
// saved.clear() — the "reset saved view" button: removes the stored entry
// and leaves the live view untouched.
</script>
<Table
{items}
{columns}
{view}
prefs={{ storage: { key: 'team-roster', kind: 'sessionStorage' } }}
/>storage takes any Storage object, so sessionStorage scopes the memory to the tab; the
preferences channel spells the same choice as its own kind option. Writes are debounced, and the binding
hands back flush and clear.
Every view setting except the page number is stored
Naming a page is what a link is for, so the URL keeps that setting and storage does not — a fresh visit starts on page one. The page size is stored — "yesterday's page size is still set" is squarely this binding's promise, and it is a change from v7, which persisted no pagination at all.Only what the reader changed
A default is never written back, so a value you change indefaults later reaches
everyone who has not overridden that setting. What a binding applies is not the reader's doing
either — arriving on someone else's link leaves the saved view alone.Clearing is a state
A setting the reader emptied — no sort, no filters, no grouping, no summaries, no hidden columns — restores empty and wins over the matching default. Only a missing or unreadable entry falls back.Selection is off by default
persistSelection keys rows by item.id; without stable ids the
selection falls back to the row position and restores onto different rows after a reorder.Upgrading from v7
The preferences keep their storage keys, so a reader's column layout survives. The view settings moved into one entry per view — the per-setting keys v7 wrote (table_sort_*, table_search_*, table_filters_*, table_group_by_*) are no longer read.