<script lang="ts">
import {
columnResizingFeature,
columnSizingFeature,
createColumnHelper,
createTable,
FlexRender,
tableFeatures,
} from '@tanstack/svelte-table'
import { makeData } from './makeData'
import type { Person } from './makeData'
import './index.css'
const features = tableFeatures({
columnSizingFeature,
columnResizingFeature,
})
const columnHelper = createColumnHelper<typeof features, Person>()
const columns = columnHelper.columns([
columnHelper.group({
header: 'Name',
footer: (props) => props.column.id,
columns: columnHelper.columns([
columnHelper.accessor('firstName', {
cell: (info) => info.getValue(),
footer: (props) => props.column.id,
}),
columnHelper.accessor((row) => row.lastName, {
id: 'lastName',
cell: (info) => info.getValue(),
header: () => 'Last Name',
footer: (props) => props.column.id,
}),
]),
}),
columnHelper.group({
header: 'Info',
footer: (props) => props.column.id,
columns: columnHelper.columns([
columnHelper.accessor('age', {
header: () => 'Age',
footer: (props) => props.column.id,
}),
columnHelper.accessor('visits', {
header: () => 'Visits',
footer: (props) => props.column.id,
}),
columnHelper.accessor('status', {
header: 'Status',
footer: (props) => props.column.id,
}),
columnHelper.accessor('progress', {
header: 'Profile Progress',
footer: (props) => props.column.id,
}),
]),
}),
])
let data = $state(makeData(200))
const refreshData = () => {
data = makeData(200)
}
const stressTest = () => {
data = makeData(5_000)
}
const table = createTable({
features,
columns,
get data() {
return data
},
defaultColumn: { minSize: 60, maxSize: 800 },
columnResizeMode: 'onChange',
debugTable: true,
debugHeaders: true,
debugColumns: true,
})
/**
* All column widths flow through CSS variables derived in ONE string and
* bound to the <table> element's style attribute. `header.getSize()` reads
* the rune-backed columnSizing atom, so a resize tick re-runs only this
* derived and its single attribute effect; header and data cells reference
* the variables and never update.
*/
const tableStyle = $derived.by(() => {
const parts = ['display: grid']
for (const header of table.getFlatHeaders()) {
parts.push(`--header-${header.id}-size: ${header.getSize()}`)
parts.push(`--col-${header.column.id}-size: ${header.column.getSize()}`)
}
parts.push(`width: ${table.getTotalSize()}px`)
return parts.join('; ')
})
</script>
<div class="demo-root">
<div>
<button onclick={() => refreshData()} class="demo-button">
Regenerate Data
</button>
<button onclick={() => stressTest()} class="demo-button">
Stress Test (5k rows)
</button>
</div>
<div class="spacer-md"></div>
<pre style="height: 10rem; overflow: auto">
{JSON.stringify(table.store.get(), null, 2)}
</pre>
<div class="spacer-md"></div>
({data.length.toLocaleString()} rows)
<div class="scroll-container">
<table style={tableStyle}>
<thead style="display: grid">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<tr style="display: flex; width: 100%; height: 30px">
{#each headerGroup.headers as header (header.id)}
<th
colspan={header.colSpan}
style="display: flex; flex-shrink: 0; width: calc(var(--header-{header.id}-size) * 1px)"
>
{#if !header.isPlaceholder}
<FlexRender {header} />
{/if}
<div
ondblclick={() => header.column.resetSize()}
onmousedown={header.getResizeHandler()}
ontouchstart={header.getResizeHandler()}
aria-hidden="true"
class="resizer {header.column.getIsResizing()
? 'isResizing'
: ''}"
></div>
</th>
{/each}
</tr>
{/each}
</thead>
<tbody style="display: grid">
{#each table.getRowModel().rows as row (row.id)}
<tr
style="display: flex; width: 100%; height: 30px; content-visibility: auto; contain-intrinsic-height: auto 30px"
>
{#each row.getAllCells() as cell (cell.id)}
<td
style="display: flex; flex-shrink: 0; width: calc(var(--col-{cell
.column.id}-size) * 1px)"
>
{cell.renderValue()}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
</div>