Skip to content

Data Table

Batteries-included table for admin list pages — search, filters, sorting, column visibility, pagination, loading skeletons and an empty state in one component. State lives outside the markup — including saved views: call createDataTable({ rows, columns, rowKey }) (rows/columns must be getter functions so the table tracks them) and hand the returned controller in as table. Any column can be drawn by hand with a snippet named after its id — { id: "status" } is rendered by {#snippet status(row)}.

A full table

Search, filters, sorting, column visibility and paging, from one controller and a list of columns. Any list with those controls uses DataTable — hand-rolling from the raw Table primitives is how the same toolbar, empty row, skeleton and pagination end up copied across three screens. createDataTable is the only thing that talks to TanStack; app code never imports it.

Channel Status
3412

Ada Lovelace

ada@example.com

Webpaid3$248.902026-08-09
3411

Grace Hopper

grace@example.com

Instagramshipped1$89.002026-08-09
3410

Alan Turing

alan@example.com

Webrefunded2$132.502026-08-08
3409

Katherine Johnson

katherine@example.com

WhatsApppaid5$512.202026-08-08
3408

Margaret Hamilton

margaret@example.com

Webpending1$45.002026-08-07
Showing 1–5 of 12

Loading

loading renders skeletonRows placeholder rows, but only while the table has no rows yet — a table that already has data keeps it during a refetch instead of blinking back to bars. A column whose cell has a distinctive shape supplies its own <id>Skeleton snippet, so the placeholder row is the height of the row it is about to become.

Channel Status
No results

Refreshing

loading on a table that already has rows is a different state from the one above: the rows stay put, and an indeterminate bar plus a dimmed, inert body carry the signal instead — a filter or sort change that refetches the same shape of data reads as *updating*, not as a table that forgot everything it knew a moment ago. Press Refresh to see it.

Channel Status
3412Ada LovelaceWebpaid3$248.902026-08-09
3411Grace HopperInstagramshipped1$89.002026-08-09
3410Alan TuringWebrefunded2$132.502026-08-08
3409Katherine JohnsonWhatsApppaid5$512.202026-08-08
3408Margaret HamiltonWebpending1$45.002026-08-07
Showing 1–5 of 12

Custom cells

A cell is either a snippet named after the column's id (customer and status below), or — for a column list assembled from config rather than typed out by hand — cell set directly on the column, as fulfillment does here. A named snippet still wins when both are present, so one column of an otherwise-shared list can be overridden at the call site. Avatar, Badge, Progress and a DropdownMenu cover the shapes that come up again and again: an identity, a state, a quantity, a row's own actions.

Status Fulfillment
3412
AL

Ada Lovelace

ada@example.com

paid
100%
3411
GH

Grace Hopper

grace@example.com

shipped
100%
3410
AT

Alan Turing

alan@example.com

refunded
0%
3409
KJ

Katherine Johnson

katherine@example.com

paid
100%
3408
MH

Margaret Hamilton

margaret@example.com

pending
45%
Showing 1–5 of 12

Views

views + activeView + viewsStorage go to createDataTable, not to DataTable — the controller owns them, so saving, renaming and deleting are one code path the app never re-implements. A fixed view (fixed: true, like both below) is always there and can't be renamed or deleted; a custom one saved through viewsStorage.save gets a rename/delete menu of its own. A view stays pressed only while the query still matches it, and Reset goes back to the active view's own query rather than clearing everything.

Channel Status
3412Ada LovelaceWebpaid3$248.902026-08-09
3411Grace HopperInstagramshipped1$89.002026-08-09
3410Alan TuringWebrefunded2$132.502026-08-08
3409Katherine JohnsonWhatsApppaid5$512.202026-08-08
3408Margaret HamiltonWebpending1$45.002026-08-07
Showing 1–5 of 12

Empty

Say what is missing, in the table's own body rather than in place of the whole table — the toolbar has to stay reachable, or a reader who filtered themselves into nothing has no way back out.

Channel Status
No orders yet.
No results

Failed to load

When the fetch itself fails, rather than returning zero rows, swap the table for ErrorState entirely — a toolbar and header row over a failure invites filtering or sorting something that was never loaded. Give the reader the one useful action: retry.

Couldn't load orders

Something went wrong while fetching orders. Check your connection and try again.

API

Prop Type Default Description
table required DataTableController

Controller returned by createDataTable(...).

rowHref (row: *) => string

Turns each row into a navigable link to a detail page.

onRowClick (row: *) => void

Click handler for a row, for drawer or modal flows.

rowClass (row: *) => string

Per-row Tailwind classes, e.g. dimming a disabled merchant.

loading boolean false

Two regimes, chosen by whether the table already has rows. With none yet, renders skeletonRows placeholder rows. With rows already showing (e.g. a filter change refetching), keeps them and instead shows an indeterminate progress bar plus a dimmed, non-interactive body — rows never blink back to skeleton bars.

empty string 'No results found.'

Message shown in a full-width centered cell when there are no rows.

searchPlaceholder string

Placeholder for the toolbar search input. Omit to hide the search field entirely.

skeletonRows number 5

Number of skeleton rows rendered while loading.

filters boolean true

Renders the built-in Filters button whenever at least one column is filterable. Set to false to hide it.

filterOptions Object<string, Array<{value: *, label: string}>> {}

{ [columnId]: [{ value, label }] } option lists for value-picker columns, for server-driven tables.

filtersLoading boolean false

Replaces the Filters popover body with a spinner while option lists are being fetched.

onFiltersOpen () => void

Fires every time the Filters popover opens; the hook for lazy-loading filterOptions.

columnToggle boolean false

Shows the Columns popover listing every column not marked hideable: false.

resizable boolean false

Lets the user drag column edges; switches the table to table-fixed.

pageSizeOptions number[]

Row-count choices offered in the footer, e.g. [5, 10, 25, 50].

toolbar import('svelte').Snippet

Rendered in the left toolbar group, after search and Filters.

toolbarEnd import('svelte').Snippet

Rendered in the right toolbar group, before the column toggle.

class string

Additional Tailwind classes merged onto the root wrapper.

cells Object<string, import('svelte').Snippet<[*]>>

Rest props: snippets named after a column's id, e.g. {#snippet status(row)}, for custom cell rendering. A column whose cell has a distinctive shape (an avatar, a two-line title/subtitle, a badge) can also supply {#snippet <id>Skeleton()} — rendered in that column's cell while loading, in place of the generic bar, so the skeleton row's height matches the real row it's about to become.

DataTableColumn

Prop Type Default Description
id required string

Unique column id; also the snippet name for custom cell rendering.

accessor string|false

Dot-path into the row for the cell value; defaults to id. false marks a display-only column (e.g. actions).

label required string

Header text.

sortable boolean

Shows a sortable header button.

searchable boolean

Included in the toolbar search; defaults to true when there's an accessor.

filter boolean|'select'|'text'|'number'|'date'|'boolean'

Forces a filter editor type, or false to exclude the column from Filters.

options Array<{value: *, label: string}>

Explicit value list for a select filter, overriding derived facets.

align 'left'|'center'|'right'

Cell/header text alignment.

class string

Extra classes on each TableCell.

headClass string

Extra classes on the TableHead.

format (value: *, row: *) => string

Formats the raw value when no custom cell snippet or cell is provided.

cell import('svelte').Snippet<[*]>

Custom cell renderer supplied as data instead of a named snippet — for columns assembled at runtime (e.g. from server-driven config), where there's no fixed id to name a {#snippet} after. A DataTable snippet named after the column's id still takes precedence when both are present, so a call site can override one column from an otherwise-shared column list.

hideable boolean

Set to false to keep the column out of the Columns toggle and always visible.

width number

Column width in px, used when resizable is on.

minWidth number

Minimum drag width in px.

maxWidth number

Maximum drag width in px.

resizable boolean

Set to false to pin this column's width even when the table is resizable.

hidden boolean

Initially hidden.

DataTableQuery

Prop Type Default Description
search required string
filters required Object
sort required {column: string, direction: 'asc'|'desc'}|null
page required number
perPage required number

DataTableState

Prop Type Default Description
search required string
filters required Object
sort required {column: string, direction: 'asc'|'desc'}|null

DataTableView

Prop Type Default Description
id required string
name required string
state required DataTableState

Applied when this view is selected.

fixed boolean

Always visible; skips the rename/delete menu the custom views get.

DataTableViewsStorage

Prop Type Default Description
save (name: string, state: DataTableState) => Promise<DataTableView>
delete (id: string) => Promise<void>
rename (id: string, name: string) => Promise<void>

DataTableController

Prop Type Default Description
rows required Array<Object>

Visible (paginated/filtered/sorted) rows for the current page.

allColumns required Array<Object>

Every column, including hidden ones (for the Columns toggle).

headers required Array<Object>

Current page's header cells, for the table head row.

columns required Array<Object>

Resolved column defs (for colspan on skeleton/empty rows).

filterableColumns required Array<Object>

Columns eligible for the Filters panel.

activeFilters required Array<{id: string, column: Object, value: *}>

Currently applied filters, for the chips readout.

filters required Object

Draft/applied filter values keyed by column id.

dirty required boolean

True when search/filters/sort differ from what Reset would restore — the active view's own query, or the table's initial one when no view is selected.

pending required boolean

True when commit: true and there are unapplied draft changes.

search required string

Bindable search box value.

commits required boolean

True when the table was created with commit: true.

page required number

Current 1-based page number (bindable).

perPage required number

Rows per page (bindable).

total required number

Total row count across all pages.

totalWidth required number

Sum of column widths, for the table's fixed-layout width when resizable.

query required DataTableQuery

Search/filters/sort/page state, for building a server request.

state required DataTableState

Current search/filters/sort, minus paging — what a view saves.

applyState required (state: DataTableState) => void

Restores a saved view's search/filters/sort; resets to page 1.

matchesState required (state: DataTableState) => boolean

Whether the table's current query still matches a given saved state.

sortOf required (columnId: string) => ('asc'|'desc'|false)

Current sort direction for a column, if any.

toggleSort required (columnId: string) => void

Cycles a column's sort direction.

resizeColumn required (columnId: string, width: number) => void

Sets a column's width.

toggleColumn required (columnId: string) => void

Shows/hides a column.

clearFilter required (columnId: string) => void

Removes one active filter.

counts required (columnId: string) => Object

Facet value counts for a column's filter editor.

reset required () => void

Restores what the reader last chose: the active view's own query, or the table's initial search/filters/sort when no view is selected.

apply required () => void

Commits draft search/filters when commit: true.

views required DataTableView[]

Saved queries, in the order they render. Assign a new array to replace the list (e.g. after fetching it).

activeView required string|undefined

Id of the view Reset restores. Set by selectView, and by a successful saveView/deleteView.

canSaveView required boolean

Whether viewsStorage can create views, i.e. whether to offer "Add view".

canRenameView required boolean

Whether viewsStorage can rename a view.

canDeleteView required boolean

Whether viewsStorage can delete a view.

viewPending required boolean

True while a save/rename/delete is in flight.

viewError required string

Message from the last failed view action; empty when the last one succeeded.

isViewActive required (id: string) => boolean

Whether a view is both the selected one and still what the table is showing — what marks its button aria-pressed.

selectView required (id: string) => void

Makes a view active and applies its state.

saveView required (name: string) => Promise<DataTableView|null>

Persists the current state as a new view; resolves null and fills viewError when storage rejects.

renameView required (id: string, name: string) => Promise<boolean>

Renames a view through storage.

deleteView required (id: string) => Promise<boolean>

Deletes a view through storage, falling back to the first fixed view (and applying its state) when the active one goes.

clearViewError required () => void

Drops the last view error, e.g. when the save dialog reopens.

Also exported from this module: createDataTable