Skip to main content
Urbicon UI

Customization

Empty states, style slot overrides, and state persistence.

Empty, loading and error states

Custom Empty State

The snippet renders into the desktop <tbody>, so it must be table-row markup.
Name
Role
Department
Location

No results found

Try adjusting your filters or search term.

No data available
<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

Use 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

Two kinds of state, two channels — the view is yours, the preferences are the table's.
<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 in defaults 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.