@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
| Component | Description |
|---|---|
DataGrid.Root | Main container with title, add button |
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 |
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: 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.