Skip to main content

@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/remote'

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,
)
})

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>

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

ComponentDescription
DataGrid.RootMain container with title, add button
DataGrid.FilterFormAuto-generated filter form from schema
DataGrid.FilterFieldCustom filter field (use inside FilterForm with children snippet)
TableLower-level table component
PaginationPagination controls

useDataGrid Options

OptionDescription
tableOptionsTanStack Table options (columns, initial state)
queryRemote query function
filterSchemaZod schema for filters
getRowLinkFunction 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: ch.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.

To disable column management entirely, set enableHiding: false in tableOptions. This hides the settings button and disables column toggling.