@repo/table
Data grid system built on TanStack Table with URL-based state management.
Usage
Query Endpoint
Create a query endpoint that handles pagination, sorting, and filtering:
// users.remote.ts
import { query } from '$app/server'
import { countSelect, overviewToQuery } from '@repo/db'
import { createOverviewQuerySchema } from '@repo/table/server'
export const getUsersQuery = query(createOverviewQuerySchema(userFilterSchema), async (data) => {
const { db, usersTable } = await getTenantDb()
return await overviewToQuery(
db
.select({
id: usersTable.id,
name: usersTable.name,
count: countSelect, // Required for pagination
})
.from(usersTable)
.$dynamic(),
usersTable,
data,
)
})Use @repo/table/server for query schemas and pagination contracts in server or remote modules.
Use @repo/table/remote for the browser data-grid API.
DataGrid Component
<script lang="ts">
import type { QueryDataPoint } from '@repo/table/remote'
import { getUsersQuery } from '$lib/domains/users/database/users.remote'
import { userFilterSchema } from '$lib/domains/users/utils/schemas'
import { createColumnHelper, DataGrid, useDataGrid } from '@repo/table/remote'
const ch = createColumnHelper<QueryDataPoint<typeof getUsersQuery>>()
const { table, dataGrid } = useDataGrid({
tableOptions: {
columns: ch.columns([
ch.accessor('name', { header: 'Name' }),
ch.accessor('createdAt', { header: 'Created At' }),
]),
initialState: {
sorting: [{ id: 'name', desc: false }],
},
},
query: getUsersQuery,
filterSchema: userFilterSchema,
getRowLink: (row) => `users/${row.id}`,
})
</script>
<DataGrid.Root title="Users" {table} {dataGrid} add="/users/add">
<DataGrid.FilterForm schema={userFilterSchema} />
</DataGrid.Root>Set addLabel to show text beside the add icon from the md breakpoint upward. The label remains
the button's accessible name when hidden on smaller screens.
Filters render in the toolbar as compact badges. The Filters pill lists inactive fields; choosing a field adds it to the bar, where its popover can edit or remove the filter. Active filters remain URL-backed, so paging, sorting, and reloads preserve the current query.
Set applyFiltersImmediately={false} on DataGrid.Root to stage badge changes. In that mode the
toolbar shows a right-aligned Apply button, enabled only while the valid draft differs from the
applied URL-backed filters.
Override the table presentation without rebuilding generated rows, states, sorting, or pagination:
<script lang="ts">
import { Table } from '@repo/components'
</script>
<DataGrid.Root {table} {dataGrid} {title}>
{#snippet tableLayout({ body, headerRows, loading, pagination })}
<div class="overflow-hidden rounded-lg border border-primary/20">
<Table.Root aria-busy={loading}>
<Table.Header class="bg-primary/5 [&_th]:text-primary">
{@render headerRows()}
</Table.Header>
{@render body()}
</Table.Root>
{@render pagination()}
</div>
{/snippet}
</DataGrid.Root>tableLayout replaces the default table container. Render headerRows, body, and pagination
once and preserve the table semantics.
Custom Filter Fields
Use DataGrid.FilterField inside DataGrid.FilterForm for custom field rendering (e.g., select dropdowns with dynamic options):
<DataGrid.FilterForm schema={userFilterSchema}>
{#snippet children({ fields, value })}
<TypedForm.Field field={fields.name} bind:value={value.name} />
<TypedForm.Field
field={fields.status}
bind:value={value.status}
input={TypedForm.SelectInput}
optionItems={statusOptions}
/>
<TypedForm.Field
field={fields.roleId}
bind:value={value.roleId}
input={TypedForm.SelectInput}
optionItems={roleOptions}
label="Role"
/>
{/snippet}
</DataGrid.FilterForm>Custom filter snippets receive fields and the current filter value. Bind custom fields to value.*. Apply/Clear buttons are handled automatically by FilterForm.
Option Label Cells
Use optionsCell to display SelectOption labels in table cells while keeping the raw value for sorting and filtering:
<script lang="ts">
import { optionsCell } from '@repo/table'
const statusOptions = [
{ label: 'Active', value: 'active' },
{ label: 'Archived', value: 'archived' },
]
const columns = ch.columns([
ch.accessor('status', {
header: 'Status',
cell: optionsCell(statusOptions),
}),
])
</script>Components
| Component | Description |
|---|---|
DataGrid.Root | Main container with title, actions, and optional controls |
DataGrid.FilterForm | Auto-generated filter form from schema |
DataGrid.FilterField | Custom filter field (use inside FilterForm with children snippet) |
Table | Lower-level table component |
Pagination | Pagination controls |
Remote data grids show the filtered and unfiltered counts in the table footer, for example
4 of 12 items.
useDataGrid Options
| Option | Description |
|---|---|
tableOptions | TanStack Table options (columns, initial state) |
query | Remote query function |
filterSchema | Zod schema for filters |
getRowLink | Function to generate row links |
Row Selection
Enable checkbox-based row selection via tableOptions.enableRowSelection (native TanStack option). A checkbox column is automatically prepended.
<script lang="ts">
const { table, dataGrid, selectedRows, resetRowSelection } = useDataGrid({
tableOptions: {
columns,
enableRowSelection: true,
},
query: getUsersQuery,
})
function handleBulkDelete() {
// selectedRows contains the actual data items
console.log(selectedRows)
resetRowSelection()
}
</script>A checkbox column is automatically prepended. The header checkbox supports select-all with indeterminate state.
URL State
Sorting, pagination, filters, and column configuration are automatically synced to URL params for shareable links.
Column visibility uses a cols param listing visible column IDs (e.g. ?cols=name,email). No cols param means all columns are visible.
The grid uses TanStack Table v9's feature-bound table hook and external atoms for sorting, pagination, row selection, and column visibility. Read and update these slices through table.state, table.atoms, and table methods; do not add local state bridges or call setOptions to force reactivity.
Column management is enabled by default. Set showColumnManagement={false} on
DataGrid.Root to hide its menu. Set enableHiding: false in tableOptions, or
on an individual column, to prevent column toggling.