6. Going to production
Step 6 of 6 · ← Theme and density
You have a working grid. This page is the checklist that turns it into something you'd ship.
1. Server-side data
For datasets that don't fit in memory, drive the grid from the server.
Pair externalSort + externalFilter with the corresponding callbacks
so the grid records the user's intent but doesn't try to re-order rows
it didn't fetch.
The examples on this page run against these rows:
<script lang="ts">
import { SvGrid, type GridColumns } from '@svgrid/grid'
type Person = {
id: number
name: string
email: string
department: string
age: number
salary: number
city: string
startDate: string
active: boolean
}
const people: Person[] = [
{ id: 1, name: 'Ada Lovelace', email: '[email protected]', department: 'Engineering', age: 36, salary: 142000, city: 'London', startDate: '2021-03-01', active: true },
{ id: 2, name: 'Grace Hopper', email: '[email protected]', department: 'Engineering', age: 45, salary: 168000, city: 'New York', startDate: '2019-07-15', active: true },
{ id: 3, name: 'Linus Torvalds', email: '[email protected]', department: 'Platform', age: 54, salary: 155000, city: 'Portland', startDate: '2020-01-20', active: false },
{ id: 4, name: 'Radia Perlman', email: '[email protected]', department: 'Networking', age: 49, salary: 161000, city: 'Seattle', startDate: '2022-09-05', active: true },
{ id: 5, name: 'Barbara Liskov', email: '[email protected]', department: 'Platform', age: 52, salary: 172000, city: 'Boston', startDate: '2018-11-11', active: true },
]
const columns: GridColumns<Person> = [
{ field: 'name', header: 'Name', width: 200 },
{ field: 'department', header: 'Department', width: 150 },
{ field: 'city', header: 'City', width: 140 },
{ field: 'age', header: 'Age', width: 90 },
{ field: 'salary', header: 'Salary', width: 130, format: { type: 'currency', currency: 'USD' } },
]
</script>
<script lang="ts">
import { SvGrid, tableFeatures, rowSortingFeature,
columnFilteringFeature } from '@svgrid/grid'
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
let sort = $state<Array<{ id: string; desc: boolean }>>([])
let filters = $state<Array<{ id: string; operator: string; value: string }>>([])
let page = $state(0)
const pageSize = 50
let rows = $state<Person[]>([])
let total = $state(0)
let loading = $state(false)
let controller: AbortController | null = null
async function load() {
controller?.abort()
controller = new AbortController()
loading = true
try {
const res = await fetch('/api/people?' + new URLSearchParams({
sort: JSON.stringify(sort),
filters: JSON.stringify(filters),
page: String(page),
size: String(pageSize),
}), { signal: controller.signal })
const body = await res.json()
rows = body.rows
total = body.total
} catch (err) {
if ((err as Error).name !== 'AbortError') throw err
} finally {
loading = false
}
}
$effect(() => { sort; filters; page; load() })
</script>
<SvGrid
data={rows}
columns={columns}
features={features}
filterMode="menu"
externalSort={true}
externalFilter={true}
showPagination={false}
onSortingChange={(next) => { sort = next; page = 0 }}
onFiltersChange={(next) => { filters = next.columns; page = 0 }}
/>
The 09-server-side demo
has the full runnable version with debounce + abort + a 60 ms mock
latency. The server-side guide covers
sparse infinite scroll, velocity-aware chunk loading, and
backpressure.
Open the live example: Server-side data (Server-Side Data)
2. Virtualization for large datasets
For more than ~2k rows, enable row virtualization. For very wide grids (50+ columns) also enable column virtualization. Both are opt-in so small grids don't pay the cost.
<SvGrid
data={rows}
columns={columns}
features={features}
virtualization={true}
columnVirtualization={true}
overscan={8}
columnOverscan={3}
rowHeight={32}
containerHeight={600}
/>
The wrapper's row + column virtualizers handle variable row heights via
the headless createSvelteVirtualizer / createColumnVirtualizer. See
demo 06 for 100k
rows × 100 columns with smooth scroll.
Open the live example: 100k rows × 100 columns (Getting Started)
3. Accessibility
The grid implements the WAI-ARIA 1.2 grid pattern out of the box. Every node has the right role, the active cell carries focus, the keyboard map matches what assistive tech expects.
You don't need to add anything for a baseline accessible grid. To go further:
- Add
aria-liveannouncements for changes outside the grid (filter applied, X rows selected) - see demo 17. - Toggle a high-contrast focus outline for users who need it - also in demo 17.
- Read Accessibility for the WCAG 2.1 AA mapping, forced-colors-mode behaviour, and reduced-motion handling.
Open the live example: Accessibility (Keyboard & Accessibility)
4. SSR-friendly markup
The render component produces meaningful HTML before hydration. In a
SvelteKit +page.server.ts load, the grid's markup hits the browser
already filled with data - first paint shows the table, hydration only
attaches event listeners.
<!-- +page.svelte -->
<script lang="ts">
import { SvGrid, tableFeatures, rowSortingFeature } from '@svgrid/grid'
let { data } = $props()
</script>
<SvGrid
data={data.rows}
columns={columns}
features={tableFeatures({ rowSortingFeature })}
/>
What is and is not in the server HTML:
- In it: the header, and a viewport-sized window of rows with their real cell values. Enough for a crawler to index the content and for a no-JS client to read the table.
- Not in it: rows below that first window. Virtualization survives SSR, so a 5,000-row grid does not serialise 5,000 rows into the page; the rest arrive when the client measures the viewport and takes over.
pnpm ssr:check asserts this against a real generate: 'server' build and runs
in CI. It exists because the grid silently stopped server-rendering rows for a
while: both virtualizers learn their row and column count from an $effect, and
effects never run during SSR, so the server emitted an empty <tbody> while
this page claimed otherwise.
Demo 19 - SSR illustrates the shape
of the pre-hydration markup by snapshotting the rendered grid into a
script-blocked iframe. Note it snapshots the client-rendered DOM, so it shows
what the markup looks like without JS - it does not measure the server output.
pnpm ssr:check is what actually verifies that.
5. Content Security Policy
SvGrid runs cleanly under a strict CSP - no eval, no new Function,
no inline scripts, no inline event handlers. The recommended header:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self' data:;
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
Note: no 'unsafe-eval' and no 'unsafe-inline' on script-src.
Demo 16 - CSP-compliant
runs a live runtime self-check + a violation listener inside a working
grid.
6. TypeScript notes
import type {
ColumnDef,
SvGridApi,
SortingState,
TableFeatures,
} from '@svgrid/grid'
// 1. Constrain ColumnDef to your row type so editors and accessors stay typed.
type Row = { id: string; firstName: string; age: number }
const columns: GridColumns<Row> = [
{ field: 'firstName', header: 'First' }, // OK
{ field: 'middleName', header: 'Mid' }, // ✗ "middleName" not on Row
]
// 2. The api type matches your features + row type.
let api = $state<SvGridApi<typeof features, Row> | null>(null)
The features generic on ColumnDef is the type of the features
object - tableFeatures({ rowSortingFeature }) produces a different
type than tableFeatures({}). Pass typeof features so column-level
inference picks up which capabilities your grid has.
7. What's next
- Why headless? - the layered architecture and when to drop down to the headless core.
- Enterprise features - export, import, AI, pivot.
- Help index - the topic-page catalogue.
- Recipes - 20+ copy-paste patterns.
- How SvGrid compares - what evaluators read.
- Missing features - the honest gap list.
Live examples
- Server-side data - Sort/filter/page round-tripped to a mock endpoint with debounce + cancel.
- 100k rows × 100 columns - Row + column virtualization. Chunked load with progress + cancellation.
- Accessibility - WAI-ARIA grid, keyboard navigation, aria-live announcements, focus toggle.
- Server-side rendering - SvelteKit-style SSR with a sandboxed pre-hydration snapshot.
- CSP-compliant grid - No eval, no inline scripts. Documented CSP header + live runtime self-check.
Related articles
- Going AI-Native: The SvGrid MCP Server - Most AI assistants invent data grid APIs. We built an MCP server so they look up the real one instead.