# sv-grid - full documentation Generated 2026-06-15 from 195 pages. # Changelog The user-facing log of what shipped. Format follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/); versioning follows [Semantic Versioning](https://semver.org/). Every release has up to six sections: - **Added** - new features - **Changed** - existing functionality, behaviour changes - **Deprecated** - soon-to-be-removed features - **Removed** - features removed in this release - **Fixed** - bug fixes - **Security** - vulnerability patches (with CVE ids when applicable) Pre-release entries live under `## [Unreleased]`. They graduate to a dated heading on publish via `pnpm changeset version` (see [.changeset/README.md](../.changeset/README.md)). For machine-readable releases, fetch [`/changelog.json`](/changelog.json) - same content, parseable shape. ## [Unreleased] ### sv-grid-community - **New** `onCellValueChange` callback on ``. Fires after every inline edit commits with `{ rowIndex, columnId, oldValue, newValue, row }`. The recommended hook for server-side persistence and cascading recomputes; see demo `18-cascade-editing` and [Saving values](./help/editing/saving-values.md). - **New** `externalSort` + `externalFilter` props. Grid records the UI state but does NOT re-order / filter rows - the consumer owns the pipeline. Paired with `onSortingChange` / `onFiltersChange` for server-side data and tree-data scenarios. - **New** `onSortingChange(sorting)` / `onFiltersChange(filters)` callbacks. Fire on every change with the consolidated payload shapes documented in the [SvGrid reference](./reference/SvGrid.md). - **New** `onRowSelectionChange(selection, rows)` and `onActiveCellChange(cell)` callbacks. - **New** `fitColumns` prop. Scales column widths to fill the viewport, with rounding-residue absorbed in the last scalable column. Shrinks down to 85 % of natural widths; beyond that a horizontal scrollbar appears. - **New** `showRowNumbers` prop. Leading 1-based row-number column, rendered before any selection column. - **New** `pageSize` prop on ``. Initial page size for the built-in pager. - **Renamed** `accessorKey` → `field` on `ColumnDef`. **Breaking**. Bulk-rename in your column defs: `accessorKey` → `field`. The identifier is the only change; semantics are identical. - **Improved** column virtualizer now detects per-item size changes, not just total size. Fixes a regression where resizing a column under `fitColumns` left other columns stale. - **Improved** the wrapper-managed filter pipeline no longer applies filters to the pre-paginated view - the filter UI always sees the full dataset, not just the visible page. (Removed `paginatedRowModel` from the engine pipeline; pagination is applied AFTER filters by the wrapper.) - **Renamed package** `svgrid-community` → `sv-grid-community`. **Breaking**. Update imports: `from 'svgrid-community'` → `from 'sv-grid-community'`. Subpath imports follow the same change. ### sv-grid-pro - **New** package. The paid feature pack adds: - **Export** to xlsx / pdf / csv / tsv / html, with theme-matched styles, multi-sheet workbooks, header + footer with logo, embedded cell images. See [Export](./help/export.md). - **Print** with repeat-on-page headers, optional cover page, page-size + orientation. - **Import** from xlsx / csv / tsv / json with column mapping + per-row validation. See [Import](./help/import.md). - **AI assistant**: provider-agnostic helpers for natural-language filter, smart fill, summarize, and classify. Bring your own model adapter. See [AI](./help/ai.md). - **Pivot tables** via `createPivotModel(data, config)` and `pro.pivot.build(config)`. Row + column axes, 8 built-in aggregators or custom, grand-total row + column, subtotals, custom axis sort. See [Pivot tables](./help/pivot.md). - **New** `installPro(api)` augments a `SvGridApi` with `exportData`, `print`, `importData`, `ai.*`, and `pivot.*` methods. - **Soft-gated licensing**: features run unlicensed but the grid shows a small watermark + a one-time console nudge. `setLicenseKey('SVPRO-…')` at app startup clears both. ### Examples gallery - **New demos** (56-59): theme-matched export, branded export with header + footer + logo, export with cell images, multi-sheet export. - **New demos** (52-pivot-table, 51-ai-assistant, 53-excel-import): reference implementations for each Pro feature. - **New demos** for industry verticals: stock market (live ticks), HR team, finances ledger, industrial IoT, localization, CSP compliant, accessibility, cascade editing, server-side rendering, industrial dashboard, healthcare EMR, logistics, compliance queue, field service, gantt, scheduler, CRM, admin dashboard, seller panel. - **New** "Source" button on every demo opens the raw `.svelte` file in a modal with a Copy-to-clipboard button. Wired via `import.meta.glob('../demos/*.svelte', { query: '?raw' })` so it picks up new demos automatically. ### Docs - **New** [Pro feature pack](./pro/README.md) landing page. - **New** [API reference](./reference/index.md) with hand-curated pages for ``, `SvGridApi`, `ColumnDef`, features, and the full Pro surface. - **New** [Why headless?](./why-headless.md) explains the layered architecture and when to drop down to the headless core. - **New** [Tailwind integration](./help/tailwind.md) walks the `--sg-*` token surface, dark-mode wiring via `data-theme`, and the override hooks for the stable `.sv-grid-*` class names. - **Split** [Getting started](./getting-started.md) into six short pages (install / first-grid / data-and-columns / features / theme-and-density / going-to-production); the old single-page version is preserved at [getting-started-full.md](./getting-started-full.md). - **Cleaned** every code sample against the live library surface. Removed phantom APIs: `state={...}`, `rowModels={...}`, `initialState={...}`, `manualFiltering`, `manualSorting`, `manualPagination`, `onColumnFiltersChange`, the wrapper `getRowId` prop, per-column `enableSorting` / `enableColumnFilter` / `enableGrouping` flags, the never-shipped `sv-grid-community/themes/default.css` import. - **Em-dashes globally swept to hyphens** (`-` → `-`) across all source-controlled text. Codified as a rule for new content. ## How we version | Tier | What it means | | ----------------- | -------------------------------------------------------------------------------------------------------------- | | **Major** (1.x) | Breaking change to a Stable API. Includes prop / method renames, type-narrowing, default-behaviour changes. | | **Minor** | New Stable API. New Experimental API. New demo. Doc rewrites that touch the public-facing claims. | | **Patch** | Bug fixes. Type-only fixes. Internal refactors with no public surface change. Doc typo fixes. | The [API stability page](./help/api-stability.md) annotates each export with its current tier (`Stable`, `Experimental`, `Internal`). Until 1.0, treat anything not flagged `Stable` as subject to change. # Audit log integration A typed, tamper-evident audit trail of every user action in the grid, in under 50 lines. Pattern works for any backend - your existing audit table, Datadog logs, an immutable ledger (DynamoDB with conditional writes, AWS QLDB, Kafka), or a write-once S3 bucket. > Live in [demo 35 (Permissions, audit & history)](https://svgrid.com/#/demos/35-permissions-audit) > and [demo 49 (Admin dashboard)](https://svgrid.com/#/demos/49-admin-dashboard). ## What's in scope Every event the grid surfaces: | Event | Grid callback | Audit record | | --------------- | -------------------------------------- | --------------------------------------------------------- | | Cell edit | `onCellValueChange` | `{actor, resource, before, after, ts}` | | Row selection | `onRowSelectionChange` | `{actor, selectedIds, ts}` | | Filter change | `onFiltersChange` | `{actor, filters, ts}` | | Sort change | `onSortingChange` | `{actor, sort, ts}` | | Group change | `onGroupingChange` | `{actor, groupBy, ts}` | | Bulk row delete | wrap `api.removeRows(...)` | `{actor, deletedIds, ts}` | | Bulk import | wrap `api.addRows(...)` after smart-paste | `{actor, addedIds, source: 'paste', ts}` | | Export | wrap `api.exportData(...)` | `{actor, format, rowCount, columns, ts}` | ## Minimal implementation ```svelte audit({ actor: currentUser.id, action: 'cell.edit', resource: `patient/${e.row.id}/${e.columnId}`, before: e.oldValue, after: e.newValue, })} onApiReady={(next) => (api = next)} /> ``` For the `api.removeRows / addRows / exportData` actions, wrap each imperative call site: ```ts async function deleteSelected() { if (!api) return const ids = selectedRows.map((r) => r.id) api.removeRows(ids) await audit({ actor: currentUser.id, action: 'row.delete', resource: `patient/${ids.join(',')}` }) } ``` ## Tamper-evident pattern (SHA-256 chain) When a "trust but verify" auditor wants to confirm the log wasn't edited in the database, append the SHA-256 of the previous record to each new one. Cheap, no extra service needed: ```ts async function append(rec: AuditRecord) { const prev = await fetch('/api/audit/latest-hash').then((r) => r.text()) const enc = new TextEncoder() const payload = JSON.stringify({ ...rec, prevHash: prev }) const hashBuf = await crypto.subtle.digest('SHA-256', enc.encode(payload)) const hash = Array.from(new Uint8Array(hashBuf)).map((b) => b.toString(16).padStart(2, '0')).join('') await fetch('/api/audit', { method: 'POST', body: JSON.stringify({ ...rec, prevHash: prev, hash }), }) } ``` Now any audit-log tampering breaks the chain - one Merkle-style verify scan over the log catches it. ## Buffer + flush pattern (production-grade) Fire-and-forget HTTP per edit is fine for low-traffic admin apps; for a heavy editor (spreadsheet-style work) buffer and flush: ```ts const buf: AuditRecord[] = [] let flushTimer: number | null = null function buffer(rec: Omit) { buf.push({ ...rec, ts: new Date().toISOString() }) if (flushTimer === null) { flushTimer = window.setTimeout(flush, 2000) } } async function flush() { flushTimer = null if (buf.length === 0) return const batch = buf.splice(0) try { await fetch('/api/audit/batch', { method: 'POST', body: JSON.stringify(batch) }) } catch { buf.unshift(...batch) // retry next tick } } window.addEventListener('beforeunload', flush) ``` The `beforeunload` flush is the trick: even if the user closes the tab mid-session, the buffer ships. ## What an auditor wants to see When demonstrating the audit trail to an external auditor, the four demonstrations: 1. **End-to-end edit trace.** Open a row, edit the salary field, show the audit record in your backend within 5s. 2. **Tamper detection.** SQL-update a row in the audit table to a different value; show that the SHA chain breaks. 3. **Role gating.** Log in as a viewer (no edit rights); attempt to edit; show the audit record is `{action: 'edit.denied', reason: 'rbac'}`. 4. **Retention.** Show your retention policy: "audit records older than 7 years are archived to S3 with object-lock; nothing is ever deleted." ## See also - [Observability](../help/observability.md) - the broader callback surface - [SOC 2 posture](./soc2.md) - [HIPAA posture](./hipaa.md) - [Demo 35 - Permissions, audit & history](https://svgrid.com/#/demos/35-permissions-audit) # GDPR + data residency sv-grid is **GDPR-neutral**: the library never transmits personal data, never stores it server-side, and never logs it. If your app is GDPR-compliant before adding sv-grid, adding sv-grid does not break that compliance. That said, GDPR reviewers ask the same five questions every time - this page answers each one. ## 1. Where does data live? In your application's browser memory, for the duration of the user's session. The grid never holds a copy outside what's in the `data` prop. **Exceptions you opt into:** | Feature | What gets written | | ---------------------------------------------------- | ------------------------------------------------------------------ | | [Saved views](../help/saved-views.md) | View name + column widths + filter / sort state. Not the rows. | | [State maintenance](../help/state-maintenance.md) | Same - state only, never data, unless you call `setData(...)`. | | [Clipboard copy/paste](../help/state-maintenance.md) | When the user explicitly copies, the row values land in the OS clipboard. | To audit: run your app with DevTools → Application → Local Storage. With saved-views disabled, sv-grid writes nothing. ## 2. Does the library transmit personal data? No. The library has **zero outbound network calls**. Inspect with DevTools → Network: nothing from sv-grid appears. Server-side adapters (your `onFiltersChange` / `onSortingChange` handlers) ARE outbound calls, but they're code YOU wrote. The grid just notifies you of state changes; what you do next is your code. ## 3. How do we honour data-subject requests? GDPR Articles 15-22 (access, rectification, erasure, portability, restriction, objection, automated decision-making) all apply to your DATABASE, not the grid. The grid is purely a view layer. The one nuance: if you use [saved views](../help/saved-views.md) to persist user-specific layouts, those layouts may count as personal data under Article 4(1). Two safe responses: ```ts // Article 15: provide a copy. const allViews = JSON.parse(localStorage.getItem('svgrid:views') ?? '{}') // Article 17: forget. localStorage.removeItem('svgrid:views') api.clearAllFilters() api.setColumnPinning({ left: [], right: [] }) ``` If your saved views live server-side (the recommended pattern for multi-device users), your existing data-subject endpoint covers them. ## 4. Does the library use cookies or fingerprinting? No. The library: - Sets no cookies. - Reads no cookies. - Does not call any fingerprinting API (`screen.width`, `navigator.platform`, `canvas.toDataURL` etc.) at runtime for the purpose of tracking. The PDF/xlsx exporters use `canvas.toDataURL` to rasterise SVG thumbnails for embedding - that's the only canvas use anywhere, and it happens locally for the export only. ## 5. Where are the docs hosted? Where is the npm registry? | Surface | Hosted at | | ------------------------ | ----------------------------------------------- | | Doc site (`svgrid.com`) | Vercel (US, EU edge) | | Source repo | GitHub | | npm packages | npmjs.com | | MCP server | Your machine (runs locally) | If your data-residency policy requires EU-only origins, all of these can be replaced: - Mirror the docs internally (the `docs/` folder is in the repo). - Use npmjs.eu or your private registry. - The MCP server has no upstream calls; it runs against your installed copy of `node_modules`. ## Sub-processors The library has none. There is no sv-grid service. For the doc site at `svgrid.com` we use Vercel + GitHub OAuth (for the "Edit on GitHub" link only) - these are not sub-processors of your app, just of the doc site. ## Data-protection impact assessments A DPIA is required when processing creates "high risk to rights and freedoms". The library itself doesn't process data; your app does. The risk profile of using sv-grid vs hand-writing a `` is **identical** - both display the data your app already has. ## See also - [SOC 2 posture](./soc2.md) - [HIPAA posture](./hipaa.md) - tighter rules for healthcare PHI - [Security & supply chain](../help/security.md) - [Saved views](../help/saved-views.md) - the one feature that writes to localStorage # HIPAA posture sv-grid is **HIPAA-neutral**: the library does not transmit, store, or process PHI on its own. If your app is HIPAA-compliant before adding sv-grid, adding sv-grid does not break that compliance. This page documents the four configuration choices that matter for healthcare deployments. > **Live example:** [demo 41 (Healthcare EMR - inpatient board)](https://svgrid.com/#/demos/41-healthcare-emr) > shows a role-based ICU census - the same patterns this page describes. ## 1. Disable persistent saved-views The default `` writes nothing to disk. Saved views (an opt-in feature) writes view layouts to localStorage. If you don't want PHI to ever land in localStorage, the simplest answer is: don't use saved views, OR make sure the view payload contains no PHI. ```ts // Save the LAYOUT but never the filter VALUES (which could leak PHI // like 'Patient: Jane Doe'). const view = { widths: api.getColumnWidths(), pinning: api.getColumnPinning(), // intentionally do NOT save api.getFilters() } localStorage.setItem('view', JSON.stringify(view)) ``` ## 2. Disable clipboard copy/paste for PHI columns The grid's copy/paste serialises selected cells to the OS clipboard as TSV. Clipboards are not under your app's control - browsers, extensions, OS-level sync (e.g. Apple Universal Clipboard) all read from them. The simplest mitigation: drop the cell-selection feature, which removes the keyboard surface that triggers copy: ```svelte ``` For finer control, intercept the `onActiveCellChange` callback and block the column you don't want copyable - see [accessibility](../help/accessibility.md#keyboard-map) for the keyboard map. ## 3. Disable AI helpers for PHI rows (or route to a HIPAA-BAA provider) The [AI assistant](../help/ai.md) helpers (`aiFilter`, `aiSmartFill`, `aiSummarize`, `aiClassify`) send row data to whatever model provider you registered via `setAIProvider(...)`. For PHI: - Use a model hosted on a HIPAA-eligible service (AWS Bedrock with Anthropic / Cohere / Mistral; Azure OpenAI with a signed BAA; Google Vertex AI with a signed BAA). - OR redact PHI fields BEFORE calling the helper. Example redaction in the provider adapter: ```ts import { setAIProvider, type AIProvider } from 'sv-grid-pro' const redactingProvider: AIProvider = async ({ prompt, ...rest }) => { const safe = prompt.replace(/MRN-?\d{8}/g, '<>').replace(/SSN[:\s-]*\d{3}-?\d{2}-?\d{4}/g, '<>') return fetch('/api/ai', { ... body: JSON.stringify({ prompt: safe, ...rest }) }).then((r) => r.text()) } setAIProvider(redactingProvider) ``` The library hands the prompt to the provider VERBATIM - any redaction happens in the wrapper you author, where it's auditable. ## 4. Audit every cell edit Most HIPAA reviewers care more about the audit trail than the display surface. The grid's `onCellValueChange` fires on every committed edit with `{ rowIndex, columnId, oldValue, newValue, row }`. Pipe it to your audit pipeline: ```svelte { await fetch('/api/audit', { method: 'POST', body: JSON.stringify({ actor: currentUser.id, action: 'cell-edit', resource: `patient/${e.row.id}/${e.columnId}`, before: e.oldValue, after: e.newValue, ts: new Date().toISOString(), }), }) }} /> ``` See [audit log integration](./audit-log.md) for the full pattern, including a CryptoSign-the-row trick that makes the audit log tamper-evident. ## What the library does NOT do - It does not transmit row data anywhere on its own. - It does not store row data on disk. - It does not phone home with telemetry. - It does not log to the console at runtime in production builds. You can confirm with DevTools: `` produces zero network requests of its own. ## Browser-level mitigations to know about These are general HIPAA-in-the-browser concerns; the grid doesn't make them worse, but you should be aware: - **Browser auto-fill**: disable on PHI fields with `autocomplete="off"`. - **Browser screenshot APIs** (`getDisplayMedia`): browsers ask the user; your app can't fully block. Combine with `Cache-Control: no-store` on your origin so screenshots don't end up in browser history thumbnails. - **Browser back-forward cache** caches the DOM. Use `Cache-Control: no-store` and `Pragma: no-cache` on PHI pages. ## See also - [SOC 2 posture](./soc2.md) - [GDPR + data residency](./gdpr.md) - [Audit log integration](./audit-log.md) - [Security & supply chain](../help/security.md) - [Demo 41 - Healthcare EMR](https://svgrid.com/#/demos/41-healthcare-emr) - role-based cell editing in practice # Compliance sv-grid is a client-side UI library - **all data stays in the browser**, the library never makes a network call of its own, no telemetry phones home. The compliance story is therefore short, but because enterprise procurement asks the same questions every time, this section answers each one directly. > If your reviewer wants a one-pager: jump to the > [vendor-questionnaire shortlist](#vendor-questionnaire-shortlist) > at the bottom. ## Pages - [SOC 2 posture](./soc2.md) - what the library covers, what your hosting / build pipeline must cover - [GDPR + data residency](./gdpr.md) - personal-data handling, where data physically sits, the user-rights surface - [HIPAA posture](./hipaa.md) - PHI handling in the browser, what "no PHI on disk" requires you to wire - [Audit log integration](./audit-log.md) - turn the grid's callbacks into an immutable audit trail with one adapter ## Vendor-questionnaire shortlist | Question | Answer | | ------------------------------------------------- | --------------------------------------------------------------- | | Does the library transmit any data? | **No.** Zero outbound network calls. Inspect with DevTools. | | Does the library write to localStorage? | **Only when you opt in.** [Saved views](../help/saved-views.md) writes when you tell it to. | | Does the library evaluate user input as code? | **No.** CSP-compliant; no `eval` / `new Function`. | | Does the library include third-party trackers? | **No.** Verify the bundle - 49 kB gzip, no analytics SDK. | | Is the library SOC 2 / ISO 27001 certified? | The LIBRARY can't be certified - it's not a service. Your hosted app gets certified; the library is in-scope as a dependency. See [SOC 2 posture](./soc2.md). | | Is the library GDPR-compliant? | The library is GDPR-neutral: it never processes data the user didn't already see. See [GDPR + data residency](./gdpr.md). | | Is the library HIPAA-compliant? | Same: HIPAA-neutral. PHI handling is a property of your app, not the grid. See [HIPAA posture](./hipaa.md). | | Is the source code auditable? | **Yes.** MIT-licensed; published as readable source (no minified obfuscation). | | Where is data stored? | **In your app's memory.** Never on a sv-grid server. There is no sv-grid server. | | Is there a security disclosure policy? | Yes - email `support@jqwidgets.com`. Patches typically ship within 7 days for high-severity issues. | | Is the library tested for accessibility? | Yes - WAI-ARIA 1.2 grid pattern + axe-core in CI. See [accessibility](../help/accessibility.md). | | Are dependencies vetted? | Yes - 0 runtime dependencies in `sv-grid-community`. `sv-grid-pro` lazy-loads `jszip` + `pdfmake` as peers. See [security](../help/security.md) for the dep table. | | Is there an SBOM? | Yes - `pnpm run sbom` emits CycloneDX 1.5. See [security](../help/security.md#sbom-generation). | ## See also - [Security & supply chain](../help/security.md) - the parent posture - [Observability](../help/observability.md) - the audit log seam - [API stability](../help/api-stability.md) - the deprecation promise # SOC 2 posture sv-grid is a UI library, not a service - so it cannot itself hold a SOC 2 report. But it can sit inside a SOC 2-audited application, and the controls below describe the line where the library's responsibility ends and yours begins. ## TL;DR for procurement > sv-grid is a client-side JavaScript library shipped as MIT-licensed > source. It performs no network IO, holds no user data, and has no > backend. SOC 2 audit scope applies to your hosting and build > pipeline, not the library. We give you the inputs (SBOM, security > disclosure policy, deterministic builds) you need to include > sv-grid in your own SOC 2 report. ## What the library guarantees | SOC 2 control area | Library covers | | --------------------- | --------------------------------------------------------------- | | **CC6.1 Logical access** | n/a - no service to log into | | **CC6.6 Encryption in transit** | n/a - no transit | | **CC7.1 System operations** | Bundle is deterministic; SHA-256 published per release | | **CC8.1 Change management** | Every change ships as a PR with reviews; release notes per version | | **CC9.2 Vendor management** | 0 runtime deps in `sv-grid-community`; lazy peer deps in `sv-grid-pro` documented in [security](../help/security.md) | ## What you cover (in your own SOC 2) | Control area | Your responsibility | | --------------------- | --------------------------------------------------------------- | | Hosting | Wherever your app is served from (Vercel / Cloudflare / your own infra) | | User authentication | Your app's auth layer - sv-grid never sees credentials | | Data at rest | Wherever your data sits BEFORE it reaches the grid | | Audit logging | Wire `onCellValueChange` etc. to your audit pipeline - see [audit log](./audit-log.md) | | Backup / restore | Your DB - the grid is stateless | | Incident response | Your SRE process | ## Inputs we provide to your auditor 1. **MIT licence** - vetted by your legal team once, valid forever 2. **Public source code** - no obfuscation; your auditor can read every line 3. **Published SBOM** - CycloneDX 1.5 generated per release; tracks every direct + transitive dep 4. **Security disclosure policy** - email `support@jqwidgets.com`, GPG fingerprint published, response SLA documented in [security](../help/security.md) 5. **Vulnerability history** - every CVE attributed to sv-grid published in the [changelog](../changelog.md) with disclosure date, fix version, mitigation 6. **Deterministic builds** - reproducible `dist/` from a clean clone; the SHA-256 of each release artefact is published in the GitHub release notes 7. **Code-review evidence** - every commit signed; every PR requires review from a CODEOWNER 8. **Dependency review** - Renovate bot opens a PR within 24h of any upstream release; we review and tag ## Common auditor questions > *"Is there a SOC 2 report for sv-grid?"* No - it would be meaningless. There's no service. The library is a dependency of your application, the same way React or Svelte is. Your auditor will treat it as a dependency, in scope under CC9.2. > *"Does sv-grid have access to our data?"* No. The library runs in the user's browser. Your data is whatever your app hands to the `` prop. We never see it. > *"Can we self-host the docs?"* Yes - the entire `docs/` folder is in the repo. Clone, build, host behind your VPN if your compliance regime requires it. The [MCP server](../help/mcp-server.md) runs locally too. > *"What happens if a CVE is found in sv-grid?"* Triage within 24h. High-severity patches typically ship within 7 days. Subscribers to the GitHub release notifications get the release tag the moment we cut it. We backport security fixes to the last 2 minor versions; see [api-stability](../help/api-stability.md) for the support window. ## See also - [GDPR + data residency](./gdpr.md) - [Security & supply chain](../help/security.md) - SBOM, signing, dep table - [Audit log integration](./audit-log.md) - turn callbacks into audit events # Getting Started with SvGrid SvGrid is a modern, production-ready data grid for Svelte 5 - a headless core engine paired with a Svelte render component (``). It scales from a 10-row read-only table to a virtualized 100,000-row, 100-column editing surface with grouping, multi-column filtering, server-side data, and full keyboard and screen-reader support. This page walks you from `pnpm add` to a feature-complete grid. It is the canonical entry point - every other page in the documentation assumes you've finished this one. Estimated reading time: 15 minutes. > **New here?** Two short companion reads: > > - [Why headless?](./why-headless.md) - the architecture decision > behind the `createSvGrid` core vs. the `` renderer. > - [Tailwind integration](./help/tailwind.md) - how `--sg-*` custom > properties + Tailwind v4 + dark mode fit together. > `sv-grid-community` is published under the **MIT License** - permissive > for commercial use, redistribution, and modification. The paid companion > `sv-grid-pro` (data export + print) ships under a separate commercial > license. See [LICENSE](../LICENSE) and > [packages/sv-grid-pro/LICENSE](../packages/sv-grid-pro/LICENSE). --- ## Contents 1. [Your first grid in 60 seconds](#1-your-first-grid-in-60-seconds) 2. [Install the package](#2-install-the-package) 3. [Provide row data](#3-provide-row-data) 4. [Define column definitions](#4-define-column-definitions) 5. [Register features (row models)](#5-register-features-row-models) 6. [Styling: theme, density, dark mode](#6-styling-theme-density-dark-mode) 7. [Sizing the grid](#7-sizing-the-grid) 8. [Custom cells with FlexRender](#8-custom-cells-with-flexrender) 9. [Sorting, filtering, pagination](#9-sorting-filtering-pagination) 10. [Selection, editing, keyboard](#10-selection-editing-keyboard) 11. [Server-side data](#11-server-side-data) 12. [Virtualization for large datasets](#12-virtualization-for-large-datasets) 13. [Accessibility](#13-accessibility) 14. [TypeScript notes](#14-typescript-notes) 15. [What's next](#15-whats-next) --- ## 1. Your first grid in 60 seconds ```svelte ``` That's a complete, working grid. The rest of this page is about turning it into something you'd ship. --- ## 2. Install the package SvGrid is a single npm package. There is no peer dependency on a CSS framework - bring your own, or use the bundled stylesheet. ```bash # pnpm (recommended) pnpm add sv-grid-community # npm npm install sv-grid-community # yarn yarn add sv-grid-community ``` **Requirements.** - Svelte **5.x** (uses runes - `$state`, `$derived`, `$effect`). - TypeScript **5.4+** (optional but recommended). - Node **18+** for tooling. Once installed, import the component, the features you want, and the matching `ColumnDef` type: ```ts import { SvGrid, tableFeatures, rowSortingFeature, columnFilteringFeature, type ColumnDef, } from 'sv-grid-community' ``` The bundle is tree-shakeable - features you don't import don't ship. The default render component (``) brings its own scoped CSS, so there's no separate stylesheet to import. Re-theming happens via the `--sg-*` custom-property surface; see [Tailwind integration](./help/tailwind.md) for the full list. --- ## 3. Provide row data SvGrid is data-agnostic. The `data` prop is any `ReadonlyArray` - a Svelte 5 `$state` array, a derived store, an SWR/React-query-style cache, the result of a `+page.ts` load function, or a plain literal. ```svelte ``` **Identity.** Today the wrapper uses the row's array index as its id. That is fine for read-only data; if you mutate `rows`, prefer keeping the same object references for rows that didn't change so selection and edit state line up. A `getRowId` prop on the wrapper is tracked in [Missing features](./help/missing-features.md) and supported by the headless `createSvGrid` core today. **Immutability.** SvGrid never mutates your data. When you edit a cell the grid emits an event; you decide whether to mutate in place or copy. See [§10 - Editing](#10-selection-editing-keyboard). --- ## 4. Define column definitions A column definition tells SvGrid how to read a value out of a row, how to render it, and which features apply to it. ```ts import type { ColumnDef } from 'sv-grid-community' type Person = { id: string firstName: string lastName: string age: number joinedAt: string // ISO date salary: number active: boolean } const columns: ColumnDef<{}, Person>[] = [ // Simple accessor by key { field: 'firstName', header: 'First name' }, // Computed accessor { id: 'fullName', header: 'Full name', accessorFn: (row) => `${row.firstName} ${row.lastName}`, }, // Numeric with locale-aware formatting { field: 'age', header: 'Age', format: { type: 'number', options: { maximumFractionDigits: 0 } }, }, // Date with explicit pattern { field: 'joinedAt', header: 'Joined', format: { type: 'date', pattern: 'y-m-d' }, }, // Currency { field: 'salary', header: 'Salary', format: { type: 'currency', currency: 'USD' }, }, // Boolean rendered as a checkbox { field: 'active', header: 'Active', editorType: 'checkbox', }, ] ``` **Common properties.** | Property | Purpose | | --- | --- | | `field` | Reads `row[key]`. | | `accessorFn` | Computes the value from the row. | | `id` | Stable column id (required if you use `accessorFn`). | | `header` | String or render snippet for the header. | | `footer` | String or render snippet for the footer row. | | `cell` | Render snippet/component for the body cell. | | `format` | Locale-aware formatter (`number`, `currency`, `percent`, `date`). | | `formatter` | Function for one-off custom value formatting. | | `editorType` | Inline editor: `text` \| `number` \| `checkbox` \| `date` \| `datetime`. | | `width` | Initial column width in pixels (default `columnWidth` prop). | | `align` | Header + body alignment: `'left'` \| `'right'` \| `'center'`. Inferred from `editorType` when omitted. | | `columns` | Child column defs (for column groups). | Sorting / filtering / grouping are toggled per-grid via the registered features - there is no per-column `enableSorting` / `enableColumnFilter` flag yet; those entries are in [Missing features](./help/missing-features.md). See [`packages/sv-grid-community/src/core.ts`](../packages/sv-grid-community/src/core.ts) for the full type. --- ## 5. Register features (row models) The grid engine is feature-gated. Out of the box you get the **core row model** (the rows in their original order). To enable sorting, filtering, grouping, expansion, pagination, or selection you opt in with `tableFeatures(...)` and the matching `create*RowModel` factory. ```svelte ``` **Rule of thumb.** Only register the features you use. The wrapper wires the matching row-model pipeline (core → filtered → sorted → grouped → expanded) for you and exposes the user-facing toggles via props (`showPagination`, `filterMode`, `showRowSelection`, …). If you need the headless pipeline directly - e.g. a custom renderer - drop down to `createSvGrid` from the same package; see [Why headless?](./why-headless.md). | Feature | Factory | What it does | | --- | --- | --- | | `rowSortingFeature` | `createSortedRowModel` | Click headers to sort; shift-click for multi-sort. | | `columnFilteringFeature` | `createFilteredRowModel` | Per-column filters with built-in `filterFns`. | | `rowPaginationFeature` | `createPaginatedRowModel` | Page slicing + footer state. | | `rowSelectionFeature` | - | Row checkboxes, range selection, headless API. | | `columnGroupingFeature` | `createGroupedRowModel` | Group-by-column + aggregators. | | `rowExpandingFeature` | `createExpandedRowModel` | Tree / master-detail expansion. | --- ## 6. Styling: theme, density, dark mode SvGrid's render component (``) ships its own scoped styles. You re-theme it by declaring `--sg-*` custom properties at any level above the grid - the included light, dark, and high-contrast palettes in the [`10-custom-cells-and-themes`](../examples/src/demos/10-custom-cells-and-themes.svelte) demo are themselves just three sets of these tokens applied via `style="--sg-bg: …; --sg-fg: …; …"`. **Customising tokens.** Override at any level - `:root`, a wrapper, or directly on ``. ```css :root { --sg-row-height: 36px; --sg-header-bg: #f6f7f9; --sg-header-fg: #1f2933; --sg-row-hover-bg: #eef2ff; --sg-selection-bg: #dbeafe; --sg-border: #e5e7eb; --sg-focus-ring: 0 0 0 2px #2563eb; --sg-font: 'Inter', system-ui, sans-serif; } @media (prefers-color-scheme: dark) { :root { --sg-header-bg: #0f172a; --sg-header-fg: #f1f5f9; --sg-row-hover-bg: #1e293b; --sg-border: #334155; } } ``` **Density.** The default theme reads `--sg-row-height`; flip it to `28px` for compact mode and `48px` for comfortable. Density changes are applied without remounting the virtualizer. **Reduced motion.** Sort animations and expand transitions respect `prefers-reduced-motion: reduce` automatically. --- ## 7. Sizing the grid `` fills its parent. Give it a height and it scrolls - without one, it expands to its content and never virtualises. ```svelte
``` **Auto-height (small datasets only).** For grids with fewer than ~200 rows you can let the grid grow to its content: ```svelte ``` Auto-height disables row virtualization. Don't use it for large data. --- ## 8. Custom cells with FlexRender For anything beyond a stringified value, render with `FlexRender`, `renderComponent`, or `renderSnippet`. ### As a Svelte snippet ```svelte {#snippet StatusCell({ value }: { value: string })} {value} {/snippet} ``` ### As a Svelte component ```ts import StatusBadge from './StatusBadge.svelte' import { renderComponent } from 'sv-grid-community' const columns = [ { field: 'status', header: 'Status', cell: renderComponent(StatusBadge, (ctx) => ({ status: ctx.getValue() })), }, ] ``` `renderComponent` and `renderSnippet` both receive a `CellContext` so you can read sibling values, mutate state, or call back into the grid via `ctx.table`. --- ## 9. Sorting, filtering, pagination Once their features are registered (see §5) the UI affordances appear automatically. The state is controllable. **Quick way - capability shortcuts.** Every capability is off by default; the fastest way to opt in is a boolean shortcut prop, no feature constants required. `sortable` and `filterable` inject the matching feature for you; `editable`, `groupable`, and `pageable` alias `enableInlineEditing`, `showGroupingControls`, and `showPagination`: ```svelte ``` Reach for the explicit `features` set + fine-grained props below when you need more control (filter mode, page size, per-column opt-outs). ### Uncontrolled (the default) The wrapper owns sort, filter, pagination, selection, and expansion state by default. Set the initial page size and which filter UI to show via props: ```svelte ``` ### Observable (callbacks fire when state changes) The wrapper still owns the state, but emits callbacks on every change. Use this when an outside piece of UI needs to react (a "X rows selected" pill, a router that syncs sort to the URL, a server fetch). ```svelte (sorting = next)} onFiltersChange={(next) => (filters = next.columns)} /> ``` ### External (you own row ordering / filtering) For server-side data or tree-structured data the wrapper records the sort + filter UI state but does **not** re-order the rows - you do. Pair `externalSort` / `externalFilter` with the callbacks above: ```svelte fetchPage({ sort: next, page: 0 })} onFiltersChange={(next) => fetchPage({ filters: next.columns, page: 0 })} /> ``` For Excel-style filter operators and the active-filter chip UI, see [`applyExcelFilter`](../packages/sv-grid-community/src/filtering/excel-filters.ts). --- ## 10. Selection, editing, keyboard ### Row selection ```svelte (selected = rows)} /> {#if selected.length}

{selected.length} selected

{/if} ``` `selectionMode` is the umbrella prop: `'row'` shows the checkbox column, `'cell'` enables click-and-drag range selection, `'both'` (default) enables both, `'none'` disables both. The `onRowSelectionChange` callback receives the selection record AND the materialised row array. ### Cell editing Set `editorType` on each editable column. The grid handles entry, commit, and cancel; you handle persistence. ```svelte row.id} onCellValueChange={handleCellEdit} /> ``` ### Keyboard The grid follows the WAI-ARIA grid pattern: | Keys | Action | | --- | --- | | `←` `↑` `→` `↓` | Move active cell | | `Home` / `End` | First / last column of the row | | `Ctrl+Home` / `Ctrl+End` | First / last cell of the grid | | `PageUp` / `PageDown` | Move one viewport | | `Shift + ` | Extend cell-range selection | | `Space` | Toggle row selection (when selection enabled) | | `Enter` / `F2` | Begin editing the active cell | | `Esc` | Cancel edit / clear selection | | `Ctrl/Cmd + C` | Copy selection as TSV | | `Ctrl/Cmd + V` | Paste TSV into selection | If you implement your own header or toolbar, route keys through `getKeyboardIntent` and `getNextActiveCell` so behaviour stays consistent. --- ## 11. Server-side data For datasets that don't fit in memory, drive the grid from the server. The pattern is: turn the wrapper's `onSortingChange` / `onFiltersChange` callbacks into a query, fetch, hand the page back as `data`, and use the `externalSort` + `externalFilter` props so the grid doesn't try to re-order rows it didn't fetch. ```svelte { sort = next; page = 0 }} onFiltersChange={(next) => { filters = next.columns; page = 0 }} /> {#if loading}
Loading…
{/if} ``` The `external*` props tell the grid not to re-derive that dimension locally - the data you pass in is already the answer. Pagination above is hand-rolled so total-row-count and "show next page" stay in your control; if a built-in pager is enough, leave `showPagination={true}` on and the wrapper will page the local `rows` array (which, in this server-side pattern, only ever holds one page anyway). See the [`09-server-side` demo](../examples/src/demos/09-server-side.svelte) for a complete runnable version with debounce, abort wiring, and a 60 ms mock latency. --- ## 12. Virtualization for large datasets For more than a few thousand 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. ```svelte ``` For full control (e.g. variable row heights, programmatic scroll), use the headless virtualizer directly: ```ts import { createSvelteVirtualizer } from 'sv-grid-community' const virtualizer = createSvelteVirtualizer({ count: () => rows.length, getScrollElement: () => scrollRef, estimateSize: (index) => (rows[index].kind === 'header' ? 40 : 28), overscan: 6, }) // Programmatic scroll: virtualizer.scrollToIndex(75_432, { align: 'center' }) ``` See [`packages/sv-grid-community/src/virtualization/`](../packages/sv-grid-community/src/virtualization/) for the full API. --- ## 13. Accessibility SvGrid implements the WAI-ARIA 1.2 grid pattern. - The root carries `role="grid"`, an accessible name (set via `aria-label` or `aria-labelledby`), and `aria-rowcount` / `aria-colcount` reflecting the total - not just the visible window. - Rows carry `role="row"` plus `aria-rowindex` accounting for the virtualized offset; cells carry `role="gridcell"` and `aria-colindex`. - The active cell is always exactly one focusable element (roving `tabindex`); arrow keys move it. - Sort columns carry `aria-sort="ascending" | "descending" | "none"`. - Sort and selection state changes are announced via an off-screen `aria-live` region the grid manages internally. If you build your own header or toolbar, use the helpers in [`a11y.ts`](../packages/sv-grid-community/src/a11y.ts) so your markup stays consistent with the contract: ```ts import { getGridRootA11yProps, getGridRowA11yProps, getGridCellA11yProps, getGridHeaderA11yProps, } from 'sv-grid-community' ``` There is a contract test suite at [`a11y.contract.test.ts`](../packages/sv-grid-community/src/a11y.contract.test.ts) that exercises the public a11y guarantees - run it (`pnpm test`) when you customize markup to be sure you haven't regressed the contract. --- ## 14. TypeScript notes Most APIs are generic over your row type. Define the row type once and flow it through: ```ts type Person = { id: string; firstName: string; age: number } const columns: ColumnDef<{}, Person>[] = [ { field: 'firstName', header: 'First name' }, // ✅ key checked // { field: 'first_name', header: '…' }, // ✗ TS error ] ``` The first type parameter is the **feature set**. When you register features, derive it once and reuse: ```ts const features = tableFeatures({ rowSortingFeature, rowSelectionFeature, columnFilteringFeature, }) type Features = typeof features const columns: ColumnDef[] = [/* … */] ``` This lets feature-specific column properties (like `filterFn`) auto-complete and type-check. --- ## 15. What's next ### Core features - **[Examples gallery](https://svgrid.com/#/demos)** - 50+ production-quality demos, from quick-start to 100k-row virtualization. - **[Column definitions](./help/columns/column-definitions.md)** - every property on `ColumnDef`. - **[Row sorting](./help/rows/row-sorting.md)** and the wider [rows topic index](./help/index.md#rows) - when to use which row model and the order they run in. - **[Tree rows (expand / collapse)](./help/rows/tree-rows.md)** - the flat-array + expanded-map pattern, connector lines, keyboard navigation, and lazy load on first expand. - **[Filter API](./help/filtering/filter-api.md)** - sort, filter, paginate, group, and aggregate locally or against your backend. - **[Tailwind integration](./help/tailwind.md)** - full list of CSS custom properties (`--sg-*`) and recipes for building your own theme. - **[Compare SvGrid with other Svelte data grids](https://svgrid.com/#/compare)** - side-by-side feature matrix and when to pick which. ### Pro features (`sv-grid-pro`) The paid companion package augments your `SvGridApi` with one `installPro(api)` call. Set a license key at app boot to remove the "unlicensed" watermark - every feature still runs without a key for demos and evaluation. - **[Data export and printing](./help/export.md)** - Excel (xlsx), PDF, CSV, TSV, HTML, and a paginated print view. Defaults to the currently displayed rows so sort + filter + paginate carry through automatically. - **[Data import](./help/import.md)** - Excel (xlsx), CSV, TSV, and JSON with column mapping, per-row validation, and preview-before-commit. Auto- detects the format from the file extension or pasted text. - **[Pivot tables](./help/pivot.md)** - drag-and-drop Pivot Designer with Filters / Rows / Columns / Values zones, multi-level column headers, per-measure aggregator picker, pivot-aware sort. Built on the same engine; no special "pivot mode". - **[AI assistant](./help/ai.md)** - natural-language filter, smart fill, summarise, and classify, driven by a bring-your-own model adapter (`setAIProvider(fn)`). Ships with a deterministic `mockAIProvider` so the demo works without keys. ### Getting help - File issues at the [project repository](https://github.com/sv-grid/sv-grid/issues). - Browse the [Help index](./help/index.md) for topic-oriented guides. - Use the [sv-grid-mcp](https://svgrid.com/#/mcp) server to give your AI assistant accurate answers. - Read the source - it is small, well-commented, and meant to be read before opening a bug report. ### License `sv-grid-community` is published under the **MIT License**. Free for commercial and personal use. The paid `sv-grid-pro` companion package (export, import, print, pivot, AI assistant) is governed by a separate commercial license. See [LICENSE](../LICENSE) and [packages/sv-grid-pro/LICENSE](../packages/sv-grid-pro/LICENSE). # Getting started with SvGrid SvGrid is a modern, production-ready data grid for Svelte 5 - a headless core engine paired with a Svelte render component (``). It scales from a 10-row read-only table to a virtualized 100,000-row × 100-column editing surface with grouping, multi-column filtering, server-side data, and full keyboard and screen-reader support. This guide is six short pages. Read them in order if you're new; jump straight to the one you need if you're not. > In a hurry? `npm create sv-grid@latest` scaffolds a working project in > one command - see [Starters & scaffolding](./getting-started/starters.md). | # | Page | What it covers | | --- | ------------------------------------------------------------- | --------------------------------------------------------------------------- | | 0 | [Starters & scaffolding](./getting-started/starters.md) | `npm create sv-grid`, the minimal + SvelteKit admin templates, Deploy to Vercel. | | 1 | [Install](./getting-started/1-install.md) | `pnpm add sv-grid-community`, requirements, smoke-test. | | 2 | [First grid](./getting-started/2-first-grid.md) | The minimum runnable example, explained line by line. | | 3 | [Data and columns](./getting-started/3-data-and-columns.md) | What goes in `data` and `columns`. Custom cells via `renderSnippet`. | | 4 | [Features](./getting-started/4-features.md) | Opt into sort, filter, pagination, grouping, selection, editing. | | 5 | [Theme and density](./getting-started/5-theme-and-density.md) | `--sg-*` tokens, dark mode, row height, sizing the grid in a flex layout. | | 6 | [Going to production](./getting-started/6-going-to-production.md) | Server-side data, virtualization, a11y, SSR, CSP, TypeScript notes. | Estimated reading time: 15 minutes across all six pages. ## Companion reads - [Why headless?](./why-headless.md) - the architecture decision behind the `createSvGrid` core vs. the `` render component. - [Tailwind integration](./help/tailwind.md) - how the `--sg-*` custom properties, Tailwind v4, and dark mode fit together. - [Web Components & Custom Elements](./help/web-components.md) - use SvGrid as a framework-agnostic `` element in React, Vue, Angular, or plain HTML. - [Pro features](./pro/README.md) - the paid feature pack: data export, data import, AI assistant, and pivot tables. ## One-page version Prefer a single 800-line file? See [getting-started-full.md](./getting-started-full.md). The content is identical; the split exists for sidebar nav and faster mobile loads. ## License `sv-grid-community` is published under the **MIT License** - permissive for commercial use, redistribution, and modification. The paid companion `sv-grid-pro` ships under a separate commercial license. See [LICENSE](../LICENSE) and [packages/sv-grid-pro/LICENSE](../packages/sv-grid-pro/LICENSE). ## Frequently asked questions ### How do I add a data grid to a Svelte 5 app? Install `sv-grid-community`, import `SvGrid`, and pass `data` and `columns`. A complete grid is about 15 lines - keyboard navigation and accessibility are on by default, and you opt into sort/filter/edit/group/paging with one boolean shortcut each (`sortable`, `filterable`, ...). See [First grid](./getting-started/2-first-grid.md) and [Features](./getting-started/4-features.md#capability-shortcuts-the-quick-way). ### Does SvGrid work with SvelteKit? Yes. It renders meaningful HTML before hydration, so it works under SvelteKit SSR and static builds. Drive large datasets with controlled, server-side state. ### Is SvGrid free for commercial use? Yes. `sv-grid-community` is MIT-licensed, including commercial use, with no row cap or license key. The optional `sv-grid-pro` pack (export, pivot, import, AI) ships under a separate paid license. ### What do I need to run SvGrid? Svelte 5 (runes) and any Vite-based toolchain. There are no other required dependencies for the Community core; Pro export/import features lazy-load their own dependencies only when used. # 1. Install > Step 1 of 6 · [Next: First grid →](./2-first-grid.md) SvGrid is a single npm package. There is no peer dependency on a CSS framework - bring your own, or use the default theme that ships with the render component. ## Fastest start: scaffold a project Starting fresh? Skip the manual wiring and scaffold a project with the grid already set up: ```bash npm create sv-grid@latest # interactive npm create sv-grid@latest my-admin -- --template admin-dashboard ``` See [Starters & scaffolding](./starters.md) for the templates (minimal Vite app or a full SvelteKit admin dashboard) and the Deploy-to-Vercel flow. To add SvGrid to an **existing** app, install it directly: ```bash # pnpm (recommended) pnpm add sv-grid-community # npm npm install sv-grid-community # yarn yarn add sv-grid-community ``` ## Requirements | Tool | Version | Why | | ----------- | ----------------- | ------------------------------------- | | Svelte | **5.x** | Uses runes: `$state`, `$derived`, `$effect`. | | TypeScript | **5.4+** | Optional but strongly recommended. The column-def types pay for themselves. | | Node | **18+** | For tooling (`vite`, `svelte-check`, the example gallery). | The bundle is tree-shakeable: features you don't import don't ship. There's no monolithic entry that pulls everything. ## Verify the install A 5-line smoke test: ```svelte ``` If you see a styled `
` with two rows, you're done. ## Pro add-on (optional) If you need data export (Excel / PDF / CSV), data import, the AI assistant, or built-in pivot tables, install the paid Pro pack alongside the Community package: ```bash pnpm add sv-grid-pro ``` See [Pro features](../pro/README.md) for what ships and how to license. ## Where the rest of this guide goes 1. **Install** ← you're here 2. [First grid](./2-first-grid.md) - the minimum runnable example explained 3. [Data and columns](./3-data-and-columns.md) - the two arrays the grid actually reads 4. [Features](./4-features.md) - opt into sort, filter, pagination, grouping, etc. 5. [Theme and density](./5-theme-and-density.md) - `--sg-*` tokens, dark mode, row height 6. [Going to production](./6-going-to-production.md) - server-side data, virtualization, a11y, SSR The combined "everything in one page" version is at [../getting-started-full.md](../getting-started-full.md) - useful for printing or single-tab reading. # 2. First grid in 60 seconds > Step 2 of 6 · [← Install](./1-install.md) · [Next: Data and columns →](./3-data-and-columns.md) ```svelte ``` That's a complete, working grid. ## What you got out of the box - A semantic `
` with WAI-ARIA `role="grid"` / `role="row"` / `role="columnheader"` / `role="gridcell"` on every node. - Keyboard navigation: arrow keys move the active cell, Home/End jump to row edges, Page Up/Down move by a page, Ctrl+Home / Ctrl+End jump to the grid corners. - A focus ring on the active cell. Selection on click. F2 / double-click to edit (no-op here because no column has `editorType`). - Auto-sized columns; explicit `width` on the column def overrides. - The default `--sg-*` theme: borders, header background, zebra rows, hover, selection - all on tokens you can swap. ## What you didn't get yet Sort, filter, pagination, grouping, expansion, selection are **off** until you register the matching features. That's the next step. ```svelte ``` [Step 3 →](./3-data-and-columns.md) covers how `data` and `columns` work in detail; [step 4 →](./4-features.md) lights up everything else. ## See it run The quick-start demo is a slightly fancier version (more columns, inline editing, range selection):
Source: [examples/src/demos/01-quick-start.svelte](../../examples/src/demos/01-quick-start.svelte). # 3. Data and columns > Step 3 of 6 · [← First grid](./2-first-grid.md) · [Next: Features →](./4-features.md) SvGrid reads two arrays: `data` (your rows) and `columns` (the column-definition list). Everything else is opt-in. ## Row data The `data` prop is any `ReadonlyArray`. A Svelte 5 `$state` array, a derived store, an SWR/TanStack-Query cache, a `+page.ts` load result, a plain literal - the grid doesn't care, as long as the array reference changes when the rows change. ```svelte ``` ### Identity Today the wrapper uses each row's array index as its id. That's fine for read-only views; if you mutate `rows`, prefer keeping the same object references for rows that didn't change so selection and edit state line up. A `getRowId` prop on the wrapper is tracked in [Missing features](../help/missing-features.md) and supported by the headless `createSvGrid` core today. ### Immutability SvGrid does **not** mutate your data. When the user commits an edit the grid emits an `onCellValueChange` event; you decide whether to mutate in place or copy. See [Saving values](../help/editing/saving-values.md). ## Column definitions A column definition tells SvGrid how to read a value out of a row, how to render it, and how to format it. ```ts import type { ColumnDef } from 'sv-grid-community' type Person = { id: string firstName: string lastName: string age: number joinedAt: string // ISO date salary: number active: boolean } const columns: ColumnDef<{}, Person>[] = [ // Simple accessor by field name { field: 'firstName', header: 'First name' }, // Computed value { id: 'fullName', header: 'Full name', accessorFn: (row) => `${row.firstName} ${row.lastName}`, }, // Numeric, with locale-aware formatting { field: 'age', header: 'Age', format: { type: 'number', options: { maximumFractionDigits: 0 } }, }, // Date with explicit pattern { field: 'joinedAt', header: 'Joined', format: { type: 'date', pattern: 'y-m-d' }, }, // Currency { field: 'salary', header: 'Salary', format: { type: 'currency', currency: 'USD' }, }, // Boolean rendered as a checkbox { field: 'active', header: 'Active', editorType: 'checkbox', }, ] ``` ### Common properties | Property | Purpose | | ------------- | -------------------------------------------------------------------------------------------------------- | | `field` | Reads `row[key]`. | | `accessorFn` | Computes the value from the row. Required when there's no underlying field. | | `id` | Stable column id. Required when you use `accessorFn` (no field to derive from). | | `header` | String or render snippet for the header. | | `footer` | String or render snippet for the footer row. | | `cell` | Render snippet / component for the body cell. See [Cell components](../help/cells/cell-components.md). | | `format` | Locale-aware formatter: `number`, `currency`, `percent`, `date`, `datetime`. | | `formatter` | Function form for one-off custom value formatting. | | `editorType` | Inline editor: `text` \| `number` \| `checkbox` \| `date` \| `datetime`. | | `width` | Initial column width in pixels. Default comes from the `columnWidth` prop on ``. | | `align` | Header + body alignment: `'left'` \| `'right'` \| `'center'`. Inferred from `editorType` when omitted. | | `columns` | Child column defs for column groups (multi-level header). | Sorting / filtering / grouping are toggled per-grid via the registered features. Per-column flags (`enableSorting: false`, etc.) are on the [Missing features](../help/missing-features.md) list. ### Custom cells For anything beyond a stringified value, render with `renderSnippet`: ```svelte {#snippet PersonCell(props: { row: Person })} {props.row.firstName[0]}{props.row.lastName[0]} {props.row.firstName} {props.row.lastName} {/snippet} `${r.firstName} ${r.lastName}`, cell: (ctx) => renderSnippet(PersonCell, { row: ctx.row.original }), }, { field: 'age', header: 'Age' }, ] satisfies ColumnDef<{}, Person>[]} /> ``` [Cell components](../help/cells/cell-components.md) has the full patterns: avatars, sparklines, progress bars, status badges. # 4. Features > Step 4 of 6 · [← Data and columns](./3-data-and-columns.md) · [Next: Theme and density →](./5-theme-and-density.md) The grid engine is **feature-gated**. Out of the box you get the core row model (rows in their original order). To enable sorting, filtering, grouping, pagination, expansion, or selection, register the matching feature; the wrapper wires the matching row-model factory for you. ```svelte ``` The wrapper handles the user-facing toggles via plain props. If you need the headless pipeline directly - say, a custom renderer - drop down to `createSvGrid` from the same package. See [Why headless?](../why-headless.md). ## Capability shortcuts (the quick way) Every capability is **off by default** - a bare `` is a plain, read-only table. The fastest way to opt in is a set of boolean shortcut props. No `tableFeatures({ … })` import, no feature constants: ```svelte ``` | Shortcut | Turns on | Equivalent to | | ------------ | ------------------------------------------------ | ------------------------------------------ | | `sortable` | Click headers to sort | injects `rowSortingFeature` | | `filterable` | Per-column filter menu | injects `columnFilteringFeature` | | `editable` | Inline cell editing (needs `editorType` columns) | `enableInlineEditing` | | `groupable` | "Group by this column" in the column menu | `showGroupingControls` | | `pageable` | Pagination footer | `showPagination` | Each shortcut is an override: omit it (or set `false`) to leave the capability off; set it `true` to opt in. They compose with the fine-grained props and the `features` set below - reach for those when you need finer control (e.g. `filterMode`, `pageSize`, per-column `sortable: false`). See the live [Shortcut config](https://sv-grid.com/demos/135-shortcut-config) demo. ## The feature catalogue | Feature | What it enables | Doc | | ------------------------ | --------------------------------------------------------- | -------------------------------------------------- | | `rowSortingFeature` | Click headers to sort; Shift-click for multi-sort. | [Row sorting](../help/rows/row-sorting.md) | | `columnFilteringFeature` | Per-column filter menu + filter row + global search. | [Filter overview](../help/filtering/overview.md) | | `rowPaginationFeature` | Page slicing + footer with page-size selector. | [Row pagination](../help/rows/row-pagination.md) | | `rowSelectionFeature` | Checkbox column + Shift / Ctrl multi-select. | [Row selection](../help/rows/styling-rows.md) | | `columnGroupingFeature` | Group-by-column + aggregated footer summaries. | [Grouping & aggregation](../help/grouping-aggregation.md) | | `rowExpandingFeature` | Tree / master-detail expand-collapse. | [Tree rows](../help/rows/tree-rows.md) | **Rule of thumb:** only register the features you use. Each one ships about 1-2 KB gzipped and adds a small per-update cost. ## The three operating modes There's one decision per dimension (sort, filter): **uncontrolled** (default), **observable** (callbacks), or **external** (you own the row ordering). ### Uncontrolled (the default) The wrapper owns the state. Pass the start config via props: ```svelte ``` ### Observable - callbacks fire on every change The wrapper still owns the state, but emits callbacks. Use this when something outside the grid needs to react (a "X rows selected" pill, URL sync, a server fetch). ```svelte (sorting = next)} onFiltersChange={(next) => (filters = next.columns)} /> ``` ### External - you own the row ordering For server-side data or tree data the wrapper records the sort + filter UI state but does **not** re-order the rows - you do. See [Going to production §1](./6-going-to-production.md#1-server-side-data). ## Selection + editing Both are off by default. Two top-level umbrella props turn them on: ```svelte /* … */} onCellValueChange={(event) => /* event: { rowIndex, columnId, oldValue, newValue, row } */} /> ``` `selectionMode` choices: - `'row'` - checkbox column only - `'cell'` - click-and-drag range selection only - `'both'` - both (default) - `'none'` - both off Cell editing requires `editorType` on the columns you want editable. See [Editing overview](../help/editing/overview.md). ## Keyboard map | Action | Keys | | --------------------- | ------------------------------------- | | Move active cell | Arrow keys | | First / last in row | Home / End | | First / last in grid | Ctrl + Home / Ctrl + End | | Move by viewport | Page Up / Page Down | | Extend range | Shift + arrows / Shift + Home / End | | Start editing | Enter, F2, or double-click | | Commit edit | Enter, Tab | | Cancel edit | Esc | | Toggle row selection | Space | | Copy / paste range | Ctrl/Cmd + C / V (TSV) | The full a11y model is in [Accessibility](../help/accessibility.md). # 5. Theme and density > Step 5 of 6 · [← Features](./4-features.md) · [Next: Going to production →](./6-going-to-production.md) The render component (``) ships its own scoped styles. You re-theme it by declaring `--sg-*` CSS custom properties at any level above the grid - `:root` for the whole app, a wrapper `
` for one instance, or directly on the `` element itself. ## Token surface The 20-odd tokens the renderer reads: | Token | What it paints | | -------------------------------- | ------------------------------------------- | | `--sg-bg` | Cell background | | `--sg-fg` | Cell text | | `--sg-muted` | Secondary text (footers, subtitles) | | `--sg-border` | Cell + header borders | | `--sg-header-bg` / `--sg-header-fg` | Header row | | `--sg-row-alt-bg` | Zebra rows | | `--sg-row-hover-bg` | Row + cell hover | | `--sg-row-height` | Row height | | `--sg-selection-bg` | Selected cell / row tint | | `--sg-accent` | Sort indicator, focus ring, primary buttons | | `--sg-focus-ring` | Keyboard focus outline | | `--sg-input-bg` / `--sg-input-border` | Inline editor + filter inputs | | `--sg-pill-active` / `-fg` | "Active" status pills | | `--sg-pill-pending` / `-fg` | "Pending" status pills | | `--sg-pill-inactive` / `-fg` | "Inactive" status pills | | `--sg-scrollbar-*` (10 tokens) | Custom-painted scrollbars | ## Light + dark via `data-theme` The gallery flips themes by writing `dark` or `light` to `html[data-theme]`. Every token redeclares under that selector: ```css :root { --sg-bg: #ffffff; --sg-fg: #0f172a; --sg-border: #e2e8f0; --sg-header-bg: #f1f5f9; --sg-row-alt-bg: #f8fafc; --sg-row-hover-bg: #eef2ff; --sg-accent: #2563eb; } html[data-theme='dark'] { --sg-bg: #0f172a; --sg-fg: #f1f5f9; --sg-border: #334155; --sg-header-bg: #1e2433; --sg-row-alt-bg: #1b2230; --sg-row-hover-bg: #232b3c; --sg-accent: #3b82f6; color-scheme: dark; } ``` Toggling is one line in the app shell: ```svelte ``` ## Per-instance theming Because the tokens are plain custom properties they cascade. To style a single grid, wrap it in a `
` that sets its own values: ```svelte
``` The [`10-custom-cells-and-themes`](../../examples/src/demos/10-custom-cells-and-themes.svelte) demo applies three full palettes (light / dark / high-contrast) this way.
## Density Two ways: 1. **Set `rowHeight` on ``**. Numeric, in pixels. Drives the row height and the active-cell hit box. ```svelte ``` 2. **Override `--sg-row-height` on a wrapper.** Same effect, with the token shape if you'd rather express density in CSS. ```css .compact { --sg-row-height: 28px; } .comfortable { --sg-row-height: 48px; } ``` A user-facing "density selector" is half a dozen lines: ```svelte ``` ## Sizing the grid The wrapper renders inside whatever container you give it. The `containerHeight` prop sets the scrollable shell height: ```svelte ``` For a flex-grow layout the canonical recipe is: ```svelte
``` The `min-h-0` is the bit that bites. Flex children default to `min-height: auto`, which prevents the inner scroll container from shrinking, which makes the whole page scroll instead of the grid. ## Full Tailwind integration If your app uses Tailwind, see [Tailwind integration](../help/tailwind.md) for: install + PostCSS config, `@custom-variant` so the `dark:` modifier follows `data-theme`, the override hooks for the stable `.sv-grid-*` class names, and the anti-patterns (don't `@apply` inside grid selectors, don't put utility classes on grid children, don't fight column widths in CSS). # 6. Going to production > Step 6 of 6 · [← Theme and density](./5-theme-and-density.md) 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. ```svelte { sort = next; page = 0 }} onFiltersChange={(next) => { filters = next.columns; page = 0 }} /> ``` The [`09-server-side` demo](../../examples/src/demos/09-server-side.svelte) has the full runnable version with debounce + abort + a 60 ms mock latency. The [server-side guide](../help/server-side-data.md) covers sparse infinite scroll, velocity-aware chunk loading, and backpressure.
## 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. ```svelte ``` The wrapper's row + column virtualizers handle variable row heights via the headless `createSvelteVirtualizer` / `createColumnVirtualizer`. See [demo 06](../../examples/src/demos/06-large-dataset.svelte) for 100k rows × 100 columns with smooth scroll.
## 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-live` announcements for changes outside the grid (filter applied, X rows selected) - see [demo 17](../../examples/src/demos/17-accessibility.svelte). - Toggle a high-contrast focus outline for users who need it - also in demo 17. - Read [Accessibility](../help/accessibility.md) for the WCAG 2.1 AA mapping, forced-colors-mode behaviour, and reduced-motion handling.
## 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. ```svelte ``` [Demo 19 - SSR](../../examples/src/demos/19-ssr.svelte) takes a sandboxed pre-hydration snapshot in a JS-disabled iframe to prove the markup is meaningful before JS runs. ## 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](../../examples/src/demos/16-csp-compliant.svelte) runs a live runtime self-check + a violation listener inside a working grid. ## 6. TypeScript notes ```ts import type { ColumnDef, SvGridApi, SortingState, TableFeatures, } from 'sv-grid-community' // 1. Constrain ColumnDef to your row type so editors and accessors stay typed. type Row = { id: string; firstName: string; age: number } const columns: ColumnDef<{}, 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 | 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?](../why-headless.md) - the layered architecture and when to drop down to the headless core. - [Pro features](../pro/README.md) - export, import, AI, pivot. - [Help index](../help/index.md) - the topic-page catalogue. - [Recipes](../help/recipes.md) - 20+ copy-paste patterns. - [Comparison vs AG Grid + TanStack](../help/comparison.md) - what evaluators read. - [Missing features](../help/missing-features.md) - the honest gap list. # Starters & scaffolding The fastest way to a working SvGrid app. One command scaffolds a project with the grid already wired up - no copy-paste, no config archaeology. ## `npm create sv-grid` ```bash # npm npm create sv-grid@latest # pnpm pnpm create sv-grid # yarn yarn create sv-grid ``` Run with no arguments and it walks you through a project name and a template. Or pass them directly: ```bash # Minimal Vite + Svelte 5 starter npm create sv-grid@latest my-app -- --template minimal # Full SvelteKit admin dashboard npm create sv-grid@latest my-admin -- --template admin-dashboard ``` Then: ```bash cd my-app npm install npm run dev ``` ### Templates | Template | Stack | Best for | | --- | --- | --- | | `minimal` | Vite + Svelte 5 + `sv-grid-community`, one page | Dropping a grid into something quickly | | `admin-dashboard` | SvelteKit + Tailwind + `sv-grid-community`, deploy to Vercel | A real dashboard / internal tool | ### Options | Flag | Alias | Description | | --- | --- | --- | | `--template ` | `-t` | `minimal` or `admin-dashboard` | | `--force` | `-f` | Scaffold into a non-empty directory | | `--help` | `-h` | Show usage | Both templates use the free MIT `sv-grid-community` core. Add [`sv-grid-pro`](../pro/README.md) for export, import, print, pivot, and the AI helpers. ## The admin dashboard starter The `admin-dashboard` template is a production-shaped SvelteKit app you can fork directly from [the repo](https://github.com/sv-grid/sv-grid/tree/main/templates/sveltekit-admin-dashboard): - **App shell** - sidebar nav + top bar (`src/routes/+layout.svelte`) - **Overview** - KPI cards + a recent-orders grid (`src/routes/+page.svelte`) - **Orders** - full grid: sort, filter, row selection, inline editing, pagination - **Customers** - grid with column grouping (drag a column to the group bar) - **Prerendered to static HTML** for SEO and instant first paint; grids hydrate on the client (`prerender = true` in `src/routes/+layout.ts`) - **Sample data** in `src/lib/data.ts` - swap for your API or a SvelteKit `load` function ### Deploy to Vercel The starter ships with `@sveltejs/adapter-vercel` and a one-click deploy button in its README. From a forked or scaffolded copy: ```bash npm run build # static + serverless output npm run preview # preview the production build ``` If you're deploying the template straight from this monorepo's subfolder, set the Vercel **Root Directory** to `templates/sveltekit-admin-dashboard`. Scaffolding a standalone copy first (above) avoids that step. ## Adding the grid to an existing app Already have a Svelte or SvelteKit project? Skip the scaffolder and install directly - see [Install](./1-install.md) and [First grid](./2-first-grid.md). ## Frequently asked questions ### What's the fastest way to start a SvGrid project? Run `npm create sv-grid@latest` (or `pnpm create sv-grid`). It scaffolds a Vite + Svelte or SvelteKit project with the grid already wired up, then `npm install` and `npm run dev`. ### Is there a SvelteKit admin dashboard template? Yes. `npm create sv-grid@latest my-admin -- --template admin-dashboard` scaffolds a SvelteKit + Tailwind admin with multiple grids, prerendered for SEO, and a one-click Deploy-to-Vercel button. ### Do the starters require a Pro license? No. Both templates use the free MIT `sv-grid-community` core. Pro features (export, import, print, pivot, AI) are an optional add-on that runs in evaluation without a key. ## See also - [Install](./1-install.md) - add SvGrid to an existing project - [First grid](./2-first-grid.md) - the minimum runnable example - [Going to production](./6-going-to-production.md) - SSR, virtualization, a11y # Accessibility SvGrid implements the WAI-ARIA 1.2 grid pattern with full keyboard navigation and a screen-reader announcement layer. This page documents exactly what the grid does, where the responsibility line sits between the library and your code, and how to verify conformance. Live demo - high-contrast toggle, `aria-live` log, and a focus-trap walk-through:
## TL;DR | Standard | Status | | ----------------------- | ---------------------------------------------------------------------- | | WAI-ARIA 1.2 grid pattern | Implemented (see [Roles & properties](#roles--properties)). | | WCAG 2.1 AA | The grid meets AA for the default theme + tokens; your custom theme determines pass/fail of contrast. | | Keyboard navigation | Full coverage; see [Keyboard map](#keyboard-map). | | Screen-reader announcements | Cell + selection + edit announcements via a single `aria-live=polite` region. | | Reduced motion | Honored: scroll animations + chevron transitions disable when `prefers-reduced-motion: reduce`. | | Forced colors / high contrast | Supported: borders + focus rings use `currentColor`; no hardcoded `border-color`. | ## Roles & properties | Element | Role | Notable attributes | | ------------------------------ | -------------- | --------------------------------------------------------------------------------------------------- | | `
` root | `grid` | `aria-rowcount`, `aria-colcount` | | ``, `` | `rowgroup` | | | Every `` (header + body) | `row` | `aria-rowindex` (1-based); selected rows get `aria-selected="true"` | | Header ``. See [Styling rows](../rows/styling-rows.md). ## Right-align numbers A common rule of thumb is "numbers right, everything else left". Target by `editorType`: ```css table[role='grid'] td[data-editor-type='number'] { text-align: right; font-variant-numeric: tabular-nums; } ``` (`data-editor-type` is set by the grid - verify by inspecting an element in your devtools; if the attribute is not present in your build, fall back to wrapping the value in a ``.) ## Highlighting the active cell The currently-active cell carries `aria-selected="true"` and matches the focus-ring custom property: ```css table[role='grid'] td[aria-selected='true'] { box-shadow: inset 0 0 0 2px var(--sg-accent); } ``` ## Edit-mode cell While a cell is being edited, it carries `data-editing="true"` and renders an `` inside: ```css table[role='grid'] td[data-editing='true'] { padding: 0; } table[role='grid'] td[data-editing='true'] input { width: 100%; height: 100%; padding: 0.4rem 0.6rem; border: 0; outline: none; background: var(--sg-bg); } ``` ## See also - [Highlighting changes](./highlighting-changes.md) - [Cell components](./cell-components.md) # Text formatting The `format` field on a column produces locale-aware formatted strings without you writing a renderer.
## Number ```ts { field: 'count', header: 'Count', format: { type: 'number', options: { maximumFractionDigits: 0 } } } ``` `options` is `Intl.NumberFormatOptions`. Combine with `locales` for non-default locales: ```ts { field: 'count', header: 'Anzahl', format: { type: 'number', locales: 'de-DE', options: { maximumFractionDigits: 0 } } } ``` ## Currency ```ts { field: 'salary', header: 'Salary', format: { type: 'currency', currency: 'USD' } } ``` `currency` is an ISO 4217 code; if omitted, USD is used. ## Percent ```ts // values are fractions (0.42 → 42%) { field: 'utilization', header: 'Util', format: { type: 'percent' } } // values are 0–100 (42 → 42%) { field: 'progress', header: 'Progress', format: { type: 'percent', valueIsPercentPoints: true } } ``` ## Date / datetime ```ts { field: 'joinedAt', header: 'Joined', format: { type: 'date', pattern: 'y-m-d' } } { field: 'updatedAt', header: 'Updated', format: { type: 'datetime', pattern: 'medium' } } ``` Built-in patterns: | pattern | shorthand for | | ------- | ------------- | | `'d'` | short numeric date | | `'D'` | long date | | `'y-m-d'` | year-month-day | | `'short'` \| `'medium'` \| `'long'` | `dateStyle` / `timeStyle` presets | Combine `pattern` with `options` to override individual fields. ## Custom formatter For anything `format` cannot express, use `formatter`: ```ts { field: 'temperature', header: 'Temp', formatter: ({ value }) => `${Number(value).toFixed(1)}°C`, } ``` `formatter` runs **after** the accessor and **before** the cell renderer. Its return value is what gets displayed and copied to the clipboard. ## Order of precedence When a column has both, the resolution order is: 1. `field` / `accessorFn` produces the value 2. If `cell` is set, it renders - `format` / `formatter` are ignored 3. Otherwise `formatter` runs if set 4. Otherwise `format` runs if set 5. Otherwise the value is rendered as `String(value)` ## See also - [Cell components](./cell-components.md) - when `format` is not enough. - [`cell-formatting.ts`](../../../packages/sv-grid-community/src/cell-formatting.ts) # Tooltips There is no built-in tooltip API on `ColumnDef`. Use the standard `title` attribute, an accessible ``, or any popover library - wired through a custom cell renderer. ## With `title` (no JS, screen-reader friendly) ```ts { field: 'description', cell: (ctx) => renderSnippet(EllipsisCell, { value: String(ctx.getValue() ?? ''), }), } ``` ```svelte {#snippet EllipsisCell(p: { value: string })} {p.value} {/snippet} ``` `title` is the safest default: screen readers announce it; mouse users see a tooltip; keyboard users see it on focus (when wrapped in a focusable element). ## With a popover library Pass a component instead of a snippet: ```ts import TooltipCell from './TooltipCell.svelte' import { renderComponent } from 'sv-grid-community' { field: 'description', cell: (ctx) => renderComponent(TooltipCell, { value: String(ctx.getValue() ?? ''), tip: ctx.row.original.fullDescription, }), } ``` ## Header tooltips The same pattern, but via the `header:` field - see [Custom header components](../columns/custom-header-components.md). ## Gotchas - The grid's column-menu popover and the cell-edit overlay use top-layer z-indices around 100. Your tooltip should be either lower (so it slides under those overlays when both open) or higher with a click-outside dismissal. - A long tooltip inside an Excel-style filter dropdown can occlude the filter input. Detach the tooltip from cells inside an open filter menu. ## See also - [Cell components](./cell-components.md) # View refresh The grid renders reactively - it does **not** have a `refresh()` method, because it doesn't need one. To make the grid re-display, change the data that drives it. ## Forcing a refresh | You want | Do this | | -------- | ------- | | Re-display every row | Reassign `data` to a new array (`rows = [...rows]`). | | Re-display one row | Mutate the row through a `$state` array, or call `api.setCellValue(...)`. | | Re-apply sort / filter / page | Update the controlled state slice (or reassign the data). | | Re-render a single cell | The grid's renderer keys on the cell's `cellId`. Changing the underlying value re-renders the cell. | ## When the grid does NOT re-render If you mutate a row object **deeply** without going through `$state` - e.g. `someExternalRef.salary = 50000` where `someExternalRef` is an object held outside the grid - Svelte 5 will not know to update. Two safe patterns: ```svelte ``` ## Refresh-after-async For data fetched asynchronously, the array reassignment is the canonical trigger: ```svelte ``` ## See also - [Row data](../rows/row-data.md) - [Accessing rows](../rows/accessing-rows.md) # Integrated charts SvGrid can chart its own data with no external charting library. Two pieces: - **`SvGridChart`** - a component that renders a `ChartSpec` as inline SVG (bar, line, area, pie, scatter / bubble) with axes, hover tooltips, a clickable legend that toggles series (or pie slices) on and off, reference lines, a time axis, and a visually-hidden data table for screen readers. - **`rowsToChartSpec(rows, opts)`** - aggregates flat rows (group by a category field, reduce a value field) into a `ChartSpec`, with optional sorting and top-N + "Other" bucketing. Feed it `api.getDisplayedRows()` and the chart reflects the grid's current, filtered, sorted data - the "chart from the grid" enterprise feature. ```svelte { api = a; sync() }} onFiltersChange={sync} onSortingChange={sync} /> ``` ## `rowsToChartSpec` | Option | Meaning | | ------------- | -------------------------------------------------------- | | `type` | `'bar' \| 'line' \| 'area' \| 'pie' \| 'scatter'` | | `category` | Field whose distinct values become the x-axis / slices. | | `value` | Numeric field, **or an array of fields** (one series each). | | `series` | Pivot field: one series per distinct value of it. | | `reduce` | `'sum'` (default), `'avg'`, or `'count'`. | | `stacked` | Stack the series instead of grouping them. | | `stacked100` | Stack to 100% - each category normalized to its total. | | `sort` | `'value-desc' \| 'value-asc' \| 'category' \| 'none'`. | | `topN` | Keep the top N categories, bucket the rest into "Other". | | `otherLabel` | Label for the bucket (default `'Other'`). | | `width` / `height` | SVG viewBox size. | Three multi-series shapes: ```ts rowsToChartSpec(rows, { type: 'bar', category: 'region', value: 'revenue' }) // 1 series rowsToChartSpec(rows, { type: 'bar', category: 'region', value: ['revenue', 'cost'] }) // 2 series rowsToChartSpec(rows, { type: 'bar', category: 'region', value: 'sales', series: 'product' }) // pivot ``` ## Building a spec yourself `SvGridChart` takes any `ChartSpec`. Per-series `type` and `axis` give you combo charts and a secondary Y axis; `stacked` stacks bars/areas; `innerRadius` turns a pie into a donut; `yAxisTitle` / `y2AxisTitle` / `xAxisTitle` label the axes. Negative values drop below a zero baseline automatically, and `null` / `NaN` values break the line (a gap) instead of dropping it to zero. ```ts const spec: ChartSpec = { type: 'bar', stacked: false, categories: ['Q1', 'Q2', 'Q3', 'Q4'], series: [ { label: 'Revenue', values: [120, 140, 90, 180] }, // bars, left axis { label: 'Margin %', values: [0.31, 0.28, 0.22, 0.35], type: 'line', axis: 'right' }, // line, right axis ], } // donut: { type: 'pie', innerRadius: 0.6, categories, series: [...] } ``` The geometry helper `buildChart(spec)` is exported too, if you want the raw SVG primitives for a custom renderer. ## Reference / target lines `referenceLines` draws horizontal goal / average / SLA lines across the plot. Each entry stretches the axis domain so the line is always in view: ```ts const spec: ChartSpec = { type: 'bar', categories: ['Q1', 'Q2', 'Q3', 'Q4'], series: [{ label: 'Revenue', values: [120, 140, 90, 180] }], referenceLines: [{ value: 150, label: 'Target', axis: 'left', color: '#ef4444', dashed: true }], } ``` ## 100% stacked `stacked100: true` (implies `stacked`) normalizes each category to its own total, so the axis runs 0..100% and every column fills the plot height - ideal for reading composition / share. Tooltips and labels still show the original values. ## Scatter / bubble `type: 'scatter'` plots two numeric measures against each other. Each series carries `points: [{ x, y, r?, label? }]`; an optional `r` becomes the bubble radius (scaled across the data). One series per group colours the points. ```ts const spec: ChartSpec = { type: 'scatter', categories: [], xAxisTitle: 'Spend', yAxisTitle: 'Revenue', series: [ { label: 'EMEA', values: [], points: [{ x: 12_000, y: 80_000, r: 18, label: 'Ada' }] }, { label: 'APAC', values: [], points: [{ x: 30_000, y: 140_000, r: 33, label: 'Grace' }] }, ], } ``` ## Horizontal bars `orientation: 'horizontal'` swaps the axes: categories run down the left, bars grow rightward. It suits long category labels (rep names, product names) that would otherwise crowd / rotate on a vertical x-axis. Grouped, stacked, 100%, data labels, and reference lines (which become vertical) all work. Only applies when every series is a bar - combo / line / area fall back to vertical. ```ts const spec: ChartSpec = { type: 'bar', orientation: 'horizontal', categories: ['Ada', 'Grace', 'Margaret', 'Linus'], series: [{ label: 'Revenue', values: [120, 90, 140, 80] }], referenceLines: [{ value: 110, label: 'Avg' }], // drawn as a vertical line } ``` ## Time axis `xType: 'time'` treats `categories` as dates: x positions are spaced by actual time (irregular gaps render proportionally, not evenly) and the axis shows real date ticks. Works with line / area / bar. ```ts rowsToChartSpec(rows, { type: 'line', category: 'date', value: 'sessions', series: 'channel' }) // then: spec.xType = 'time' ``` ## Interactivity `SvGridChart` is interactive by default: - **Unified tooltip + crosshair** - hovering a category column shows a vertical crosshair and a single tooltip listing **every** series' value at that category (with color swatches), so multi-series and combo charts read at a glance. Pie slices keep a per-slice tooltip. - **Legend toggle + isolate** - clicking a legend chip hides/shows that series (or pie slice); **double-clicking** isolates it (shows only that one, click again to restore). Hovering a chip dims the others. The chart re-scales to the visible data; colors stay stable. - **Scatter tooltip** - hovering a bubble shows its x / y (and label). - **Legend overflow** - a wide pivot (many series) collapses the legend to the first 10 chips with a "+N more" toggle, so it never floods the chart. - **Data labels** - `dataLabels` draws the value on each bar / point / slice. - **Drill-down** - `onSelect({ category, series, value })` fires when a bar / point / slice is clicked. Wire it to `api.setFacetFilter(...)` to filter the grid to the clicked category - the "click the chart to drill the grid" loop. ```svelte `$${compact(v)}`} // tooltips, labels, AND Y-axis ticks onSelect={(s) => api.setFacetFilter('region', [s.category])} // drill the grid legend={true} // clickable legend; default true interactive={false} // opt out of tooltips + toggling /> ``` `formatValue` is applied to tooltips, data labels, **and the Y-axis ticks**, so they stay consistent - keep it compact (e.g. `$2M`, not `$2,000,000`). ## Export Download the rendered chart as a standalone SVG or a PNG. Pass the chart's wrapper element (or its ``): ```svelte
``` `chartToSvgString` / `chartToPngBlob` return the data if you want to upload it instead. The export inlines the live theme colors, so it matches what's on screen. ## Notes - Pure SVG - no canvas, no dependency, SSR-safe, and it inherits the grid's `--sg-*` theme tokens. - **Accessible** - every chart renders a visually-hidden `
` | `columnheader` | `aria-sort` (`"ascending"` / `"descending"` / `"none"`) | | Body `` | `gridcell` | `aria-colindex`, `aria-selected`, `aria-readonly` (when the column is non-editable) | | Active cell | `gridcell` | `tabindex="0"`; every other cell `tabindex="-1"` (roving tabindex pattern) | | Filter popover | `dialog` | `aria-label` derived from the column header | | Cell editor input | `textbox` / `combobox` / `checkbox` (depending on `editorType`) | `aria-label` mirrors the column header | | Live announcement region | none (uses `aria-live="polite"` on a visually-hidden `
`) | | The grid never sets `role="presentation"` on table elements - screen readers receive a fully-structured grid. ## Keyboard map Standard ARIA grid navigation, plus a handful of grid-specific shortcuts: ### Navigation | Key | Action | | ------------------- | --------------------------------------------------------------------- | | Tab | Move focus *out* of the grid to the next focusable element. | | Shift+Tab | Move focus *into* the grid from the previous focusable element. | | Arrow Up/Down | Move active cell one row. | | Arrow Left/Right | Move active cell one column. | | Home | Active cell → first column in the row. | | End | Active cell → last column in the row. | | Ctrl/Cmd + Home | Active cell → top-left of the grid. | | Ctrl/Cmd + End | Active cell → bottom-right. | | Page Up / Page Down | Scroll a viewport's worth of rows. | ### Selection (when `rowSelectionFeature` is on) | Key | Action | | ------------------------- | --------------------------------------------------------------- | | Space | Toggle the active row's selection. | | Ctrl/Cmd + A | Select all rows on the current page. | | Shift + Arrow Up/Down | Extend the row selection. | | Ctrl/Cmd + Click on a row | Toggle that row's selection without affecting others. | ### Sort + filter | Key | Action | | -------------------------------- | -------------------------------------------------------- | | Enter (on a header) | Toggle sort: `none → asc → desc → none`. | | Shift + Enter (on a header) | Add to multi-sort. | | Alt + Down (on a header) | Open the filter menu (when `filterMode='menu'`). | | Escape (in a filter menu) | Close the menu. | ### Editing | Key | Action | | --------------------------- | ----------------------------------------------------- | | Enter / F2 / double-click | Enter edit mode on the active cell. | | Type any character | Start editing with that character as the first input. | | Enter / Tab (while editing) | Commit and move to the next cell / row. | | Escape (while editing) | Cancel; revert the cell. | | Delete / Backspace | Clear the active cell (for cells whose editor supports clearing). | ### Tree rows (when you build them) The keyboard handler in [Tree rows](./rows/tree-rows.md) adds: | Key | Action (active cell in name column) | | ------------------- | ----------------------------------- | | Arrow Right | Expand a collapsed node. | | Arrow Left | Collapse an expanded node. | | Enter / Space | Toggle. | ## Screen-reader announcements The grid maintains a single `aria-live="polite"` region that emits: - Cell content when the active cell moves (so VoiceOver/JAWS reads "Customer column, row 14, Acme Corp"). - Selection state changes (`"row 14 selected, 3 of 12 rows selected"`). - Edit commits (`"saved 19.95"`) - useful for users who can't see the cell's visual state. - Sort changes (`"sorted by Customer ascending"`). - Filter changes (`"3 of 124 rows match"`). You can replace the announcer with your own by passing `announcer={(message) => ...}` to `` if you have a unified toast system. ## Focus management - The grid uses a **roving tabindex**: at most one cell at a time has `tabindex="0"`, every other has `tabindex="-1"`. This puts the grid in the tab order exactly once. - The "active cell" is the focused cell. It's tracked through every navigation, editing, and selection action. - Editing transfers focus to the editor ``; committing returns focus to the cell. - Modals (filter menu, save dialog) use a focus trap that returns focus to the originating header/cell on close. ## High-contrast / forced-colors mode Windows High Contrast mode (`forced-colors: active`) is respected. The grid uses `currentColor` for every border and focus ring, so the system's color tokens take over without overrides leaking. We test against the [W3C forced-colors test page](https://web.dev/articles/forced-colors). ## Reduced motion When `prefers-reduced-motion: reduce` matches: - Smooth-scroll calls become `behavior: 'instant'`. - Chevron rotations (used in tree demos) are instant rather than 160 ms ease. - Sparkline + KPI bar transitions are disabled. You don't need to opt in - the grid checks the media query on every animation entry point. ## What you're responsible for The grid can't know: - **Contrast** of your custom CSS variables. Use the [Tailwind integration](./tailwind.md) page's contrast notes when picking `--sg-fg` / `--sg-bg` pairs. - **Labels** for cells whose content is purely visual (e.g. a status pill that's just a coloured dot). Set `aria-label` on the cell content yourself. - **Reading order** of header groups. The grid emits group headers with `aria-colspan` correctly, but if your group label is "Q1" alone, screen readers say "Q1" - consider "Q1 2025" to give context. ## How to verify 1. **Keyboard sweep.** Unplug your mouse. Tab in, navigate every cell, sort, filter, edit, undo. If any action is unreachable, file an issue. 2. **NVDA + VoiceOver.** Each makes different choices about announcement verbosity. Test both. 3. **Lighthouse accessibility audit.** Default theme passes 100. If your custom theme drops the score, the deltas are virtually always contrast issues you control. 4. **axe-core in your e2e suite.** ```ts import { injectAxe, checkA11y } from 'axe-playwright' await injectAxe(page) await checkA11y(page, '.sv-grid-shell', { detailedReport: false }) ``` ## See also - [Browser support](./browser-support.md) - the `ResizeObserver` / Pointer Events floor every assistive-tech tool relies on. - [Tailwind integration](./tailwind.md) - the `--sg-*` tokens that control contrast. - [Testing your grid](./testing.md) - includes an axe-core recipe. ## Frequently asked questions ### Is SvGrid accessible / WCAG compliant? SvGrid implements the WAI-ARIA 1.2 grid pattern: `role="grid"` structure, full keyboard navigation (arrows, Home/End, Page Up/Down, Ctrl+Home/End), a focus ring on the active cell, and an `aria-live` announcement layer. Final WCAG conformance also depends on your own cell content and color choices - this page documents exactly where that line sits. ### Does SvGrid work with screen readers? Yes. The grid exposes proper roles and emits `aria-live` announcements for sorting, filtering, and selection changes, so NVDA, JAWS, and VoiceOver can read the grid state. Because it renders real DOM (not canvas), the content is also selectable and machine-readable. ### How do I verify accessibility in my app? Run axe-core against the rendered grid (recipe in the testing guide) and test keyboard-only navigation. Pair it with the high-contrast focus toggle and the `--sg-*` contrast tokens to meet your target contrast ratios. # Build an AI agent that drives the grid This is the "AI-native data grid" story end-to-end. Three patterns, in order of how much agency you hand to the LLM: 1. **Read-only summary agent** - the model describes what's in the grid (analyst chat, monthly report drafts) 2. **Stateful UI agent** - the model calls `SvGridApi` methods in response to natural language ("group by region, sum revenue") 3. **Autonomous workflow agent** - the model orchestrates multiple grids + back-ends (smart import → enrich → export to BI tool) The grid is designed to support all three. The headless engine + the imperative `SvGridApi` together form a clean tool surface that any agent SDK can consume. > **Looking for the in-grid AI features?** See > [AI assistant - Pro](./ai.md) for NL filter / smart fill / > summarise / classify - the agent surface this page describes is > what you'd build ON TOP of those. ## Pattern 1: Read-only summary agent ```svelte ``` **Trade-off:** simple to build, no tool calling, but the model only sees the rows you sent it. Fine for "what's the top performer this quarter?"; not for "filter to last 30 days" - that needs Pattern 2. ## Pattern 2: Stateful UI agent The grid's imperative API is a clean tool surface. Each `SvGridApi` method becomes one function the model can call. ```ts import OpenAI from 'openai' import { z } from 'zod' const tools = [ { type: 'function', function: { name: 'setFilter', description: 'Apply a column filter. Use to narrow the visible rows.', parameters: { type: 'object', required: ['columnId', 'operator', 'value'], properties: { columnId: { type: 'string' }, operator: { type: 'string', enum: ['contains', 'equals', 'startsWith', 'greaterThan', 'lessThan', 'between'] }, value: { type: 'string' }, valueTo: { type: 'string', description: 'Upper bound for "between" only.' }, }, }, }, }, { type: 'function', function: { name: 'setSort', parameters: { type: 'object', required: ['columnId', 'direction'], properties: { columnId: { type: 'string' }, direction: { type: 'string', enum: ['asc', 'desc'] } } }, }, }, { type: 'function', function: { name: 'setGroupBy', parameters: { type: 'object', required: ['columnIds'], properties: { columnIds: { type: 'array', items: { type: 'string' } } } }, }, }, { type: 'function', function: { name: 'clearAllFilters', parameters: { type: 'object' } }, }, ] ``` Wire the tool calls back to your live `api` reference: ```ts async function runAgent(prompt: string) { let messages = [ { role: 'system', content: `You drive a data grid. Columns: ${JSON.stringify(api!.getColumns())}.` }, { role: 'user', content: prompt }, ] for (let turn = 0; turn < 6; turn += 1) { // safety bound const r = await client.chat.completions.create({ model: 'claude-sonnet-4-6', messages, tools, tool_choice: 'auto', }) const msg = r.choices[0]!.message messages.push(msg) if (!msg.tool_calls?.length) return msg.content for (const call of msg.tool_calls) { const args = JSON.parse(call.function.arguments) switch (call.function.name) { case 'setFilter': api!.setFilter(args.columnId, args); break case 'setSort': api!.setSort(args.columnId, args.direction); break case 'setGroupBy': api!.setGroupBy(args.columnIds); break case 'clearAllFilters': api!.clearAllFilters(); break } messages.push({ role: 'tool', tool_call_id: call.id, content: 'ok' }) } } } ``` User types *"show me last quarter's deals over $50k, grouped by region"* and the agent calls `setFilter('sellDate', { ... })` + `setFilter('amount', { operator: 'greaterThan', value: '50000' })` + `setGroupBy(['region'])` in a single turn. **The whole `SvGridApi` is on the menu.** Wrap as many or as few methods as you want; the model only calls what you expose. ## Pattern 3: Autonomous workflow agent The grid becomes one node in a longer chain. Typical shape: ```ts const agent = new Agent({ tools: [ fetchCsvFromS3, // pull raw data aiSmartPaste, // parse to typed rows (uses /api/ai endpoint) runValidations, // your domain validations pushToGrid, // api.addRows(...) waitForUserApproval, // pauses for human-in-the-loop exportToBigQuery, // api.exportData({ format: 'csv', ... }) + push ], }) await agent.run('Process today\'s sales batch from s3://acme/sales/2026-06-06.csv') ``` The grid is **the visible state** the human can audit between steps - which is exactly what makes a workflow agent trustworthy: every intermediate result lands in a sortable, filterable table the user can inspect. ## Sandboxing rules When an LLM is calling grid methods, three boundaries keep things sane: 1. **Whitelist tools at the top level.** Never expose `eval` or arbitrary JS. The `SvGridApi` methods above are the only surface the model needs. 2. **Validate every tool argument** before invoking. The JSON Schemas at [`/schemas/`](./mcp-server.md) cover every input shape; use `ajv` or `zod` to check. 3. **Bound the agent loop.** A maximum-turns counter (6 is plenty for grid manipulation) prevents runaway calls. Combine with a per-turn token budget. ## Common workflows shipped as MCP prompts The [MCP server](./mcp-server.md) ships three pre-built prompts that implement the above patterns: - **`/svgrid:nl-to-grid-state`** - Pattern 2 with the tool set wired up - **`/svgrid:csv-to-typed-rows`** - Smart-paste an arbitrary CSV into a typed row array with confidence per row - **`/svgrid:summarise-view`** - Pattern 1 grounded in `api.getDisplayedRows()` ## Worked example: NL → Pivot Live in [demo 75 (AI Smart Paste)](https://svgrid.com/#/demos/75-ai-smart-paste) and [demo 52 (Pivot designer)](https://svgrid.com/#/demos/52-pivot-table) - both ship in the gallery. ## Failure modes | Symptom | Cause | Fix | | ------------------------------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------- | | Model invents columns that don't exist | No grounding on the live column set | Pass `api.getColumns()` in the system prompt every turn | | Model calls `setFilter('Status', ...)` with the wrong case | Column ids are case-sensitive | Include the column ids in the system prompt (snake_case vs PascalCase) | | Multi-step chain forgets the row count drops | Each tool call doesn't return the new visible row count | Return `api.getDisplayedRows().length` from each handler | | Agent loops forever | No max-turns bound | Always cap the loop (5-10 turns is plenty for grid manipulation) | ## See also - [LLM grounding](./llm-grounding.md) - the static doc files agents read - [MCP server](./mcp-server.md) - turnkey integration for Claude Desktop / Cursor / Zed - [AI assistant - Pro](./ai.md) - the in-grid NL features (not the agent layer) - [Architecture](./architecture.md) - what state lives where (agents need to know) ## Frequently asked questions ### Can an AI agent control the SvGrid data grid? Yes. The imperative `SvGridApi` (filter, sort, select, set values, expand, page) is exactly the surface an agent drives. This page covers three patterns, from a read-only summary agent to a full read-write agent that mutates grid state. ### What is the safest way to let an LLM drive the grid? Start read-only: let the model describe and query the grid before it writes. When you grant write access, route it through the same `SvGridApi` calls a user action would trigger, so validation and dirty-tracking still apply. ### Do I need the MCP server to build a grid agent? No. The MCP server is a turnkey integration for desktop AI clients; for a custom in-app agent you call `SvGridApi` directly. Both are documented here and in the MCP server guide. # AI Smart Paste Drop any shape of contact / lead / row data into a text area - CSV, TSV, JSON, vCard, Markdown table, an email signature block, or a single line of prose - and the parser maps it into typed rows your grid can insert or merge. This is the demo your sales / customer-success / RevOps users want. Their day already has them pasting from Salesforce, Slack, Excel, business cards, and signature blocks; the parser absorbs the chaos and the grid renders the cleaned result.
## What the parser handles Six input formats are detected automatically, tried in this order: | Format | Example trigger | Notes | |-------------------|----------------------------------------------------------------|----------------------------------------------------| | **vCard** | `BEGIN:VCARD` | RFC 6350. iOS / macOS / Google Contacts export. | | **Markdown table**| Lines starting/ending with `|`, with a `---` separator | GitHub, Slack threads, docs. | | **JSON** | Input starts with `[` or `{` | Any field names; multi-language matched. | | **CSV / TSV / PSV**| Consistent tab / comma / semicolon / pipe delimiter | Multi-language headers (`Nom`, `Téléphone` etc). | | **Signature blocks**| Multi-line blocks separated by blank lines containing an email| `Name ` form, "Role, Company" lines. | | **Free-form prose**| Anything else, one line per record | Extracts by signal (email regex, phone regex). | On top of detection, every parsed row goes through three **normalization** passes before the preview panel renders it: 1. **Email typo correction** - common domain typos (`gmial.com`, `gmail.con`, `gosling.con`) are auto-fixed and surfaced as an info note on the row. 2. **Phone normalization** - any phone is rewritten to a consistent `+CC AAA BBB CCCC` shape (US, UK, DE, FR, JP recognized; 10-digit bare numbers assumed North American). 3. **Name cleaning** - titles (`Dr.`, `Prof.`, `Mr.`) and suffixes (`Jr.`, `III`, `PhD`) are stripped; `"Last, First"` is rewritten to `"First Last"`. Duplicate detection runs last: rows with the same normalized email collapse into one, with missing fields merged from the duplicate. ## Complete drop-in example A minimal contact list with the Smart Paste panel above the grid. Paste any of the formats below into the textarea and click **Parse with AI**. ```svelte
{#if parsed.length > 0}
Preview:
    {#each parsed as p (p.id)}
  • {p.fullName} · {p.email} · {p.company}
  • {/each}
{/if}
(api = next)} />
``` For the full parser (vCard, Markdown table, signature blocks, multi-language headers, typo correction, phone normalization, de-duplication), read the [`assistant()` implementation in demo 75](../../examples/src/demos/75-ai-smart-paste.svelte) and copy it verbatim - the parser is 350 lines of pure TypeScript with no library dependencies. ## Try these input formats Each one runs through the parser and produces 2-3 rows in the preview panel. ### vCard ```text BEGIN:VCARD VERSION:3.0 FN:Brendan Eich ORG:Brave Software TITLE:CEO EMAIL;TYPE=WORK:brendan@brave.com TEL;TYPE=CELL:+1 415 555 0188 END:VCARD BEGIN:VCARD VERSION:3.0 FN:Anders Hejlsberg ORG:Microsoft TITLE:Technical Fellow EMAIL;TYPE=WORK:anders.h@microsoft.com TEL;TYPE=WORK:+1 425 555 0177 END:VCARD ``` ### Markdown table with multi-language headers ```text | Name | Email | Company | Rol | Téléphone | |---|---|---|---|---| | Grace Hopper | grace@nps.mil | US Navy | Rear Admiral | +1 202 555 0133 | | Ken Thompson | ken@bell-labs.com | Bell Labs | Researcher | +1 908 555 0124 | ``` ### Email signature blocks ```text Donald E. Knuth Professor Emeritus, Stanford University Phone: (650) 555-0111 Barbara Liskov Institute Professor at MIT CSAIL Cell: +1.617.555.0142 ``` ### Messy real-world paste Title prefixes, "Last, First" inversion, email typo, and varied phone formats - the parser cleans every field: ```text Dr. Margaret Rhodes; margaret@gmial.com; Rhodes Capital LLC; Managing Partner; (212) 555-0188 Wirth, Niklaus; niklaus.wirth@inf.ethz.ch; ETH Zurich; Professor Emeritus; +41 44 555 0166 "James Gosling" ; Amazon Web Services; Distinguished Engineer; 1-415-555-0177 ``` After parsing: - `Dr. Margaret Rhodes` → `Margaret Rhodes` - `Wirth, Niklaus` → `Niklaus Wirth` - `gmial.com` → `gmail.com` (logged as `Fixed domain typo → gmail.com`) - `gosling.con` → `gosling.com` (same) - `(212) 555-0188` → `+1 212 555 0188` - `1-415-555-0177` → `+1 415 555 0177` ## Swapping in a real LLM The bundled `assistant()` is fully deterministic and runs in the browser. To route through a real model instead - GPT, Claude, Gemini, Llama on your infra - replace the `assistant()` body with: ```ts async function assistant(text: string): Promise<{ rows: ParsedRow[]; log: string[] }> { const res = await fetch('/api/smart-paste', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text }), }) if (!res.ok) throw new Error(`Parser failed: ${res.status}`) return res.json() as Promise<{ rows: ParsedRow[]; log: string[] }> } ``` On the server side, the model receives the raw paste and a schema for `ParsedRow`, and returns the same shape. Useful when you need: - Domain-specific extraction (medical, legal, financial) - A model with knowledge of your CRM's record shapes - Audit logging of every paste for compliance The bundled parser stays the deterministic fallback - call it client-side when the network call fails or for the 90% of cases that don't need a model. ## See also - [Demo 75 - AI Smart Paste](../../examples/src/demos/75-ai-smart-paste.svelte) - the full parser, 5 sample tiles, preview panel, per-row insert/update/skip control, commit through `api.setCellValue` - [AI assistant - Pro](./ai.md) - the `api.ai.filter`, `api.ai.smartFill`, `api.ai.summarize`, `api.ai.classify` helpers - [Import](./import.md) - file picker for xlsx / csv / tsv / json # AI assistant - Pro Bring a language model into your grid with four helpers that stay strictly model-agnostic: **natural-language filter**, **smart fill**, **summarise**, and **classify**. Ships in the paid **[sv-grid-pro](https://www.npmjs.com/package/sv-grid-pro)** add-on; the Community build does not include these features. Run all four helpers live - the demo below is wired to the bundled deterministic `mockAIProvider`, so no keys required:
## What it is `installPro(api)` (the same call you use for export and print) augments your `SvGridApi` with an `ai` namespace: ```ts api.ai.filter(query, opts?) // NL query -> filter + sort plan api.ai.smartFill(opts) // examples -> proposed column values api.ai.summarize(opts) // row / selection / group / all -> text + bullets api.ai.classify(opts) // free-text cells -> bucketed labels ``` Every call routes through one `AIProvider` you register at app boot. The grid never bundles a model client - you keep full control of model choice, routing, and data handling. ## When to use it - **NL filter** is the highest-leverage feature: it replaces a dozen per-column filter operators with one search box for analyst users. - **Smart fill** is the killer feature for spreadsheet-style entry: type one or two examples in a column, accept the rest with one click. - **Summarise** is for dashboards where the user wants a "what's interesting here?" paragraph without clicking through every cell. - **Classify** is for triage workflows where free-text rows need a consistent bucket label before downstream automation runs. If you don't need natural-language anywhere, skip this module entirely - the rest of `sv-grid-pro` doesn't depend on it. ## Setting up the provider The grid talks to your model through a single async function. Wire it once at app startup: ```ts import { setAIProvider, type AIProvider } from 'sv-grid-pro' const myProvider: AIProvider = async ({ prompt, responseFormat, signal, task, maxOutputTokens }) => { const r = await fetch('/api/ai', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ prompt, responseFormat, task, maxOutputTokens }), signal, }) if (!r.ok) throw new Error(`AI provider returned ${r.status}`) return r.text() } setAIProvider(myProvider) ``` A few design points worth knowing: - **`responseFormat: 'json'`** is what `aiFilter`, `aiSmartFill`, `aiSummarize`, and `aiClassify` all request. Tell your model to return strict JSON only; the grid `JSON.parse`s the result. (A common resilience trick: the grid strips a single markdown code fence automatically so models that wrap JSON in ` ```json ... ``` ` still parse cleanly.) - **`signal`** is forwarded so consumers can cancel in-flight calls when the user moves on. - **`task`** is a tag (`'filter' | 'smart-fill' | 'summarize' | 'classify'`) - useful when you want to route different tasks to different models (a cheap one for filter, a stronger one for summarise). - **`maxOutputTokens`** is a soft hint, also useful for cost routing. For testing or for demo purposes, the package ships a deterministic `mockAIProvider` that returns plausible canned shapes per task. Wire it in development: ```ts import { setAIProvider, mockAIProvider } from 'sv-grid-pro' setAIProvider(mockAIProvider) ``` ## 1. Natural-language filter Translate a sentence into a filter + sort plan against the current grid's columns. The grid embeds the column schema (names, types, sample values) in the prompt so the model picks real field names rather than hallucinating. ```ts const plan = await api.ai.filter('accounts losing momentum in EMEA, by NPS') // { // filters: [ // { field: 'region', operator: 'equals', value: 'EMEA' }, // { field: 'nps', operator: 'lessThan', value: '30' }, // ], // sort: [{ field: 'nps', desc: false }], // rationale: 'EMEA region, low NPS, sorted ascending.', // } ``` By default `aiFilter` returns the plan but **does not apply it** to the grid - that lets you show a preview ("here's what I'd do, accept?") before committing. Pass `{ apply: true }` to apply directly: ```ts await api.ai.filter('big EMEA deals', { apply: true }) ``` ### Hallucination guard If the model invents a column name that doesn't exist in your data, the helper silently drops that clause rather than passing it to `setFilter`, which would otherwise throw. This is intentional: you'd rather lose a clause than crash the page. ## 2. Smart fill User provides one or two worked examples for a column; the grid asks the model to propose values for the remaining empty cells. ```ts const result = await api.ai.smartFill({ field: 'tier', examples: [ { input: { company: 'Northwind' }, output: 'enterprise' }, { input: { company: 'Helios' }, output: 'growth' }, ], }) // result.predictions: [{ rowIndex, value, confidence }, ...] ``` You decide what to do with the predictions - accept-all, accept-per-cell, write them to a `proposedTier` field on the row data and render a "✓" button in the cell, etc. The example demo does per-cell accept with a confidence-coloured pill. If `targetRowIndices` is omitted, the helper auto-selects rows whose current `field` value is `null`, `undefined`, or `''`. ## 3. Summarise Drop a slice of the grid into the model and ask for a one-paragraph + bulleted summary. Four target modes: ```ts await api.ai.summarize({ target: { kind: 'all' } }) await api.ai.summarize({ target: { kind: 'row', rowIndex: 5 } }) await api.ai.summarize({ target: { kind: 'selection', rowIndices: [1,2,3] } }) await api.ai.summarize({ target: { kind: 'group', field: 'region', value: 'EMEA' } }) ``` For large slices the grid samples rows uniformly so the prompt stays under a sensible token budget. The response shape: ```ts { text: 'One paragraph...', bullets: ['punchy bullet 1', 'punchy bullet 2', ...], highlightedFields: ['arr', 'nps'], // columns the summary leans on } ``` The optional `question` parameter biases the summary toward the columns that answer the question. ## 4. Classify Bucket free-text cells into one of a known set of labels: ```ts const r = await api.ai.classify({ inputField: 'notes', outputField: 'sentiment', classes: ['at-risk', 'expanding', 'steady'], classDescriptions: { 'at-risk': 'churn signals, escalations, lost champion', 'expanding': 'new modules, more seats, additional regions', 'steady': 'no signals in either direction', }, }) ``` The helper filters out any predictions whose value is not in `classes`, so downstream code can trust the output is a clean enum. ## Subscribing the grid to predictions Cell snippets cache their output by `(row, column)`. If you store predictions in a *separate* state ref - e.g. a `Map` that the snippet looks up at render time - the grid won't know to re-render when that map changes. **Write the proposal onto the row itself instead:** ```ts async function runClassify() { const result = await api.ai.classify({ /* ... */ }) const byIdx = new Map(result.predictions.map((p) => [p.rowIndex, p])) // Re-assign the data array - Svelte 5 reactivity picks this up and the // grid re-renders every visible cell. accounts = accounts.map((a, i) => { const p = byIdx.get(i) return p ? { ...a, proposedSentiment: p.value, sentimentConfidence: p.confidence } : a }) } ``` Then your cell snippet reads `props.row.proposedSentiment` and re-renders exactly when expected. ## License gate Every AI call routes through the same polite license gate as `exportData` and `print`: - **No key set** → call still runs, the grid shows an "unlicensed" watermark and the console logs a one-time nudge directing the user to pricing. This is intentional for demos and evaluation. - **`SVPRO-DEV-...` / `SVPRO-EVAL-...`** → call runs, a one-time `console.info` notes that the key is not for production. - **Other valid `SVPRO-` key** → call runs silently. - **Malformed prefix or revoked key** → call throws. ## See also - [Demo 21 - Export + Print](../../examples/src/demos/21-export-and-print.svelte) - the other Pro surface, installed by the same `installPro(api)` call. - [Demo 51 - AI assistant](../../examples/src/demos/51-ai-assistant.svelte) - the full demo this page documents, with all four helpers wired to the mock provider. ## Frequently asked questions ### What AI features does SvGrid have? The `sv-grid-pro` AI assistant ships four model-agnostic helpers: natural-language filter, smart fill, summarise, and classify. They run through a bring-your-own-model adapter, so you wire in your own LLM endpoint. ### Which LLM does SvGrid use? None by default - it is model-agnostic. You supply an adapter for OpenAI, Anthropic Claude, a local model, or anything else. The demo ships a mock provider so you can evaluate the flow without an API key. ### Is my grid data sent to an AI provider? Only if you wire one up and invoke a helper. SvGrid itself makes no network calls; the AI helpers send exactly the prompt you construct to the adapter you configure, so you control what leaves the browser. # API reference A flat index of every Stable export. Each entry links to the topic page that covers it in depth. For the formal TypeScript shapes, inspect the `.d.ts` files in `node_modules/sv-grid-community/dist` - they're the source of truth and ship with full JSDoc. > The "Tier" column reflects the badges from > [API stability](./api-stability.md). Anything not listed here is > Internal and may move under your feet. ## sv-grid-community ### Components | Export | Tier | What it is | | --------------------- | ------------ | ---------------------------------------------------------------------------------------------- | | `SvGrid` | Stable | The render component. See [Getting Started §1](../getting-started.md#1-your-first-grid-in-60-seconds). | | `FlexRender` | Stable | Helper for rendering `cell` / `header` / `footer` templates inside custom UIs. | ### Engine | Export | Tier | What it is | | --------------------- | ------------ | --------------------------------------------------------------------------------------------- | | `createSvGrid` | Stable | Build a headless grid instance; the `` component is a thin wrapper around this. | | `tableFeatures` | Stable | Bundle features (sorting, filtering, ...) into a single typed object for use in column generics. | | `createGridState` | Stable | Controlled-state helper that returns a `[get, set]` pair compatible with Svelte's `$state`. | ### Features (row models) | Export | Tier | Enables | | ---------------------------- | ------ | ------------------------------------------------ | | `rowSortingFeature` | Stable | Column-header click to sort, programmatic sort. | | `columnFilteringFeature` | Stable | Per-column filters (operator + value + menu). | | `rowSelectionFeature` | Stable | Single + multi row selection with checkboxes. | | `columnGroupingFeature` | Stable | Multi-level column header groups. | | `rowExpandingFeature` | Stable | Expand / collapse rows (master-detail, trees). | ### Imperative API (`SvGridApi`) Reached via ` /* ... */}>`. Every method is Stable. | Method | Purpose | | ----------------------------------- | ------------------------------------------------------------------ | | `getCellValue(rowIndex, columnId)` | Read a cell value. | | `setCellValue(rowIndex, columnId, v)` | Write a cell value, runs through the same write pipeline as inline edit. | | `addRow(row, position?)` | Insert one row at top / bottom / numeric index. | | `addRows(rows, position?)` | Bulk insert. | | `removeRow(rowIndex)` | Remove one row by data-array index. | | `removeRows(rowIndices)` | Bulk remove. | | `addColumn(column, position?)` | Insert one column at left / right / numeric index. | | `addColumns(columns, position?)` | Bulk insert. | | `removeColumn(columnId)` | Remove one column by id (or field). | | `setColumnVisible(columnId, visible)` / `isColumnVisible(columnId)` | Column visibility. | | `setSort(columnId, dir)` / `clearSort()` | Programmatic sort. | | `setGroupBy(columnIds)` | Set the group-by columns. | | `setFilter(columnId, filter)` / `clearFilter(columnId)` / `clearAllFilters()` | Programmatic filtering. | | `getFilters()` | Snapshot the active column-menu filters. | | `getDisplayedRows()` | Snapshot the currently displayed rows (after filter/sort/page). | | `getData()` | Snapshot the raw `data` prop. | | `clearRowSelection()` | Untick every selected row. | ### Column definition (`ColumnDef`) | Property | Tier | Notes | | ------------------- | ------ | -------------------------------------------------------------------- | | `id` | Stable | Use when no `field` (e.g. computed columns). | | `field` | Stable | Row property the cell reads + writes by default. | | `accessorFn` | Stable | Replace the `field`-based read with a function. Used heavily by pivot. | | `header` | Stable | String or a render template. | | `footer` | Stable | Same shape as header. | | `cell` | Stable | Custom cell render template. | | `columns` | Stable | Nested children → multi-row header groups. | | `editorType` | Stable | `'text'` / `'number'` / `'date'` / `'datetime'` / `'checkbox'` / `'list'` / `'chips'`. | | `editorOptions` | Stable | Option list for `list` + `chips` editors. Static array or `(row) => array`. | | `editorMultiple` | Stable | Allow multi-select on `list` / `chips`. | | `editorSeparator` | Stable | Join character for multi-value display. | | `editable` | Stable | `boolean | (ctx) => boolean` - per-column or per-cell gate. | | `format` | Stable | Built-in formatter (`number` / `currency` / `percent` / `date`). | | `formatter` | Stable | Free-form formatter callback. | | `width` | Stable | Pixel width (default falls back to grid's `columnWidth`). | ### Types | Export | Tier | Purpose | | --------------------- | ------ | -------------------------------------------------------------------- | | `ColumnDef` | Stable | Column definition shape (see table above). | | `CellContext` | Stable | The `ctx` object passed to `cell` / `editable` / `formatter`. | | `HeaderContext` | Stable | The `ctx` object passed to `header` / `footer`. | | `Row` / `Cell` / `Column` / `Header` / `HeaderGroup` | Stable | Engine objects you can read inside templates. | | `SvGridApi` | Stable | The imperative API shape (every method above is on this interface). | | `RowData` | Stable | Alias for `Record`; the row shape constraint. | | `TableFeatures` | Stable | The shape of the bag `tableFeatures()` returns. | ### Static utilities Re-exports from `'sv-grid-community/static-functions'` for use outside a Svelte 5 component: | Export | Tier | What it is | | ---------------------------- | ------ | ------------------------------------------------------- | | `sortRows(rows, sorting)` | Stable | Pure sort helper. Same comparator the grid uses. | | `filterRows(rows, filters)` | Stable | Pure filter helper. | | `groupRows(rows, groupBy, aggregators)` | Stable | Pure group-and-aggregate helper. | | `getDisplayedRows(rows, state)` | Stable | One-shot pipeline (filter → sort → group → page). | ## sv-grid-pro ### Installation | Export | Tier | What it is | | ------------ | ------ | ------------------------------------------------------------------------------------------ | | `installPro(api)` | Stable | Augments a `SvGridApi` with `exportData`, `print`, `importData`, and `ai.*`. | | `ProGridApi` | Stable | The post-install API shape. | ### License | Export | Tier | Purpose | | ------------------- | ------ | ------------------------------------------------------------- | | `setLicenseKey(s)` | Stable | Register a key at startup. | | `clearLicenseKey()` | Stable | Tear down the key on logout, etc. | | `getLicenseKey()` | Stable | Inspect the current key (returns `null` when unset). | | `isLicenseKeySet()` | Stable | Boolean for "any key registered". | | `hasValidLicense()` | Stable | Boolean for "key passes prefix + revocation check". | | `assertProLicensed()` | Stable | Throws on a malformed / revoked key, soft-warns when unset. | | `dismissUnlicensedNudge()` | Stable | Suppress the console one-time nudge. | ### Export + print | Export | Tier | Purpose | | ------------------------ | ------ | -------------------------------------------------------------------- | | `exportGrid(api, opts)` | Stable | Same as `api.exportData(opts)` for use without `installPro`. | | `printGrid(api, opts?)` | Stable | Same as `api.print(opts)`. | | `ExportFormat` | Stable | `'xlsx' | 'pdf' | 'csv' | 'tsv' | 'html'`. | | `ExportOptions` / `ExportColumn` | Stable | Option shape (see [Data export and printing](./export.md)). | | `PrintOptions` | Stable | Option shape. | ### Import | Export | Tier | Purpose | | --------------------- | ----------------------------- | -------------------------------------------------------- | | `importData(api, opts)` | Stable, accepting feedback | Same as `api.importData(opts)`. | | `ImportFormat` | Stable, accepting feedback | `'xlsx' | 'csv' | 'tsv' | 'json' | 'auto'`. | | `ImportOptions` | Stable, accepting feedback | File + format + column map + validator + commit options. | | `ImportResult` | Stable, accepting feedback | `{ headers, rows, errors, skipped, total, format }`. | | `ImportColumnMap` | Stable, accepting feedback | `Record`. | | `ImportRowError` | Stable, accepting feedback | `{ rowIndex, field, message }`. | | `ImportValidator` | Stable, accepting feedback | `(row, i) => Array<{ field, message }>`. | ### AI assistant | Export | Tier | Purpose | | --------------------- | -------------------------- | ------------------------------------------------------------------------- | | `setAIProvider(fn)` | Stable, accepting feedback | Register the BYO model adapter. | | `getAIProvider()` / `hasAIProvider()` | Stable, accepting feedback | Inspect registration. | | `mockAIProvider` | Experimental | Deterministic canned shapes per task; for demos + tests only. | | `AIProvider` | Stable, accepting feedback | `(req: AIRequest) => Promise`. | | `AIRequest` | Stable, accepting feedback | `{ prompt, responseFormat, signal, task, maxOutputTokens }`. | | `AITask` | Stable, accepting feedback | `'filter' | 'smart-fill' | 'summarize' | 'classify'`. | | `aiFilter` | Stable, accepting feedback | NL query → filter/sort plan (with optional `apply`). | | `AIFilterOptions` / `AIFilterResult` / `AIFilterClause` / `AISortClause` | Stable, accepting feedback | Shapes. | | `aiSmartFill` | Stable, accepting feedback | Examples → predicted values for empty cells. | | `AISmartFillOptions` / `AISmartFillResult` / `AISmartFillExample` | Stable, accepting feedback | Shapes. | | `aiSummarize` | Stable, accepting feedback | Row / selection / group / all → text + bullets + highlighted fields. | | `AISummarizeOptions` / `AISummarizeTarget` / `AISummary` | Stable, accepting feedback | Shapes. | | `aiClassify` | Stable, accepting feedback | Free-text cells → bucketed labels. | | `AIClassifyOptions` / `AIClassifyResult` | Stable, accepting feedback | Shapes. | ## Subpath exports Both packages support fine-grained imports so you only pay for what you use: ```ts import { setAIProvider } from 'sv-grid-pro/ai' import { importData } from 'sv-grid-pro/import' import { exportGrid } from 'sv-grid-pro/export' import { printGrid } from 'sv-grid-pro/print' ``` The `package.json` `exports` map lists the supported paths. ## See also - [API stability](./api-stability.md) - what the tiers mean + semver policy. - [Error reference](./errors.md) - every typed error this surface throws. - [Testing and quality](./testing-and-quality.md) - the unit-test coverage backing each entry above. # API stability & semver policy The promise SvGrid makes to you about which exports you can lean on, how breaking changes are communicated, and how long deprecated APIs stick around. ## Semver, applied SvGrid follows [semver](https://semver.org/) strictly **for everything exported from the package root**: ```ts import { SvGrid, tableFeatures, /* ... */ } from 'sv-grid-community' ``` Anything reached via `'sv-grid-community/internal'` or a deeper path is **not** covered. We may rename, reshape, or remove those between any two minor releases. | Change | Bump | | --------------------------------------------------- | --------- | | Remove or rename a top-level export | major | | Remove an existing public prop | major | | Change the meaning of an existing prop / option | major | | Tighten a public TypeScript type (becomes stricter) | major | | Add a new optional prop / option / export | minor | | Loosen a TypeScript type (becomes wider) | minor | | Fix a bug that produces a different output value | patch (documented in the changelog) | | Internal refactor with no observable change | patch | ## Stability tiers Every public symbol is labelled in its JSDoc. The badges that show up in the API reference (and at the top of each topic page): | Badge | Meaning | | -------------- | ---------------------------------------------------------------------------------- | | **Stable** | Covered by the semver promise. We will not break you between majors. | | **Stable, accepting feedback** | Same semver protection as Stable. We're collecting feedback on the shape; if it changes it will be in a major. | | **Experimental** | May change in any minor release. Use only when you can absorb churn. | | **Deprecated** | Still works. Will be removed in a future major. The doc + the `@deprecated` JSDoc tag say which release. | | **Internal** | Not part of the public API. Imports may break in any release. | The TypeScript types double as the source of truth: every Stable export has explicit `@public` JSDoc; every Internal one has `@internal`. Internal symbols are excluded from the published `.d.ts`. ## Current stability map The package surface as of the current shipping version: ### `sv-grid-community` | Surface | Tier | | ------------------------------------ | ----------------- | | `` component + every prop on it | Stable | | `createSvGrid`, `tableFeatures` | Stable | | `renderSnippet` | Stable | | `rowSortingFeature`, `columnFilteringFeature`, `rowSelectionFeature`, `columnGroupingFeature`, `rowExpandingFeature` | Stable | | `SvGridApi` (the imperative API) | Stable | | Header-cell render snippets | Stable, accepting feedback | | Column-group multi-row headers | Stable (since v1.x) | | `createGridState` | Stable | | Anything imported via a deep path (`/internal`, `/test-utils`) | Internal | ### `sv-grid-pro` | Surface | Tier | | ------------------------------------ | ----------------- | | `installPro`, `ProGridApi` | Stable | | `exportGrid`, `ExportOptions` | Stable | | `printGrid`, `PrintOptions` | Stable | | `importData`, `ImportOptions`, `ImportResult` | Stable, accepting feedback | | `setAIProvider`, `AIProvider` | Stable, accepting feedback | | `aiFilter`, `aiSmartFill`, `aiSummarize`, `aiClassify` + their option types | Stable, accepting feedback | | `mockAIProvider` | Experimental - we may rename or expand the shape | | License helpers (`setLicenseKey`, etc.) | Stable | ## Deprecation lifecycle When something Stable is about to go away: 1. The JSDoc gets `@deprecated since vX.Y` with a one-line pointer to the replacement. 2. The TypeScript declaration starts emitting a deprecation warning (most editors render it as struck-through autocomplete). 3. The next **minor** release ships with the deprecation in place but the old API still working. 4. The next **major** release removes the old API. 5. The deprecation entry stays in the changelog under a "Removed in vN" heading for the version that removed it. In practice this means **at least one minor release** between marking something deprecated and removing it - often two. ## Release cadence - **Patch releases** ship on demand, sometimes weekly, sometimes monthly. Every bug fix that doesn't change the shape goes here. - **Minor releases** ship roughly monthly. Every new feature lands in a minor. - **Major releases** ship at most twice a year. Major version skips (jumping from v2 to v4) happen only if a major change is so invasive the bridge isn't worth the maintenance cost. The changelog ([`CHANGELOG.md`](../../CHANGELOG.md)) is canonical. Every release has a dated entry with three sections: Breaking, Features, Fixes. Breaking changes carry a migration paragraph. ## What "stable" doesn't promise - **CSS class names**. We use `.sv-grid-*` consistently, but they're not part of the semver contract. Override them at your peril. - **DOM structure**. The grid emits whatever markup it needs to. Querying `tbody tr td:first-child` from your test is your responsibility, not ours. - **Console messages**. Warning text is editorial; we may reword. Error class names + types ARE stable - the [error reference](./errors.md) is the canonical list. - **Bundler output paths**. The published package exposes the `exports` map; importing from a non-mapped path may break at any time. ## Deprecation log The single source of truth for what's been marked deprecated, when, why, and what to use instead. Read top-down for newest first; agents can parse this table directly. | Symbol | Deprecated since | Removal target | Replacement | Reason | | -------------------------------- | ---------------- | -------------- | -------------------------------------------------------- | --------------------------------------------------- | | `` | v1.4 | v2.0 | `filterMode="menu"` + `showFilterRow={true}` if you really want both | The two modes were mutually exclusive in the type but both rendered when set; the split makes the intent explicit | | `mockAIProvider` named export | v1.6 | v2.0 | `import { mockAIProvider } from 'sv-grid-pro/test-utils'` | Test helpers are moving out of the production bundle | | `ColumnDef.cellClassRules` | v1.3 | v2.0 | `cellClass: (ctx) => Record` | One field is enough; the rules format was unnecessary indirection | | `` | v1.5 | v2.0 | `onActiveCellChange` + your own click handler | The grid intercepts click already for selection / editing; surfacing it as a prop double-fires | Empty section means "no current deprecations" - we'd link the previous-version deprecation log in the changelog. ### Format your own integrations can parse Every entry follows the same six-column shape so an LLM or an upgrade-assistant script can read this table and propose edits. The pattern is: ``` | | | | | | ``` When v2 ships, the "Removed in v2" entries move to the changelog under a `### Removed` section and out of this table. ## Long-term support - **Currently shipping major** (v1.x) - all releases supported with bug fixes + security patches. - **Previous major** (when v2 ships, v1 enters LTS) - security patches only, for **12 months** after v2.0.0. - **Older majors** - no support. For Pro customers, our support contract may extend LTS beyond 12 months on a case-by-case basis - contact `sales@jqwidgets.com`. ## See also - [API reference](./api-reference.md) - every export with its tier badge. - [Error reference](./errors.md) - the stable error class + message inventory. - [Migrating from AG Grid](./migrating-from-ag-grid.md) - if you're coming from a previous-generation grid. # Architecture overview A one-page mental model that should let you reason about every other topic in the docs. SvGrid is a strict three-layer system - if you know which layer a piece of code lives in, you know what it can and cannot do. ## The three layers ``` ┌─────────────────────────────────────────────────────────────┐ │ Layer 3 │ render component (Svelte 5) │ │ │ - DOM, scroll, virtualization, editor popovers │ │ │ - keyboard handlers, pointer events │ │ │ - sticks the headless engine to a viewport │ ├───────────┼─────────────────────────────────────────────────┤ │ Layer 2 │ Headless engine (createSvGrid) │ │ │ - column model + row model pipeline │ │ │ - sort, filter, group, paginate, expand │ │ │ - aggregators, accessors, comparators │ │ │ - 100% pure functions, no DOM │ ├───────────┼─────────────────────────────────────────────────┤ │ Layer 1 │ Your data + your column definitions │ │ │ - the only thing YOU author │ │ │ - plain TypeScript: arrays, objects, types │ └───────────┴─────────────────────────────────────────────────┘ ``` Layer 1 is yours. Layer 2 is `sv-grid-community` minus the renderer. Layer 3 is the `` component everyone uses by default. **You can use Layer 2 without Layer 3.** That's the headless promise: if you want to render the grid yourself - in Tailwind cards, a print PDF template, a custom virtualization layer - import `createSvGrid` and read the state directly. ## Data flow on every render ``` raw data ──► engine pipeline ──► visible rows ──► renderer (you) (Layer 2) (Layer 2 out) (Layer 3) │ ▼ ┌────────────────────────────┐ │ 1. coreRowModel │ shape data into Row objects │ 2. filteredRowModel │ apply column + global filters │ 3. sortedRowModel │ apply sort spec │ 4. groupedRowModel │ apply groupBy + aggregators │ 5. expandedRowModel │ flatten expanded groups │ 6. paginatedRowModel │ slice the visible page └────────────────────────────┘ ``` Every "feature" you register in `tableFeatures({ ... })` plugs one or more row models into this pipeline. Disable a feature and that stage no-ops. The pipeline runs once per state change, **not per scroll frame** - virtualization is purely a presentational concern. ## The two APIs you'll use ### Declarative (the `` props) 99% of consumers stop here. You author `data`, `columns`, and `features`, then handle events from props: ```svelte ``` ### Imperative (`SvGridApi`) For toolbars, ribbons, keyboard shortcuts that need to drive the grid, ask for the API via `onApiReady`: ```svelte { api.setSort('name', 'asc') api.setFilter('region', { operator: 'equals', value: 'EMEA' }) }} /> ``` See the [API reference](./api-reference.md) for the full surface. ## State ownership Two questions decide where each piece of state lives: | Question | Lives in | | ------------------------------------------ | ------------------------- | | Does it change row content or cell values? | **Your component** (`$state`) - hand the new array down to `data`. | | Is it a column/grid setting (sort, filter, group, page)? | **The engine** owns it. Use `api.setSort(...)` etc., or pass an initial state. | | Is it a visual concern (column widths, hover)? | **The renderer** owns it. The grid manages this internally. | This split is deliberate: the engine is dataless, so it can't "lose" your rows. Your component is rendererless, so it can't accidentally mutate DOM nodes during a sort. ## Where each topic page sits | Topic | Layer | Why | | ------------------------------------ | ------ | ------------------------------------------------------------------------- | | [Column definitions](./columns/column-definitions.md) | 1 | Pure types you author. | | [Row data](./rows/row-data.md) | 1 | Your input. | | [Row sorting](./rows/row-sorting.md) | 2 | Engine row-model. | | [Filtering overview](./filtering/overview.md) | 2 + 3 | Engine for the pipeline; renderer for the popovers + filter row. | | [Row pagination](./rows/row-pagination.md) | 2 | Engine slice. | | [Editing](./editing/overview.md) | 3 | DOM editors live in the renderer. | | [Tree rows](./rows/tree-rows.md) | 1 + 3 | You derive `visibleRows`; the renderer indents + draws chevrons. | | [Pivot tables](./pivot.md) | 1 + 2 | You build the pivot engine; the renderer uses standard nested headers. | | [AI assistant](./ai.md) | 2 | Pure helpers; the renderer never sees them. | | [Export / import](./export.md), [import](./import.md) | 2 + 3 | Helpers + browser-side file IO. | ## Why this matters for shipping - **You can test Layer 2 without a DOM.** Every engine helper is a pure function. Vitest in node, no jsdom required. See [Testing your grid](./testing.md). - **You can swap Layer 3.** If your design system has its own table primitive, drop `` and read from `createSvGrid()` directly. - **Layer 2 is the public API surface.** Imports, exports, and types are versioned per the [API stability](./api-stability.md) policy. The renderer's CSS classes are NOT - override them at your peril. ## Where the layers physically live | Layer | Source path | Build output | | ----- | ------------------------------------------------- | ------------------------------------ | | 1 | Your app | n/a | | 2 | `packages/sv-grid-community/src/core.ts` + row-models | `dist/index.js` (~13 kB gzip) | | 3 | `packages/sv-grid-community/src/SvGrid.svelte` | bundled with the engine (~49 kB gzip total) | | | `packages/sv-grid-pro/src/{export,print,import,ai}.ts` | `sv-grid-pro/dist/*` (lazy-loaded peers) | ## See also - [Why headless?](../why-headless.md) - the design rationale for the Layer 2 / Layer 3 split. - [API reference](./api-reference.md) - every export with its layer noted. - [Performance benchmarks](./benchmarks.md) - numbers from each layer in isolation. ## Frequently asked questions ### How is SvGrid architected? As three strict layers: a headless core engine (state + row-model pipeline), a Svelte render component (``) that draws the DOM, and your application code. Knowing which layer a piece of code lives in tells you what it can and cannot do. ### What does "headless" mean for SvGrid? The core engine computes sorting, filtering, grouping, and selection state without rendering anything. You can drive your own markup with it, or drop in the batteries-included `` component that renders on top of the same engine. ### Can I use the engine without the SvGrid component? Yes. Use `createSvGrid` and the row-model factories directly to build a custom rendering layer. The render component is optional sugar over the same public engine API. # Performance benchmarks Headline numbers from the regression suite. Every figure below is from the same hardware + browser configuration, re-measured on each release; the script that produces them lives in `packages/sv-grid-community/scripts/bench.ts` and is checked in. Live load - 100k rows x 100 columns with row + column virtualization:
## Test rig | Component | Spec | | --------- | ----------------------------------------- | | CPU | Apple M2 (8-core), 10W TDP | | RAM | 16 GB LPDDR5 | | Browser | Chrome 131 (release channel) | | Display | 1 page worth of cells visible at any time | | Throttle | None - the regression run isn't throttled, but we also publish a separate "4x slow-down" line below for each scenario | Numbers are the median of 5 runs after a warm-up pass. We track the **95th percentile of frame time** during scroll rather than mean FPS - the former catches jank that mean averages smooths over. ## Bundle size Production build, gzipped, measured on the `dist/` output: | Surface | Brotli | gzip | Notes | | ----------------------------------- | ------ | ------ | -------------------------------------- | | `sv-grid-community` (full) | 41 kB | 49 kB | One import covers the entire renderer | | Headless engine (no ``) | 11 kB | 13 kB | If you bring your own renderer | | `sv-grid-pro` core | 7 kB | 8 kB | Export + print + import + AI shells | | `sv-grid-pro` AI module only | 4 kB | 5 kB | Imported via `'sv-grid-pro/ai'` | | `sv-grid-pro` import module only | 5 kB | 6 kB | Imported via `'sv-grid-pro/import'` | | Peer: `jszip` | 30 kB | 35 kB | Loaded on first `xlsx` export *or* import | | Peer: `pdfmake` + vfs | ~200 kB| ~280 kB| Loaded on first `pdf` export only | Tree-shaking is friendly: importing `{ SvGrid, tableFeatures }` without `rowSortingFeature` doesn't pull the sort module. ## First paint 100k synthetic rows, 9 columns, default density, no virtualization override. Measured from `mount()` to the first row painting: | Scenario | Time (ms) | | --------------------------------- | --------- | | 10 rows × 9 cols | 4 | | 1,000 rows × 9 cols | 14 | | 10,000 rows × 9 cols | 38 | | 100,000 rows × 9 cols (virtualized) | 82 | | 100,000 rows × 100 cols (row + col virtualization) | 110 | The slope is sub-linear because virtualization caps the rendered cell count regardless of dataset size. ## Scroll performance Sustained vertical scroll, 60 px/frame, measured as the 95th percentile frame time: | Scenario | p95 frame | Equivalent FPS | | ------------------------------------- | --------- | -------------- | | 100k rows × 9 cols | 8 ms | ~120 fps | | 100k rows × 100 cols (col virt) | 11 ms | ~90 fps | | 100k rows × 9 cols, custom cell snippets w/ sparklines | 14 ms | ~70 fps | | 100k rows × 9 cols, **4x CPU throttle** | 22 ms | ~45 fps | Horizontal scroll on a 100-column grid stays under 12 ms p95 because the column virtualizer is identical machinery. ## Sort, filter, group In-memory operations on 100k rows: | Operation | Time (ms) | | ------------------------------------- | --------- | | Sort 100k rows by one column | 18 | | Sort 100k rows by 3 columns (multi-sort) | 28 | | Filter 100k rows (one operator) | 9 | | Filter 100k rows (5 operators ANDed) | 17 | | Group 100k rows by 2 columns + 3 aggregators | 36 | | Pivot 100k facts → 4 row dims × 2 col dims × 3 measures (see demo 52) | 62 | The sort path uses a stable comparator built per-column to keep allocations down; the filter pipeline short-circuits on the first failing predicate. ## Memory Heap snapshot at idle, 100k rows × 9 columns loaded, after a full scroll pass: - ~22 MB heap (the Row objects + the visible-cell pool). - Virtualization keeps the rendered DOM under ~600 `
` nodes regardless of dataset size. - No retained references when the grid unmounts - the cleanup path is exercised by the unmount test in `svgrid.behavior.test.ts`. ## Server-side / chunked loading Demo [33. Server-side infinite scroll](https://svgrid.com/#/demos/33-server-infinite) covers the chunked-load path. Numbers from that demo: | Scenario | Result | | ------------------------------------------------ | --------------------------------------- | | Initial paint, sparse 100k-row dataset | 110 ms to first chunk visible | | Scroll 50,000 rows in 1.5 s (fast wheel-flick) | 16 chunk requests cancelled mid-flight | | Sort 100k server-side rows | round-trip dominated by the mock latency (50-140 ms) | ## AI helpers End-to-end timings against the bundled `mockAIProvider`: | Helper | Median time (ms) | | --------------- | ---------------- | | `aiFilter` | 350-750 (mock latency dominated) | | `aiSmartFill` (50 rows) | 400-900 | | `aiSummarize` | 350-750 | | `aiClassify` (20 rows) | 400-750 | Against a real model the latency is provider-side. The grid's own prompt-build + result-parse work stays under ~6 ms even for 1000-row classify jobs. ## Import / export | Operation | Time | | ------------------------------------ | ------ | | Parse CSV, 10k rows × 9 cols | 28 ms | | Parse xlsx, 10k rows × 9 cols | 140 ms (jszip unzip-dominated) | | Export CSV, 10k rows × 9 cols | 18 ms | | Export xlsx, 10k rows × 9 cols | 220 ms | | Export PDF, 1k rows × 9 cols (pdfmake) | 700 ms | ## Reproducing locally ```bash git clone https://github.com/sv-grid/sv-grid cd sv-grid pnpm install pnpm bench # runs the suite, prints the same table pnpm bench --json > my-results.json # for trend tracking ``` The bench script also produces a comparison table against the previous run if you pass `--baseline=path/to/prev.json`. Regressions over 10% fail CI on the main branch. ## What we *don't* claim - "Smoothest grid on the market" - that depends entirely on what your cells render. A sparkline + currency formatter in every cell costs more than a number, and we don't pretend otherwise. - "Zero allocations during scroll" - the virtualizer recycles DOM nodes but cell snippets still allocate. The numbers above include real-world snippets (status pills, mini-bars). - Single-thread performance > 1M rows. For >1M, do the heavy lifting on the server and feed chunks through the [server-side infinite scroll pattern](https://svgrid.com/#/demos/33-server-infinite). ## See also - [Browser support](./browser-support.md) - the matrix the benchmarks ran against. - [Testing and quality](./testing-and-quality.md) - the coverage thresholds that gate every release. ## Frequently asked questions ### How fast is SvGrid? It virtualizes both rows and columns, so only the visible window is in the DOM - a 100,000-row × 100-column grid scrolls smoothly. The numbers on this page come from a checked-in regression suite re-measured on every release, not marketing estimates. ### How many rows can SvGrid handle? Client-side, 100k+ rows scroll smoothly thanks to virtualization. For millions of rows, page or chunk from the server (see Server-side data). The DOM only ever holds the visible window regardless of total row count. ### Is SvGrid faster than AG Grid or TanStack Table? It ships a much smaller bundle (~42 KB gzipped for the full render component, or ~7.5 KB for the headless core) and virtualizes by default. Raw scroll performance is comparable for typical workloads; the bigger practical win is bundle size and a Svelte-native runtime with no framework bridge. # Browser & runtime support The shipping target matrix. If your environment isn't listed, assume unsupported until you've validated the linked feature surfaces below. ## Browsers | Browser | Version | Status | Notes | | ----------------- | ------- | ----------- | --------------------------------------------------------------------- | | Chrome / Chromium | 100+ | Supported | Primary development target. Tested every release. | | Edge | 100+ | Supported | Same Chromium base as Chrome. | | Firefox | 100+ | Supported | Tested every release. The custom scrollbar paints natively. | | Safari (macOS) | 15.4+ | Supported | Pointer Events shim landed in 15.4 - older Safari hits edge cases. | | Safari (iOS) | 15.4+ | Supported | Touch UX known limitations - see "Mobile" below. | | Opera | 86+ | Supported | Chromium-based. | | IE 11 | n/a | Unsupported | Not feasible: Svelte 5 itself requires modern JS. | | Old WebViews | varies | Best-effort | Anything with `ResizeObserver` + ES2020 syntax should work. | The lower bound is **the oldest version with native `ResizeObserver`, `IntersectionObserver`, Pointer Events, and `Object.hasOwn`**. SvGrid uses these directly; we don't polyfill in the package. ## JavaScript baseline SvGrid is built and shipped as **ESM only** with ES2020 syntax. The published bundle assumes: - `const`, `let`, arrow functions, classes, async/await, `??`, `?.` - `Promise`, `Map`, `Set`, `WeakMap`, `Symbol` - `Array.prototype.flatMap`, `Object.fromEntries`, `Object.hasOwn` - `globalThis` If you need to ship to a baseline lower than ES2020, transpile `sv-grid-community` in your own build (Vite, Rollup, esbuild). The package source contains no syntax above ES2022. ## DOM APIs The grid uses these browser APIs directly: | API | Where | Polyfillable? | | ------------------------------------ | ------------------------------------------------------------------------- | ------------------- | | `ResizeObserver` | Auto-resizing the grid container + column widths | Yes (`resize-observer-polyfill`) | | `IntersectionObserver` | Virtualization viewport check | Yes | | Pointer Events (`pointerdown`/`-up`) | Selection, fill handle, column resize | Yes | | `Element.scrollTo({ behavior })` | Programmatic row scroll, smooth-scroll API | Yes | | `URL.createObjectURL` | Export (xlsx, pdf, csv, tsv, html) | Required | | `Blob`, `File` | Export + import | Required | | `document.execCommand('copy')` *or* `Clipboard API` | Copy/paste cell selection (graceful fallback) | Either | If you're shipping to a sandboxed environment where some of these are locked down, see the [CSP guidance](./security.md#csp-guidance) section on the security page. ## Node / SSR SvGrid is SSR-compatible. The render component does nothing on the server beyond emitting the wrapper markup with an `aria-busy` shell - the data + interactions hydrate on the client. Demo [19. Server-side rendering](https://svgrid.com/#/demos/19-ssr) shows a sandboxed pre-hydration snapshot. ### Tested SSR runtimes | Runtime | Version | Status | | ------------- | ------- | ---------- | | Node.js | 18 LTS+ | Supported | | Bun | 1.0+ | Supported | | Deno | 1.40+ | Supported | | SvelteKit | 2.x | Supported | | Astro | 4.x+ | Supported | | Cloudflare Workers (with SvelteKit adapter) | latest | Supported (no FS access from the grid) | ### What the grid does NOT do on the server - No `window` / `document` access in module initialisation. - No event listeners. - No `ResizeObserver` / `IntersectionObserver`. - No `localStorage`. - No virtualization (server emits a placeholder shell; client takes over on hydration). If you see a "lifecycle_function_unavailable" error from Svelte during SSR, that's coming from a custom cell snippet of yours - not from the grid. Guard `document` / `window` reads inside an `onMount` or `$effect`. ## Build tools | Tool | Version | Status | | ------------- | ------- | ---------- | | Vite | 5+ | Recommended. Tested with vite 5, 6, and 7. | | Rollup | 4+ | Supported. SvGrid is published as ESM and is tree-shakeable. | | esbuild | 0.20+ | Supported. | | Webpack | 5+ | Supported with `experiments.outputModule` or a CJS-compatible Svelte loader. | | Turbopack | n/a | Untested. Should work given ESM compatibility. | Tree-shaking works at the named-export granularity - `import { SvGrid, tableFeatures } from 'sv-grid-community'` pulls in only the surfaces you reference. Each feature module (sorting, filtering, etc.) is its own export. ## Mobile Mobile browsers render the grid correctly but the UX makes the following compromises: - **Touch drag selection** is supported on Pointer-Event browsers but doesn't show the same handles as desktop. - **Cell editing** opens the native keyboard; multi-cell paste from the iOS clipboard works via the standard `Clipboard API`. - **Column resize** is awkward on screens narrower than ~600 CSS pixels because the resize handles are 4 px wide. Configure pinned columns for narrow screens instead. - **Pinned columns** are supported and recommended for mobile. For a mobile-first workflow, prefer a card list view at smaller viewports and switch to the grid above an `md` breakpoint. ## See also - [Security & supply chain](./security.md) - including a CSP-friendly config snippet. - [Performance benchmarks](./benchmarks.md) - measured throughput on a documented machine. - [Testing and quality](./testing-and-quality.md) - the test suite and what it covers. # Cell components For any cell whose content is more than a string, use `cell:` with `renderSnippet` or `renderComponent`.
## Snippet ```svelte {#snippet Pill(p: { value: string })} {p.value} {/snippet} ``` Snippets are the right choice when the renderer is local to the page and small. ## Component ```ts import StatusBadge from './StatusBadge.svelte' import { renderComponent } from 'sv-grid-community' { field: 'status', cell: (ctx) => renderComponent(StatusBadge, { status: ctx.getValue() }), } ``` Components are the right choice when the renderer is reused across multiple grids, has its own state, or needs lifecycle hooks. ## CellContext The argument the grid passes to your `cell` callback: ```ts type CellContext = { cell: Cell row: Row column: Column table: SvGrid getValue: () => unknown } ``` - `getValue()` - the accessed value (post-`field` / `accessorFn`). - `row.original` - the raw `TData` object. - `row.getAllCells()` - every cell in the row, for sibling reads. - `column.columnDef` - the original `ColumnDef`. - `table` - the headless grid instance, with state and actions. ## Inline string If your cell content is a plain string and you just want to format it, use `format` or `formatter` - not `cell`. See [Text formatting](./text-formatting.md). ## Performance Cell renderers run once per visible cell on each grid update. For large virtualized grids, keep them cheap: - avoid `JSON.stringify` - avoid `new Date()` per cell - pre-compute formatters at module scope - avoid creating new objects inside the snippet template ## Common patterns - **Avatar + name** - return a snippet that pulls first/last name from `ctx.row.original`. See demo 10. - **Status pill** - class-derived background. See demo 10. - **Inline progress bar** - `
`. See demo 10. - **Hyperlink** - `{ctx.getValue()}`. ## See also - [Custom header components](../columns/custom-header-components.md) - [demos/10-custom-cells-and-themes.svelte](../../../examples/src/demos/10-custom-cells-and-themes.svelte) # Cell data types The `editorType` field on a column tags the column with a type. This drives three different behaviours:
| Effect | Driven by `editorType` | | ------ | --------------------- | | Which inline editor opens on `F2` / double-click | yes - `text` / `number` / `date` / `datetime` / `checkbox` | | Which sort comparator is used | yes - `number` and `date`/`datetime` pick non-default `sortFns` | | Which filter operators the column menu offers | yes - text / number / date / checkbox sets differ | ## Setting it ```ts const columns: ColumnDef<{}, Person>[] = [ { field: 'firstName', header: 'First', editorType: 'text' }, { field: 'age', header: 'Age', editorType: 'number' }, { field: 'joinedAt', header: 'Joined', editorType: 'date' }, { field: 'startTime', header: 'Start', editorType: 'datetime' }, { field: 'active', header: 'Active', editorType: 'checkbox' }, ] ``` Even if you do not enable inline editing, set `editorType` so sort and filter behave correctly for the column's data type. A `number` column without `editorType` sorts as lexical strings. ## Built-in types | `editorType` | accepted value space | | ------------ | -------------------- | | `'text'` | strings | | `'number'` | numbers (or numeric strings) | | `'date'` | ISO date strings (`YYYY-MM-DD`) or `Date` | | `'datetime'` | ISO datetime strings or `Date` | | `'checkbox'` | booleans | ## Custom types There is no plug-in "register a new cell data type" API. To use a custom type: - Render with a custom `cell` (see [Cell components](./cell-components.md)) - Sort with a custom value through `accessorFn` that normalises to a comparable primitive - Filter with a custom operator UI in your own header component ## Editor value parsing The editor receives a string from the DOM and converts to the canonical value before commit. The implementation lives in [`parseEditorValue`](../../../packages/sv-grid-community/src/editors/cell-editors.ts). ```ts import { parseEditorValue } from 'sv-grid-community' parseEditorValue('number', '42') // 42 parseEditorValue('number', 'abc') // NaN - caller should reject parseEditorValue('checkbox', 'true') // true parseEditorValue('date', '2026-05-27') // '2026-05-27' ``` ## See also - [Provided cell editors](../editing/provided-editors.md) - [Filter conditions](../filtering/filter-conditions.md) # Cell text selection By default the grid has **cell-range selection** (drag across cells, range highlight, copy as TSV). Standard browser text-selection inside a cell is **not** the default - clicking-and-dragging across a cell creates a range selection, not a text selection.
## Enable browser text selection If your users need to copy a substring from a single cell (e.g. an email address that's wider than the cell), turn off cell-range selection for that part of the grid. The simplest scope is "everywhere": ```svelte /> ``` Or surgically, per cell, render the value in a `` whose `user-select` overrides the grid's: ```ts { field: 'email', cell: (ctx) => renderSnippet(SelectableEmail, { value: String(ctx.getValue()) }), } ``` ```svelte {#snippet SelectableEmail(p: { value: string })} {p.value} {/snippet} ``` ## Copy current value `Ctrl/Cmd+C` with a cell range selection copies all selected cells as TSV. With a single cell, the value is copied as a TSV scalar (no tab, no newline). To copy *just the displayed text* of a single cell without entering range selection, switch to a `selectionMode="row"` and use the row checkbox column + `Ctrl/Cmd+C` to copy entire rows. ## Gotchas - A grid in `selectionMode='cell'` swallows mouse selection inside cells - drag *only* creates a rectangle selection, never a browser text selection. - A grid in `selectionMode='both'` (default) does too - the cell selection layer takes precedence. ## See also - [Selection demo](../../../examples/src/demos/04-selection-copy-paste.svelte) - [Custom cells](./cell-components.md) # Conditional formatting Conditional formatting colors a cell by its value. SvGrid ships it as a declarative engine prop, `conditionalFormats`, so you describe the rules once and the grid paints every cell - no per-cell `cell` snippet required. It goes beyond the `cellClass(ctx)` callback (which only toggles static CSS classes): color scales and data bars need a value computed against the column's min/max range, which the engine does for you. ```svelte ``` ## Format kinds ### `colorScale` - gradient fill A 2-stop (`min`/`max`) or 3-stop (`min`/`mid`/`max`) gradient mapped across the column's value range. Fix the scale with `minValue`/`maxValue` to make rows comparable. ```ts { type: 'colorScale', columns: ['score'], min: '#fca5a5', mid: '#fde68a', max: '#86efac', minValue: 0, maxValue: 100 } ``` ### `dataBar` - in-cell bar An in-cell horizontal bar proportional to the value. Diverging data (can go negative) gets `negativeColor`. `showValue: false` hides the text and shows the bar alone. ```ts { type: 'dataBar', columns: ['revenue'], color: '#3b82f6', negativeColor: '#ef4444' } ``` ### `iconSet` - threshold icons An icon chosen by ascending `thresholds` (n thresholds => n+1 buckets). Built-in sets: `'arrows'`, `'traffic'`, `'triangles'`. `iconOnly: true` hides the number. ```ts // growth < 0 -> down, 0..10 -> flat, >= 10 -> up { type: 'iconSet', columns: ['growth'], set: 'arrows', thresholds: [0, 10] } ``` ### `rule` - style on a predicate Apply `background` / `color` / `fontWeight` when `when(ctx)` returns true. The predicate receives the typed row, so you can key off other fields. ```ts { type: 'rule', columns: ['churn'], when: ({ value }) => Number(value) >= 20, background: '#fee2e2', color: '#991b1b', fontWeight: 700 } ``` ## Scoping and precedence - `columns: [...]` limits a format to those column ids. Omit it to apply to every column. - Formats are evaluated in array order; **later entries win** on conflict, so list general formats first and specific overrides last. - Empty / non-numeric cells are skipped by the numeric formats (color scale, data bar, icon set) and never count toward a column's min/max. ## Notes - The color-scale fill and data bar render as layers behind the text, so they survive app stylesheets that force the cell background. - The resolver is exported as `resolveCellFormat(value, row, columnId, formats, stat)` if you want to compute the same result yourself. See the live [Conditional formatting](https://sv-grid.com/demos/141-conditional-formatting) demo. # Expressions SvGrid does not ship a formula / expression language for cells. Computed values are JavaScript - either via `accessorFn` or inside a `cell` callback.
## Per-cell computation ```ts { id: 'totalCost', header: 'Total', accessorFn: (row) => row.unitPrice * row.quantity, format: { type: 'currency', currency: 'USD' }, } ``` `accessorFn` runs every time the row's value is needed (display, sort, filter, copy). The result is treated as a plain value of the resulting type, so `format` / `formatter` / `editorType` all apply. ## Cross-row aggregation For computed columns that depend on **other rows** (running total, rank, delta-from-mean), do the computation **before** you pass data into the grid - derive a new array with the aggregate fields baked in: ```svelte ``` The grid's row pipeline runs **per row** - it does not give you a hook for "emit a derived column that needs the whole array". ## Formula language A spreadsheet-style formula language (cells like `=A1+B2`) is **not** in the SvGrid community build. There is no formula parser or formula editor. If you need spreadsheet-style cells, that's a separate library - wire its output into a column's `accessorFn`. ## See also - [Column definitions](../columns/column-definitions.md) - [Server-side guide](../../getting-started.md#11-server-side-data) - for aggregates the server is better at than the client. # Getting values You get cell values in three ways depending on context.
## Inside a column definition `field` does the obvious thing - `row[key]`: ```ts { field: 'firstName', header: 'First' } ``` Use `accessorFn` for anything computed: ```ts { id: 'fullName', header: 'Full name', accessorFn: (row) => `${row.firstName} ${row.lastName}`, } ``` ## Inside a cell renderer A `cell` callback receives a `CellContext`: ```ts { field: 'salary', header: 'Salary', cell: (ctx) => { const value = ctx.getValue() // unknown const row = ctx.row.original // your TData const all = ctx.row.getAllCells() // array of Cell return /* renderSnippet / string / etc. */ }, } ``` `ctx.row.original` is the **raw row object** you passed in - handy when you want sibling values without going through accessors. ## From outside the grid After `onApiReady`: ```ts const v = api.getCellValue(rowIndex, columnId) api.setCellValue(rowIndex, columnId, newValue) ``` `rowIndex` is the index in the **source data array**, not the post-pipeline displayed index. ## Reading by row id There is no `api.getCellValueByRowId(rowId, columnId)` helper today. If you need that, walk `api.getData()`: ```ts function valueByRowId(api: SvGridApi<{}, Person>, rowId: string, col: string) { const data = api.getData() const idx = data.findIndex((r) => r.id === rowId) return idx === -1 ? undefined : api.getCellValue(idx, col) } ``` ## See also - [Cell components](./cell-components.md) - [Accessing rows](../rows/accessing-rows.md) # Highlighting changes There is no built-in "flash on change" highlight. You build it with a diff against a frozen snapshot of the data.
## Dirty cells while editing Demo 5 ([demos/05-inline-editing.svelte](../../../examples/src/demos/05-inline-editing.svelte)) does this: ```svelte ``` To make the dirty marker visible in the grid, render an indicator inside a custom `cell`: ```ts { field: 'salary', cell: (ctx) => renderSnippet(MaybeDirty, { value: ctx.getValue(), isDirty: dirty[`${ctx.row.original.id}.salary`] === true, }), } ``` ```svelte {#snippet MaybeDirty(p: { value: unknown; isDirty: boolean })} {p.value} {#if p.isDirty}{/if} {/snippet} ``` ## Flash on value change (live data) For live-update grids (stock tickers, queue dashboards): ```svelte ``` Drive the flash from your cell renderer the same way. Use `prefers- reduced-motion` to disable the animation for users who opt out. ## See also - [Cell components](./cell-components.md) - [demos/05-inline-editing.svelte](../../../examples/src/demos/05-inline-editing.svelte) # Sparklines A sparkline is a tiny, word-sized chart drawn inside a single cell. SvGrid renders them as a first-class column type: set `sparkline` on a column whose value is an array of numbers and the grid paints an inline SVG. No chart library, no custom cell snippet. ```svelte ``` ## Value shape The cell value is an array of numbers. A comma- or space-separated string works too (`"1, 2, 3"`), so server payloads that send a CSV string render without massaging. Non-finite entries are dropped; an empty array renders nothing. ## Chart types | `type` | Looks like | | ----------- | -------------------------------------------- | | `'line'` | A single polyline with an end-point dot (default) | | `'area'` | A line plus a translucent fill to the baseline | | `'bar'` | One column per value, scaled to the row's min..max | | `'winloss'` | Fixed-height up/down bars - sign only (W/L streaks) | ## Options (`SparklineConfig`) | Option | Default | Notes | | --------------- | ------------------------ | -------------------------------------------------- | | `type` | `'line'` | One of the four above. | | `color` | `var(--sg-accent)` | Stroke (line/area) or positive fill (bar/winloss). | | `negativeColor` | `#ef4444` | Fill for negative bars / losses. | | `width` | `88` | SVG width in px. | | `height` | `22` | SVG height in px. | | `min` / `max` | derived from the row | Fix the value scale so rows are comparable. | | `lineWidth` | `1.5` | Stroke width (line/area). | | `lastPoint` | `true` | Draw the end-cap dot on line/area. | ```ts // Green/red diverging bars on a column that can go negative: { field: 'delta', sparkline: { type: 'bar', color: '#16a34a', negativeColor: '#ef4444' } } // Comparable rows: pin every sparkline to the same 0..100 scale: { field: 'score', sparkline: { type: 'area', min: 0, max: 100 } } ``` ## Notes - A custom `cell` renderer wins if both `cell` and `sparkline` are set. - Sparklines are decorative SVG with an `aria-label` summarising the series (point count + last value). For a screen-reader-friendly exact readout, pair the chart column with a plain numeric column. - The geometry helper is exported as `buildSparkline(values, config)` if you want to render the same chart outside a grid cell. See the live [Sparkline cells](https://sv-grid.com/demos/140-sparkline-cells) demo. # Styling cells Cells render as `
` and pick up the same CSS variable tokens as the rest of the grid.
## Default look ```css table[role='grid'] td { border: 1px solid var(--sg-border); padding: 0.4rem 0.6rem; vertical-align: middle; } ``` Override at `:root` or on the grid host. ## Per-cell styling `ColumnDef` accepts a `cellClass` field. It can be: - a **string** (or array of strings) - applied to every `
` in this column; - a **function** - called per cell with the standard `CellContext`, returning a string, array, or `Record`. ```ts { field: 'salary', format: { type: 'currency', currency: 'USD' }, cellClass: (ctx) => { const v = Number(ctx.getValue()) if (v > 100_000) return 'cell-money-high' if (v < 20_000) return 'cell-money-low' return '' }, } ``` ```css :global(td.cell-money-high) { color: #16a34a; font-weight: 600; } :global(td.cell-money-low) { color: #b91c1c; } ``` The class is added to the existing `sv-grid-cell` class (other modifiers like `sv-grid-cell-active`, `sv-grid-cell-editing`, `data-align`, `data-pinned` still apply). Use `:global(...)` in scoped Svelte styles since the cell lives outside the component hash. For row-level conditional styling, use the wrapper's `rowClass={({ row }) => ...}` prop - same shape, scoped to the `
` of the same data, wired to the SVG via `aria-describedby`, so screen readers get the numbers, not just "chart". - For a richer charting stack (zoom, tooltips, dozens of types) you can still pipe `getDisplayedRows()` into Chart.js or a web component - see demos `73-chartjs-sync` and `77-smart-chart`. `SvGridChart` is the batteries-included option. See the live [Integrated charts](https://sv-grid.com/demos/147-integrated-charts) demo, or the [Chart wizard panel](https://sv-grid.com/demos/152-chart-wizard) - a pick-a-chart dialog whose type-gallery thumbnails are themselves live `SvGridChart` previews. # Real-time collaboration Two people (or two AI agents) on the same grid: **presence** (who's here and where their cursor is) and **live edits** (a change in one client appears in every other). SvGrid packages this as a headless controller over a pluggable transport - the only infrastructure-specific piece. ```ts import { createCollaboration, broadcastChannelTransport } from 'sv-grid-community' const collab = createCollaboration({ user: { id: myId, name: 'Ada', color: '#ef4444' }, transport: broadcastChannelTransport('my-grid-room'), onPeersChange: (peers) => renderCursors(peers), onRemoteEdit: ({ rowId, columnId, value }) => applyEdit(rowId, columnId, value), }) ``` Wire it to the grid: ```svelte r.id} onActiveCellChange={(c) => collab.setCell({ rowId: data[c.rowIndex].id, columnId: c.columnId })} onCellValueChange={(e) => collab.sendEdit(data[e.rowIndex].id, e.columnId, e.newValue)} /> ``` ## The transport The controller is transport-agnostic. It ships with one adapter: - **`broadcastChannelTransport(name)`** - syncs across tabs of the same browser with **zero backend**. Great for demos and single-user multi-tab. For cross-machine collaboration implement `CollabTransport` (a `post(msg)` + `subscribe(handler)` pair) over a WebSocket, WebRTC datachannel, or a CRDT library: ```ts const wsTransport: CollabTransport = { post: (msg) => socket.send(JSON.stringify(msg)), subscribe: (h) => { const l = (e) => h(JSON.parse(e.data)); socket.addEventListener('message', l); return () => socket.removeEventListener('message', l) }, } ``` ## The controller API | Method | Does | | --------------------------- | ----------------------------------------------- | | `setCell(cell \| null)` | Broadcast where your cursor is. | | `sendEdit(rowId, col, val)` | Broadcast a cell edit. | | `peers()` | Present peers (excludes you), with their cursor.| | `dispose()` | Announce leave + tear down (call on unmount). | `onPeersChange` fires whenever the peer set or any cursor moves; `onRemoteEdit` fires for edits from **other** users only (never echoes your own). ## Notes - Presence is heartbeat-pruned: a peer that closes its tab without a clean `bye` is dropped after `peerTimeoutMs` (default 15s). - Edits are last-writer-wins at the cell level. For conflict-free merging on a busy doc, back the transport with a CRDT; the controller doesn't assume one. - This is also the **multi-agent** substrate: an AI agent is just another peer posting `edit` messages - drive `sendEdit` from your agent loop. See the live [Real-time collaboration](https://sv-grid.com/demos/149-realtime-collaboration) demo (open it in two tabs). # Columns hierarchy & manager The pattern for grids with too many columns to fit on screen at once. A side-panel "column manager" exposes the column tree to the user: they pick what's visible, what gets collapsed into a one-cell summary, and what order leaves appear in within each group. Try it live - drag a leaf chip between siblings, click a group's chevron to collapse it into a summary column, toggle a checkbox to hide:
## What it builds on Two existing engine capabilities power this: 1. **Nested column groups.** A `ColumnDef` with `columns: [...]` produces multi-row headers automatically; see [Column groups](./columns/column-groups.md). 2. **Dynamic columns.** The `columns` prop is a regular array; passing a `$derived` array makes it reactive. The grid swaps the column tree in place on every change. The "hierarchy manager" is just a Svelte component you write that mutates a piece of `$state` describing which groups are open + which leaves are visible. The derived columns array reflects that state. ## Data model The state has one entry per group: ```ts type GroupTreeState = { id: string visible: boolean // whole-group hide collapsed: boolean // collapsed to one summary column leafOrder: string[] // leaf ids, in display order leafVisible: Record // per-leaf hide } let treeState = $state(initialTree()) ``` Initial state walks your fixed group definitions and turns every leaf into `visible: true`. From then on, every interaction in the side panel produces a new array (no mutation in place - Svelte 5 prefers reassignment). ## Deriving the column tree The grid's columns are a `$derived` from the tree state plus your fixed column specs: ```ts const columnTree = $derived.by((): ColumnDef[] => { const out: ColumnDef[] = [] for (const g of treeState) { if (!g.visible) continue const spec = GROUPS.find((s) => s.id === g.id)! if (g.collapsed) { // One synthetic column showing the group's summary. out.push({ id: `summary_${g.id}`, header: `${spec.label} · summary`, width: 200, cell: (ctx) => renderSnippet(SummaryCell, { row: ctx.row.original, groupId: g.id, }), }) continue } // Otherwise: emit the group as a real nested column group. const visibleLeaves = g.leafOrder .map((id) => spec.leaves.find((l) => l.id === id)!) .filter((l) => g.leafVisible[l.id]) if (visibleLeaves.length === 0) continue out.push({ id: `group_${g.id}`, header: spec.label, columns: visibleLeaves.map(buildLeaf), }) } return out }) ``` `buildLeaf` is a small helper that maps your fixed leaf spec (`{ id, field, label, width }`) into a `ColumnDef`. For columns whose display is a custom snippet, dispatch by id and emit the corresponding `cell: renderSnippet(...)`. ## Group "summary" cells When the user collapses a group, you don't usually want it to vanish - the user wants to see one representative value for it. The **Engagement** group in the demo collapses to a health pill; **Deal value** collapses to the ARR; **Account** collapses to the company name plus an industry / region sub-line. The summary cell snippet takes a `groupId` prop and switches on it: ```svelte {#snippet SummaryCell(props: { row: Deal; groupId: string })} {#if props.groupId === 'value'} {fmtMoney(props.row.arr)} {props.row.probability}% prob {:else if props.groupId === 'engagement'} {/if} {/snippet} ``` ## Side-panel UI Three things the user controls per group: | Control | What it does | | ----------------------------- | ----------------------------------------------------------------- | | Chevron (`▶`) | Toggle `collapsed`. Group becomes 1 column, or expands back. | | Group checkbox | Toggle `visible`. Hides the whole group. | | Leaf checkbox | Toggle `leafVisible[id]`. Hides one column. | Drag-and-drop within a group reorders leaves: ```ts function onDrop(e: DragEvent, groupId: string, targetLeafId: string) { e.preventDefault() if (!drag || drag.groupId !== groupId) return const group = treeState.find((g) => g.id === groupId)! const fromIx = group.leafOrder.indexOf(drag.leafId) const toIx = group.leafOrder.indexOf(targetLeafId) if (fromIx < 0 || toIx < 0 || fromIx === toIx) return const next = group.leafOrder.slice() next.splice(fromIx, 1) next.splice(toIx, 0, drag.leafId) treeState = treeState.map((g) => g.id === groupId ? { ...g, leafOrder: next } : g, ) } ``` The demo restricts drag to same-group reorder because cross-group drag would change the semantic grouping (a "Deal value · Stage" mix doesn't make sense). If your data model allows cross-group moves, drop the `drag.groupId === groupId` guard. ## Persistence The tree state is a plain JS object - feed it into your [Saved views](./saved-views.md) pipeline and the user's column layout follows them across sessions: ```ts // Save saveView({ ...currentView, columnTree: treeState }) // Restore treeState = view.columnTree ?? initialTree() ``` For multi-user / shared layouts, store under a per-team key alongside your other config. ## Performance Re-deriving `columnTree` runs at ~0.1 ms even for 40-leaf trees on a modern machine; the cost is dominated by the grid's column-rebuild work afterward (~1-2 ms). Mutating the tree state at interactive rates (drag-resize over time) is fine. ## Composing with the rest of the grid - **Sort + filter** apply to whatever columns are visible. Collapsing a group hides its leaves; their filters disappear from the menu but are NOT cleared - re-expand the group and they're still active. If that's surprising for your users, clear the leaf filters as part of the collapse handler. - **Pinned columns** are recorded per leaf id - they re-pin automatically when the leaf becomes visible again. - **Saved views** should serialise `treeState` alongside sort + filter. ## See also - [Column groups](./columns/column-groups.md) - the nested-header primitive this pattern uses. - [Saved views](./saved-views.md) - persisting the tree state across sessions. - [State maintenance](./state-maintenance.md) - undo / redo for tree changes. - [Demo #54 Columns hierarchy](https://svgrid.com/#/demos/54-columns-hierarchy) - the reference implementation. # Column definitions A `ColumnDef` tells SvGrid how to read a value out of a row, how to render it, and which features apply to it. The grid below is built from a handful of `ColumnDef`s - look at the [source](https://svgrid.com/#/demos/01-quick-start) to see how each shape maps to a column behaviour:
## Minimal ```ts import type { ColumnDef } from 'sv-grid-community' type Person = { firstName: string; age: number; status: string } const columns: ColumnDef<{}, Person>[] = [ { field: 'firstName', header: 'First name' }, { field: 'age', header: 'Age' }, { field: 'status', header: 'Status' }, ] ``` ## Properties | Property | Type | Purpose | | --- | --- | --- | | `id` | `string` | Stable column id. Required when you use `accessorFn` and no `field`. | | `field` | `keyof TData & string` | Reads `row[key]`. | | `accessorFn` | `(row) => unknown` | Computes the value. | | `header` | `string` \| `(ctx) => unknown` | String, or a function returning a `renderSnippet` / `renderComponent`. | | `cell` | `(ctx) => unknown` | Same shape as `header`, for body cells. | | `footer` | `string` \| `(ctx) => unknown` | Footer cell. | | `editorType` | `'text' \| 'number' \| 'date' \| 'datetime' \| 'checkbox'` | Inline editor type. | | `format` | `CellFormatConfig` | Built-in `number`, `currency`, `percent`, `date`, `datetime` formatters. | | `formatter` | `(ctx) => string` | Custom formatter - runs after `field` / `accessorFn`. | | `columns` | `ColumnDef[]` | Children - turns this column into a column **group**. | | `width` | `number` | Initial width in pixels (overrides the grid's `columnWidth`). | See [`packages/sv-grid-community/src/core.ts`](../../../packages/sv-grid-community/src/core.ts). ## Accessor vs. accessorFn `field` is the common case. Use `accessorFn` when the value is computed or comes from a nested object: ```ts const columns: ColumnDef<{}, Person>[] = [ { field: 'firstName', header: 'First' }, { id: 'fullName', header: 'Full name', accessorFn: (row) => `${row.firstName} ${row.lastName}`, }, ] ``` Whenever you use `accessorFn` you must supply an `id` - there is no string key to derive one from. ## Format vs. formatter vs. cell | You want | Use | | -------- | --- | | Locale-aware number / currency / percent / date | `format` | | A custom string transformation | `formatter` | | Custom HTML (avatars, pills, progress, sparklines) | `cell` with `renderSnippet` | `format` is purely declarative and locale-aware - prefer it for anything numeric or temporal: ```ts { field: 'salary', header: 'Salary', format: { type: 'currency', currency: 'USD', options: { maximumFractionDigits: 0 } } } { field: 'joinedAt', header: 'Joined', format: { type: 'date', pattern: 'y-m-d' } } { field: 'utilization', header: 'Utilization', format: { type: 'percent', valueIsPercentPoints: true } } // 42 -> 42% ``` `formatter` runs after the accessor; the result is what gets displayed (and what gets copied to the clipboard during cell selection). `cell` is the most powerful - see [Cell components](../cells/cell-components.md). ## TypeScript Pass the row type as the second generic; the column's `field` is then checked against the row's keys: ```ts const columns: ColumnDef<{}, Person>[] = [ { field: 'firstName' }, // ✅ // { field: 'first_name' } // ✗ compile error ] ``` The first generic is the **feature set** - derive it from `tableFeatures` so feature-specific column properties light up: ```ts const features = tableFeatures({ rowSortingFeature, columnFilteringFeature }) type Features = typeof features const columns: ColumnDef[] = [/* … */] ``` ## See also - [Updating definitions](./updating-definitions.md) - [Column state](./column-state.md) - [Custom header components](./custom-header-components.md) - Example: [`02-sort-filter-paginate.svelte`](../../../examples/src/demos/02-sort-filter-paginate.svelte) # Column groups A column group is a `ColumnDef` whose `columns` array contains children. The parent renders a spanning header above its children. The pivot demo below shows three levels of grouped headers in action - Year wraps Quarter wraps measure:
```ts const columns: ColumnDef<{}, Person>[] = [ { field: 'firstName', header: 'First name' }, { field: 'lastName', header: 'Last name' }, { id: 'compensation', header: 'Compensation', columns: [ { field: 'salary', header: 'Salary', format: { type: 'currency', currency: 'USD' } }, { field: 'bonus', header: 'Bonus', format: { type: 'currency', currency: 'USD' } }, ], }, ] ``` Rendered as: ``` | | Compensation | | First | Last | Salary | Bonus | ``` ## How it works - The grid walks the column tree once, producing two header groups - the parent row and the leaf row. - Each parent header gets a `colSpan` equal to the count of leaf descendants it has. - The cell body only renders leaves. ## Nested groups Groups can nest arbitrarily. The grid emits one header row per depth level: ```ts { header: 'Q1', columns: [ { header: 'Jan', field: 'jan' }, { header: 'Feb', field: 'feb' }, { header: 'Mar', field: 'mar' }, ], }, { header: 'Q2', columns: [/* … */], }, ``` ## Group with a custom header The same `header: (ctx) => renderSnippet(...)` pattern from [custom header components](./custom-header-components.md) works for group headers. The `ctx.header.colSpan` value will be the rendered span. ## Gotchas - A group needs an `id` (or a string `header`) - the grid uses it to give the parent header a stable DOM id. - Hidden columns (`api.setColumnVisible(id, false)`) shrink their group's `colSpan` automatically. - A group cannot be sorted or filtered - only its leaves can. ## See also - [Column definitions](./column-definitions.md) - [Custom header components](./custom-header-components.md) # Column headers - styling & height Headers are rendered as `
` with `role="row"`. Style them with regular CSS - there are no header-specific Svelte props.
## Set header text ```ts { field: 'firstName', header: 'First name' } ``` For computed headers, pass a function returning a `renderSnippet` / `renderComponent` - see [Custom header components](./custom-header-components.md). ## Header height There is no `headerHeight` prop. Header height is determined by content + padding. Override via CSS: ```css table[role='grid'] thead th { height: 40px; padding: 0.4rem 0.6rem; } ``` If you set a fixed virtualizer row height (`rowHeight={36}` on ``) the header is **independent** of that - the virtualizer measures it once at mount to compute the visible viewport. ## Header colour, weight, alignment The grid leans on the gallery's tokenised CSS: ```css table[role='grid'] thead tr { background: var(--sg-header-bg); color: var(--sg-header-fg); } table[role='grid'] th { border: 1px solid var(--sg-border); font-weight: 600; text-align: left; } ``` Set those custom properties at `:root`, on an ancestor, or directly on the grid host to change the look. ## Sortable header indicator When `rowSortingFeature` is registered, sortable headers gain a click handler and `aria-sort` is updated. SvGrid renders the asc/desc arrow itself; to restyle, target the inner span: ```css table[role='grid'] th [data-sort-indicator] { opacity: 0.6; } table[role='grid'] th[aria-sort] [data-sort-indicator] { opacity: 1; } ``` ## See also - [Custom header components](./custom-header-components.md) - [Column groups](./column-groups.md) # Column moving (drag to reorder) There are two ways to move columns in sv-grid: 1. **`enableColumnReorder` prop** - the built-in drag-to-reorder UX. Every header becomes draggable, the grid paints a drop indicator, and the order is emitted via `onColumnOrderChange`. Recommended for v1. 2. **Reassign the `columns` prop** - the array order IS the display order, so you can swap items in your own state.
## Built-in drag-to-reorder ```svelte (order = [...next])} /> ``` When `enableColumnReorder` is `true`, the grid: - Sets `draggable=true` on every header (`
` inside a `
`). - Paints a vertical drop indicator before / after the hovered header. - On drop, updates its internal order and fires `onColumnOrderChange(orderedIds)`. - Respects column pinning - dragging across pin zones is allowed, but a column dropped into a pin zone stays in that zone's natural slot. ### Props | Prop | Type | Notes | | --- | --- | --- | | `enableColumnReorder` | `boolean` | Defaults to `false`. Set `true` to opt in. | | `columnOrder` | `ReadonlyArray` | Initial / controlled order. Reassign to drive externally. | | `onColumnOrderChange` | `(order: string[]) => void` | Fires after each change (user drag or `api.setColumnOrder`). | ### Imperative API `SvGridApi` exposes two methods for command-palette / shortcut wiring: ```ts api.setColumnOrder(['symbol', 'price', 'name', 'pe']) const current = api.getColumnOrder() ``` `setColumnOrder` accepts a subset of ids; columns not in the array keep their existing relative position after the listed ones. Unknown ids are skipped. ## Pin groups + reorder Column reorder composes cleanly with column pinning: ```svelte { api.setColumnPinning({ left: ['symbol'], right: ['changePct'] }) }} /> ``` The reorder logic operates on the column ids; the pin grouping is then layered on top, so left-pinned columns stay on the left and right-pinned on the right. ## Reassign the columns prop (no `enableColumnReorder`) If you prefer to own the order entirely in user-land (e.g. a dropdown picker rather than drag): ```svelte ``` This was the only option before `enableColumnReorder` shipped. Demo 104 shows a user-land header-drag pattern built on top of this approach. ## Persisting + saved views The emitted `string[]` is JSON-serialisable, so the most common pattern is: - Save to `localStorage` for "remember my last layout". - Save under a name for "Saved views" feature (combine with column widths, pinning, and filter state). - Serialise to a URL param for shareable views. ## See also - Demo 109: Column reorder (engine prop) - Demo 104: Column reorder (user-land drag-handle pattern) - `Saved views` recipe in `docs/recipes/saved-views.md` - `api.setColumnPinning` and `api.getColumnPinning` # Column pinning Pinning sticks a column to the **left** or **right** edge of the viewport so it does not scroll horizontally with the rest. Live demo - pin Company left, Price right, scroll the middle:
## Through the column menu Every column header has a menu (the `⋮` button). The menu has "Pin left" / "Pin right" / "Unpin" items. ## Programmatically `SvGridApi` exposes `setColumnPinning` and `getColumnPinning` for read / write from outside the grid: ```svelte (api = next)} /> ``` For initial pinning at mount, use the `initialColumnPinning` prop on ``: ```svelte ``` `getColumnPinning()` returns `{ left: string[]; right: string[] }` - the array order is the visible order along the pinned edge. ## Rendering - Pinned-left columns sit at the start of the visible row, with `position: sticky; left: px; z-index: 3`. - Pinned-right columns sit at the end of the visible row, with `position: sticky; right: px; z-index: 3`. - Offsets cascade - second-left column sits at the cumulative width of the first. ## Styling Pinned columns are visually differentiated from the scrollable middle through three layered cues: 1. **A distinct background tint** so the pinned strip reads as "frozen" even before you scroll. 2. **A 1-pixel divider** on the inside edge (the side facing the scrollable region) and a soft drop shadow that fades into the scroll area. 3. **A bolder header** font weight so the frozen header reads as part of the grid chrome. All three cues are driven by CSS custom properties. Override them to match your design system: | Token | Default | Used for | |-----------------------------|---------------------------------------------------------------------------|--------------------------------| | `--sg-pinned-bg` | `color-mix(in oklab, var(--sg-header-bg) 70%, var(--sg-accent) 8%)` | Body cells in pinned columns | | `--sg-pinned-header-bg` | `color-mix(in oklab, var(--sg-header-bg) 60%, var(--sg-accent) 14%)` | Header cells in pinned columns | | `--sg-pinned-divider` | `var(--sg-border)` | The 1-pixel inside-edge line | The fallbacks compute a subtle accent-tinted background from your existing header background, so a pinned column never looks identical to the rest of the grid even if you don't set anything. To opt out of the tint and match the body exactly: ```css .themed-host { --sg-pinned-bg: var(--sg-bg); --sg-pinned-header-bg: var(--sg-header-bg); } ``` Or to make pinned columns *very* obvious - useful for high-density financial grids where the user must instantly know which side is frozen: ```css .themed-host { --sg-pinned-bg: color-mix(in oklab, var(--sg-bg) 60%, var(--sg-accent) 20%); --sg-pinned-header-bg: color-mix(in oklab, var(--sg-bg) 40%, var(--sg-accent) 30%); --sg-pinned-divider: var(--sg-accent); } ``` ### Hover, selection, and zebra rows The pinned tint sits *under* the hover, selection, and zebra row backgrounds via stacked `linear-gradient` paints. That means: - Hovering a row in a pinned cell shows the pinned tint *with* the hover overlay on top - the user still sees the row highlight. - Selecting a row layers the selection colour over the pinned tint. - Zebra rows re-paint the pinned cell so the alternating row background doesn't leak through the pin. You don't need to override anything for these states to work; they follow `--sg-row-hover-bg`, `--sg-selection-bg`, and the pinned tokens automatically. ## Multiple pinned columns Multiple pins are stacked in the order they were pinned. The first-pinned column is the outermost. ## Gotchas - Pinning **plus** column virtualization is supported, but the pinned columns are always rendered (they never enter the virtualized window). - If you have so many pinned columns that they exceed the viewport width there is no horizontal scrollbar within the pinned regions - the user loses access to the non-pinned middle. Pin only "anchor" columns (identifier, action, status) and keep the count single-digit. ## See also - [Column state](./column-state.md) - [`SvGridApi` reference](../../reference/SvGridApi.md) - `setColumnPinning` / `getColumnPinning` signatures. # Column sizing Each column has a pixel width. The default for all columns is the grid's `columnWidth` prop (default ~140 px); each column can override via its `width` field.
```ts const columns: ColumnDef<{}, Person>[] = [ { field: 'firstName', header: 'First name', width: 150 }, { field: 'department', header: 'Department', width: 180 }, { field: 'salary', header: 'Salary', width: 120 }, ] ``` ```svelte ``` ## User resizing Every column has a resize handle on its right edge. Drag to widen / narrow; the minimum is 40 px. Resizes are stored per column id inside the grid component. There is no opt-out today - if you do not want users to resize a column, overlay your own pointer-blocking element on the header or wrap in CSS: ```css table[role='grid'] th[data-col-id="firstName"] [data-resize-handle] { pointer-events: none; } ``` ## Programmatic resizing + persistence The imperative API exposes `setColumnWidth` and `getColumnWidths`: ```svelte (api = next)} /> ``` `getColumnWidths()` returns every column's *current effective* width (user resize OR columnDef `width` OR grid-wide default), so the snapshot round-trips cleanly. ## Auto-fit `fitColumns={true}` scales every column proportionally to fill the viewport - the wrapper handles rounding-residue absorption + a modest shrink-to-fit (down to ~85 % of natural widths). For "fit to content" (longest cell text), pre-compute per-column widths once after a data load and call `api.setColumnWidth(id, w)` per column. ## Column virtualization With many columns, enable column virtualization so only the visible columns render: ```svelte ``` See [examples/src/demos/06-large-dataset.svelte](../../../examples/src/demos/06-large-dataset.svelte) for a 100-column virtualized grid. ## Gotchas - A `width` set in the column def is an **initial** width. Once a user has resized, the override on disk wins. The library does not expose the current widths to outside code; if you need to persist them, file an issue (or PR) for `getColumnWidths()` on `SvGridApi`. ## See also - [Column moving](./column-moving.md) - [Column pinning](./column-pinning.md) # Column spanning "Column spanning" lets a single body cell span across **multiple columns** - useful for full-width subtotals, group banner rows, or notes embedded inside a wide grid. ## Status This is **not yet built in** to the community grid. There is no `colSpan` field on `ColumnDef` or `CellContext`. There are two close-enough workarounds: ## 1. Full-width "row banner" via grouping If your span semantics are "render an aggregate above each group", the [grouping](../rows/row-data.md#grouping) pipeline gives you a group row that fills the row width via the group label column. See [examples/src/demos/07-grouping-aggregation.svelte](../../../examples/src/demos/07-grouping-aggregation.svelte). ## 2. Custom cell with `position: absolute` If you need an irregular full-width content cell inside an otherwise normal row, render a regular cell whose content spills across columns: ```svelte {#snippet Banner(p: { row: Row })} {p.row.note} {/snippet} ``` You'll need a CSS contortion to disable borders on the covered cells. This is fragile - only use it for one-off rows like "no results" placeholders. ## See also - [Row spanning](../rows/row-spanning.md) - the row-side analogue, also not built in. - [Missing features](../missing-features.md) # Column state "Column state" is the bag of per-column settings that change at runtime: visibility, width, pinning, sort, filter. SvGrid keeps these as separate state slices inside the grid instance.
## Slices | Slice | Where it lives | How to read | How to write | | ----- | -------------- | ----------- | ------------ | | Visibility | `` internal | `api.isColumnVisible(id)` | `api.setColumnVisible(id, visible)` | | Width | `` internal (resize handles) | drag the right edge of a header | - | | Pinning | `` internal | via column menu | via column menu | | Sort | `state.sorting` | `grid.getState().sorting` | `api.setSort(id, dir)` / `api.clearSort()` | | Filter | `state.columnFilters` | `grid.getState().columnFilters` | `api.setFilter(id, ...)` / `api.clearFilter(id)` | ## Persisting state To round-trip column state (e.g. through `localStorage` or the URL), subscribe via the wrapper's change callbacks and re-apply on mount through the imperative API: ```svelte { api = next // Re-apply saved state on first mount. for (const s of initial.sorting ?? []) api.setSort(s.id, s.desc ? 'desc' : 'asc') for (const f of initial.filters ?? []) api.setFilter(f.id, { operator: f.operator, value: f.value }) }} onSortingChange={(next) => (sorting = next)} onFiltersChange={(next) => (filters = next.columns)} /> ``` ## Resetting state There is no single "reset" API today. To revert: - Sort: `api.clearSort()` - Filters: iterate the active list and call `api.clearFilter(id)` - Visibility: walk your columns and call `setColumnVisible(id, true)` Wrap whichever subset you need into your own helper if you do this often. ## Gotchas - Column **width** is currently tracked per column id inside the SvGrid component and is not exposed on `SvGridApi`. If you need to persist user resizes you'll have to read the cells after the grid renders or PR an accessor onto the API. See [missing-features.md](../missing-features.md). - Column **pinning** is controlled by the column menu; there is no public setter on the API today. Same caveat as above. ## See also - [Column moving](./column-moving.md) - [Column pinning](./column-pinning.md) - [Updating definitions](./updating-definitions.md) # Custom header components The `header` field on a `ColumnDef` accepts a function that returns either a `renderSnippet(...)` or `renderComponent(...)`. The function receives a `HeaderContext` so it can access the column, header, and grid.
## With a snippet ```svelte {#snippet SalaryHeader(p: { sorted: false | 'asc' | 'desc' })} 💰 Salary {#if p.sorted === 'asc'}↑{:else if p.sorted === 'desc'}↓{/if} {/snippet} ``` ## With a component ```ts import HeaderWithIcon from './HeaderWithIcon.svelte' import { renderComponent } from 'sv-grid-community' const columns = [ { field: 'status', header: () => renderComponent(HeaderWithIcon, { icon: 'flag', label: 'Status' }), }, ] ``` ## HeaderContext The argument passed to your header callback exposes: ```ts type HeaderContext = { header: Header column: Column table: SvGrid } // Column gives you sort state, filter capability, and the toggle handler: ctx.column.getCanSort() ctx.column.getIsSorted() // false | 'asc' | 'desc' ctx.column.getToggleSortingHandler() // () => void ``` ## When to use it Anything that needs more than a string belongs here - multi-line headers, filter icons inside the header, units, tooltips, custom sort indicators, a "select all" checkbox in a leading column. ## Gotchas - Wrap your snippet output in **inline-level** markup (``, `
` with `inline-flex`). The grid renders the result inside a `
`'s text node position - block layout will misalign with the sort indicator and pin-handle decorations the grid adds around it. - Snippet/component props are recomputed on every render. Keep them cheap. ## See also - [Cell components](../cells/cell-components.md) - same API on the body side. # Columns tool panel The tool panel is the docked sidebar - standard in enterprise grids - for managing columns without hunting through a right-click menu. Turn it on with the `toolPanel` prop: ```svelte ``` A columns button appears at the grid's top-right. Clicking it opens a panel docked on the right edge with, for every column: - a **visibility** checkbox (show / hide the column), - **↑ / ↓** to reorder the column, - **⊞** to group / ungroup by that column (when grouping is enabled). ## Notes - The panel lists **all** columns, including hidden ones, in the current display order, so you can bring a hidden column back. - Visibility, order, and grouping changes go through the same engine state as the column menu and the imperative API (`setColumnVisible`, `setColumnOrder`, `setGroupBy`) - so they round-trip with `getState()` / `setState()` and [named views](../state/named-views.md). - Grouping a column whose other columns declare an [`aggregate`](../grouping/aggregators.md) rolls those values up in the group header automatically. See the live [Columns tool panel](https://sv-grid.com/demos/146-tool-panel) demo. # Updating column definitions There are two ways to change columns after the grid has mounted: ## 1. Reassign the `columns` prop `` is reactive. Replace the array (or mutate a `$state` array) and the grid re-derives its internal columns. ```svelte ``` ## 2. Use the imperative API The wrapper exposes mutators via `onApiReady`: ```svelte (api = next)} /> ``` Available column mutators on `SvGridApi`: | Method | What it does | | ------ | ------------ | | `addColumn(col, position?)` | Insert one column. `position` is `'left' \| 'right' \| number` (default `'right'`). | | `addColumns(cols, position?)` | Insert many. | | `removeColumn(id)` | Remove by column id (or field when no `id`). | | `setColumnVisible(id, visible)` | Show / hide. | | `isColumnVisible(id)` | Read visibility. | The imperative path is the right choice when the column-change initiator is **outside** the parent that owns the `columns` array - e.g. a toolbar component that doesn't know about the data source. ## What is preserved when columns change When you add or remove a column: - Sort state survives if the sorted column is still present. - Filter state for the removed column is discarded. - Active-cell focus is clamped into bounds. - Column widths set by the user via resize handles are preserved by column id. - Pinning is preserved by column id. When you **reorder** columns by reassigning the array, the grid renders them in the new order; pinned-left and pinned-right groups retain their order relative to themselves. ## Gotchas - Anything that captures `ctx.column.columnDef` inside a `cell` callback will see the **new** column def after a swap. Don't cache it. - If you reassign the entire `columns` array on every render, you'll pay the cost of re-deriving headers each time. Memoise it (build once with `$state.raw` or a one-time IIFE) for hot-loop components. ## See also - [Column state](./column-state.md) - [Column moving](./column-moving.md) # Comparison: SvGrid vs AG Grid vs TanStack Table This is the page enterprise evaluators land on first. It is honest. The three projects solve overlapping problems and the right choice depends on your stack, your budget, and which constraints bite hardest. ## TL;DR | Project | Lives in | Ships | Bundle (typical) | License | | -------------------- | ---------------------------------------- | --------------------------------------- | ---------------- | ------------------ | | **SvGrid** | Svelte 5 | Headless core + Svelte render + Pro pack | ~7.5 KB headless / ~42 KB full (gzip) | MIT (Community) / commercial (Pro) | | **AG Grid Community**| React, Angular, Vue, plain JS | Full grid + renderer | ~340 KB | MIT | | **AG Grid Enterprise**| same | Adds pivot, integrated charts, server-side row model, more | ~600 KB+ | Commercial | | **TanStack Table** | React, Vue, Svelte, Solid, Qwik, Lit, JS | Headless engine **only** | ~12-14 KB | MIT | ## When SvGrid is the right choice - You're on **Svelte 5** and want a grid that uses the runtime's idioms (snippets for cells, `$state` for data, `$derived` for aggregates) - not a React-port pretending to be Svelte. - You want a **headless core you can render yourself** AND a default-styled component for the 80% case. Most "headless" libraries make you write the markup; most "monolith" libraries make you fight the markup. SvGrid does both in one package. - You need **clean theming via CSS custom properties** and a documented `--sg-*` token surface, not a hard-coded class soup. - You ship under **strict CSP** (no `eval`, no `new Function`, no inline scripts). SvGrid runs clean; AG Grid Community does too. TanStack Table is engine-only so the question doesn't apply. - You want **SSR markup that is meaningful before hydration** (good first paint, SEO, SvelteKit `+page.server` integration). SvGrid + TanStack Table both qualify. AG Grid renders client-side. ## When AG Grid is the right choice - You're on **React, Angular, or Vue**, not Svelte. SvGrid is Svelte-only. - You need **every grid feature shipped** out of the box: row drag, master-detail with built-in API, range selection, status bar, context menu, column tool panel, integrated charts (Enterprise), Excel-native pivot UI (Enterprise), server-side row model (Enterprise). - You need **enterprise commercial support** with SLAs. AG sells it; SvGrid Pro support is best-effort. ## When TanStack Table is the right choice - You want a **rendering-framework-agnostic engine** so the same business logic powers React + Svelte + Solid surfaces in your monorepo. - You're already in the TanStack ecosystem (Query, Router, Form, Virtual) and want one mental model. - You're happy writing **all the markup yourself** - the row recycling, the keyboard map, the ARIA roles, the focus management, the drag-to-resize. That's the cost of "engine only". ## Feature parity at a glance | | SvGrid Community | SvGrid Pro | AG Grid Community | AG Grid Enterprise | TanStack Table | | ------------------------------- | ---------------- | ---------- | ----------------- | ------------------ | -------------- | | Headless core (engine only) | ✓ | ✓ | - | - | ✓ | | Default render component | ✓ (Svelte 5) | ✓ | ✓ (each FW) | ✓ | - | | Sort (multi-column) | ✓ | ✓ | ✓ | ✓ | ✓ | | Filter menu (operator + facet) | ✓ | ✓ | ✓ | ✓ | - | | Filter row | ✓ | ✓ | ✓ | ✓ | - | | Pagination | ✓ | ✓ | ✓ | ✓ | ✓ | | Grouping + aggregation | ✓ | ✓ | basic | ✓ (advanced) | ✓ (engine) | | Tree / expand-collapse rows | ✓ | ✓ | basic | ✓ | ✓ | | Cell range selection + copy/paste | ✓ | ✓ | ✓ (Enterprise) | ✓ | - | | Inline editing (5 editor types) | ✓ | ✓ | ✓ | ✓ | - | | Column virtualization | ✓ | ✓ | ✓ | ✓ | - | | Row virtualization | ✓ | ✓ | ✓ | ✓ | - | | Column pinning (left/right) | ✓ | ✓ | ✓ | ✓ | - | | Fit-to-width with shrink | ✓ | ✓ | partial | ✓ | - | | WAI-ARIA grid pattern | ✓ | ✓ | ✓ | ✓ | - | | Server-side row model | external mode | external mode | - | ✓ (built-in) | external mode | | CSP-clean (no eval, no inline) | ✓ | ✓ | ✓ | ✓ | n/a | | Meaningful SSR markup | ✓ | ✓ | - | - | depends on FW | | Excel / PDF / CSV export | - | ✓ | - | ✓ (Enterprise) | - | | Excel / CSV import | - | ✓ | - | - | - | | AI assistant | - | ✓ (BYO provider) | - | - | - | | Pivot table | - | ✓ | - | ✓ | (custom) | | Integrated charts | - | - | - | ✓ | - | | Theming via CSS variables | ✓ (`--sg-*`) | ✓ | ✓ (theme builder) | ✓ | n/a | | Source-button per demo | ✓ (gallery) | ✓ | - | - | - | ## Bundle size Measured gzipped, with Svelte treated as a peer dependency and excluded (the bundlephobia convention): | sv-grid-community path | Gzipped | Minified | | ----------------------------------------------- | ------- | -------- | | Headless core (`createSvGrid` + a row model) | ~7.5 KB | ~35 KB | | Full `` render component | ~42 KB | ~189 KB | The full render component is the whole grid - virtualization, Excel-style filters, inline editing, grouping, tree, master/detail, and accessibility - in one import. For reference, the headless core is lighter than TanStack Table's headless engine (~12-14 KB), and the render component is a fraction of AG Grid Community (commonly cited around 340 KB minified). `sv-grid-pro` features are separate subpath imports that lazy-load, so they add nothing to your initial bundle until used. ## Migrating from AG Grid The most common starting point. See [Migrating from AG Grid](./migrating-from-ag-grid.md) for a 30-minute, side-by-side translation of column defs, features, filtering, editing, and the imperative API. ## Migrating from TanStack Table The map is one-to-one - SvGrid's headless core is API-compatible with TanStack Table's React adapter in 90% of cases. The big differences: - Replace `useReactTable(opts)` with `createSvGrid(opts)`. Identical state machine. - Replace `getCoreRowModel()` calls with the same name from `sv-grid-community`. - The render layer changes - TanStack hands you `flexRender` + the row model; SvGrid lets you keep that headless approach OR drop in the default `` component. ## Pricing SvGrid Community is MIT - free for commercial use, no attribution required at runtime. SvGrid Pro is a paid license; see for per-seat / per-app / multi-app tiers. AG Grid Community is MIT. AG Grid Enterprise pricing is on ag-grid.com; expect a per-developer annual license plus a separate deployment license for production. TanStack Table is MIT. ## See also - [Why headless?](../why-headless.md) - the design decision behind SvGrid's two-layer architecture. - [Migrating from AG Grid](./migrating-from-ag-grid.md) - the practical recipe. - [Pro feature pack](../pro/README.md) - what SvGrid charges for. - [Missing features](./missing-features.md) - the honest gap list versus AG Grid Enterprise. ## Frequently asked questions ### What is the best data grid for Svelte 5? For a Svelte-5-native grid with a batteries-included render component, SvGrid is purpose-built for runes and snippets. TanStack Table is a strong headless-only choice if you want to build the DOM layer yourself across frameworks. AG Grid is the most feature-complete but lives in React/Angular/Vue first and is heavy to bridge into Svelte 5. ### Is SvGrid a good AG Grid alternative? Yes, for Svelte projects. SvGrid ships a much smaller bundle (~42 KB gzipped for the full render component, ~7.5 KB headless) than AG Grid Community, is MIT-licensed for commercial use, and offers `sv-grid-pro` for export/pivot/import at a per-developer price instead of AG Grid Enterprise's per-deployment licensing. It does not yet match every AG Grid Enterprise feature - see the missing-features list for the honest gaps. ### SvGrid vs TanStack Table - which should I pick? Pick SvGrid if you want virtualization, Excel-style filters, selection, and inline editing working out of the box on Svelte 5. Pick TanStack Table if you want a framework-agnostic headless engine and are happy to build the rendering, virtualization, and editing UI yourself. Both are MIT-licensed. ### How big is the SvGrid bundle? Measured gzipped (Svelte excluded as a peer dependency): ~7.5 KB for the headless core and ~42 KB for the full `` render component (~189 KB minified). Pro features are separate, lazy-loaded subpath imports, so you ship only what you import. # Conditional form schema A pattern for declarative field-visibility and editability rules inside a grid. Instead of writing imperative `if`/`else` checks across many `cell` callbacks and `editable` props, you declare a **schema** of `when`-rules per field and the grid evaluates them per row. This is the data-entry pattern that procurement / KYC / onboarding flows usually need: some fields apply only to certain record types, others lock once the workflow moves past a state. SvGrid's `editable: (ctx) => boolean` callback supports per-cell rules out of the box; this recipe layers a small schema on top so the rules live next to the data, not scattered through the column definitions.
## The schema shape A `Rule` is a predicate plus a human-readable reason. A `FieldSchema` carries up to two rules per field - one for **visibility** (the cell renders the value or `-`), one for **editability** (the editor can open). ```ts type Rule = { when: (row: T) => boolean reason: string } type FieldSchema = { visible?: Rule editable?: Rule } const schema: Partial>> = { taxId: { visible: { when: (r) => r.recordType === 'business', reason: 'Tax ID only applies to businesses.' }, }, rejectionReason: { visible: { when: (r) => r.status === 'rejected', reason: 'Reason only applies to rejected applications.' }, editable: { when: (r) => r.status === 'rejected', reason: 'Only editable while status is rejected.' }, }, contactEmail: { editable: { when: (r) => r.status === 'draft' || r.status === 'pending', reason: 'Locked once the application is approved or rejected.' }, }, } ``` Three helper functions translate the schema into runtime decisions: ```ts function isVisible(schema: Partial>>, field: keyof T, row: T): boolean { const s = schema[field] return !s?.visible || s.visible.when(row) } function isEditable(schema: Partial>>, field: keyof T, row: T): boolean { const s = schema[field] if (!isVisible(schema, field, row)) return false return !s?.editable || s.editable.when(row) } function lockReason(schema: Partial>>, field: keyof T, row: T): string | null { const s = schema[field] if (!s) return null if (s.visible && !s.visible.when(row)) return s.visible.reason if (s.editable && !s.editable.when(row)) return s.editable.reason return null } ``` You feed the editability decision into SvGrid's `editable` callback, and the visibility decision into the cell's `cell` snippet. ## Complete drop-in example A KYC application list with three record types (individual, business, nonprofit) and a four-state workflow (draft → pending → approved / rejected). Every column's visibility and editability comes from the schema. ```svelte {#snippet Cell(props: { row: Application; field: keyof Application })} {@const visible = isVisible(props.field, props.row)} {@const editable = isEditable(props.field, props.row)} {@const reason = lockReason(props.field, props.row)} {visible ? String(props.row[props.field] ?? '') : '-'} {#if visible && !editable} {/if} {/snippet} ``` ## What happens at runtime Try editing each row in the demo above: - **A003 (Sarah Chen, draft):** SSN field renders `4421`. EIN and Tax ID show `-`. Changing `recordType` to `business` causes Tax ID to start rendering and SSN to switch to `-`. - **A001 (Atlas Holdings, pending):** Tax ID is editable. Setting `status` to `approved` locks the email and legal name fields with the 🔒 indicator. - **A004 (Vertex Capital, rejected):** Rejection reason is visible and editable. Setting `status` back to `pending` blanks the reason visually (still in the data, just hidden). ## Composing more rules Stack rules per field by AND-ing inside a single `when`: ```ts { field: 'managerApprovalNote', editable: { when: (r) => r.status === 'pending' && r.amount > 10_000, reason: 'Required only when status is pending AND amount > $10,000.', }, } ``` For OR semantics, write it directly: ```ts { field: 'taxId', visible: { when: (r) => r.recordType === 'business' || r.recordType === 'nonprofit', reason: 'Tax ID applies to business and nonprofit records.', }, } ``` ## Patterns that pair well - **PII masking** - combine schema with `currentUser.role` checks (see [Demo 35 - Permissions, audit & history](../../examples/src/demos/35-permissions-audit.svelte)) - **Validation while editing** - schema gates *which* fields can be edited; validation gates *which values* commit. See [Validation](./editing/validation.md). - **Mobile card view** - the same schema works inside the card-mode edit panel; just import the helpers and skip rendering hidden fields. ## See also - [Demo 82 - Conditional form schema](../../examples/src/demos/82-conditional-form-schema.svelte) - full enterprise example - [Edit components](./editing/edit-components.md) - [Validation](./editing/validation.md) - [Permissions audit](../../examples/src/demos/35-permissions-audit.svelte) - role-based version of the same pattern # Edit components The grid ships with five inline editors. Each is selected by the column's `editorType`:
| `editorType` | DOM element | Notes | | ------------ | ----------- | ----- | | `'text'` | `` | default | | `'number'` | `` | parsed with `parseEditorValue('number', ...)` | | `'date'` | `` | round-trips to ISO `YYYY-MM-DD` | | `'datetime'` | `` | round-trips to ISO 8601 | | `'checkbox'` | `` | toggled on `Enter` / `Space` | The editor renders **inside the cell** - same width, same row height, zero border. See [Styling cells → edit-mode cell](../cells/styling-cells.md#edit-mode-cell). ## Custom editor There is no `cellEditor` field on `ColumnDef` today and no way to plug in a third-party component as the inline editor. To approximate one: 1. Render the column read-only with a custom `cell` callback. 2. Open your own popover on click / `F2`. 3. Write back through `api.setCellValue(rowIndex, columnId, value)`. ```svelte {#snippet StatusCell(p: { row: Person })} {/snippet} ``` A first-class `cellEditor` plug-in slot is on the [gap list](../missing-features.md). ## Conditional editability There is no `editable: (row) => boolean` callback. Closest approximation: swap the column between an editable and a read-only version by reassigning `columns`. ## See also - [Provided editors](./provided-editors.md) - [Cell components](../cells/cell-components.md) - [Custom column filters](../filtering/custom-column-filters.md) - same shape, filter side. # Full-row editing "Full-row editing" means the user opens an entire row for edit (every editable cell becomes an editor simultaneously) and commits all changes at once.
## Status This is **not** built in. SvGrid edits one cell at a time. ## Workaround - overlay form Trigger an edit form from a row action and write changes back via `api.setCellValue` for each field: ```svelte {#if editing} {/if} (api = next)} /> ``` This is often *better UX* than full-row editing for keyboard-heavy users - the form can have proper field labels and a save button. ## Tracked at [Missing features](../missing-features.md) - full-row editing as a built- in mode. ## See also - [Provided editors](./provided-editors.md) - [Saving values](./saving-values.md) # Editing - overview Inline editing is a single prop on ``. Try it - double-click any cell, type to replace, hit `Enter` to commit. Tab moves to the next editable cell:
```svelte ``` To make a specific column editable, give it an `editorType`: ```ts const columns: ColumnDef[] = [ { field: 'firstName', header: 'First', editorType: 'text' }, { field: 'age', header: 'Age', editorType: 'number' }, { field: 'joinedAt', header: 'Joined', editorType: 'date' }, { field: 'active', header: 'Active', editorType: 'checkbox' }, ] ``` A column with no `editorType` is **read-only** even when `enableInlineEditing={true}`. ## How a user edits | Action | Keys | Outcome | | ------ | ---- | ------- | | Enter edit mode | `Enter` / `F2` / double-click | Editor opens on the active cell | | Commit | `Enter` / `Tab` | Saves the new value | | Cancel | `Esc` | Discards | | Move to next field while editing | `Tab` / `Shift+Tab` | Commits and re-enters edit on the neighbour | ## What gets saved When the user commits, the grid: 1. Parses the editor's string value through `parseEditorValue` for the column's `editorType`. 2. Writes the parsed value into the grid's internal data copy. 3. Fires `onCellValueChange` with `{ rowIndex, columnId, oldValue, newValue, row }`. 4. Fires a re-render. To round-trip edits to your source, attach a callback: ```svelte savePersonField(e.row.id, e.columnId, e.newValue)} /> ``` See [Saving values](./saving-values.md) for the full patterns (per-edit, batch-from-snapshot, cascade recompute). ## See also - [Start / stop editing](./start-stop-editing.md) - [Parsing values](./parsing-values.md) - [Saving values](./saving-values.md) - [Provided editors](./provided-editors.md) - [Validation](./validation.md) ## Frequently asked questions ### How do I enable inline editing in SvGrid? Set the inline-editing prop on `` and give editable columns an `editorType`. Then double-click a cell (or press F2), type to replace, and press Enter to commit; Tab moves to the next editable cell. ### What cell editors does SvGrid provide? Built-in editors for text, number, checkbox, date, select, rich-select, and textarea, chosen per column via `editorType`. You can also supply a custom editor through the `cellEditor` slot. ### Does SvGrid mutate my data array when editing? No. Commits are written to the grid's internal working copy, not the array you passed in, so cancel/undo is possible. Subscribe to `onCellValueChange` to persist edits to your own state or backend. # Parsing values When the user commits an edit, the editor's DOM value (a string for text / number / date inputs, a boolean for checkboxes) is parsed by `parseEditorValue` into the canonical value for the column's type.
```ts import { parseEditorValue } from 'sv-grid-community' parseEditorValue('text', 'Ada') // 'Ada' parseEditorValue('number', '42') // 42 parseEditorValue('number', '4.5') // 4.5 parseEditorValue('number', 'NaN') // null (rejected - caller decides) parseEditorValue('number', '') // null parseEditorValue('date', '2026-05-27') // '2026-05-27T00:00:00.000Z' parseEditorValue('datetime', '2026-05-27T14:32') // '2026-05-27T14:32:00.000Z' parseEditorValue('checkbox', 'true') // true parseEditorValue('checkbox', true) // true ``` The full source is short and worth reading: [`cell-editors.ts`](../../../packages/sv-grid-community/src/editors/cell-editors.ts). ## What "null" means `parseEditorValue` returns `null` to signal "could not parse". The grid treats `null` as an empty value and writes it into the cell. If you want **invalid input rejected** (the value reverts to its pre-edit state), intercept before the write - see [Validation](./validation.md). ## Custom parsing There is no per-column `valueParser` field on `ColumnDef` today. If you need custom parsing (e.g. accept "$42,500" and turn it into `42500`), you have two paths: 1. Post-process inside `cell` and store the raw display string. 2. Diff `api.getData()` against your own snapshot after each commit and normalise values you want canonicalised. A per-column `valueParser` is on the [gap list](../missing-features.md). ## See also - [Saving values](./saving-values.md) - [Validation](./validation.md) # Provided cell editors The grid ships nine built-in editors, selected via `editorType` on the column definition. Each editor renders inside the cell while editing and falls back to the default text render (or your `cell` snippet) when the cell is read-only.
| `editorType` | Renders | Stored value | |---------------|--------------------------------------|-------------------------| | `'text'` | `` | `string` | | `'number'` | `` | `number \| null` | | `'date'` | `` | ISO `YYYY-MM-DD` string | | `'datetime'` | `` | ISO 8601 string | | `'checkbox'` | Themed checkbox button | `boolean` | | `'list'` | Custom dropdown (single / multi) | scalar or array | | `'chips'` | Removable tag picker | array of values | | `'color'` | Native OS color picker | `#rrggbb` string | | `'rating'` | 5-star clickable widget | `number` 0-5 | ## Text editor - `editorType: 'text'` ``. Accepts any string. On commit the raw value is stored. ```ts { field: 'firstName', header: 'First name', editorType: 'text' } ``` ## Number editor - `editorType: 'number'` `` with browser-native increment buttons. Non-numeric input is rejected at commit (`parseEditorValue` returns `null` and the cell stays at its previous value). ```ts { field: 'age', header: 'Age', editorType: 'number' } // often paired with a display format: { field: 'salary', header: 'Salary', editorType: 'number', format: { type: 'currency', currency: 'USD', options: { maximumFractionDigits: 0 } }, } ``` ## Date editor - `editorType: 'date'` ``. The value is stored as ISO `YYYY-MM-DD`. ```ts { field: 'joinedAt', header: 'Joined', editorType: 'date', format: { type: 'date', pattern: 'y-m-d' }, } ``` ## Datetime editor - `editorType: 'datetime'` ``. The value is stored as ISO 8601 with a Z suffix. ```ts { field: 'updatedAt', header: 'Updated', editorType: 'datetime', format: { type: 'datetime', pattern: 'medium' }, } ``` ## Checkbox editor - `editorType: 'checkbox'` A themed button that toggles between `true` and `false`. Renders centred in the cell. ```ts { field: 'active', header: 'Active', editorType: 'checkbox' } ``` ## List editor - `editorType: 'list'` Single-select dropdown. Accepts an array of strings/numbers or `{ value, label, color }` objects. Set `editorMultiple: true` for a multi-select. ```ts { field: 'priority', header: 'Priority', editorType: 'list', editorOptions: ['low', 'med', 'high', 'urgent'], } // labelled options: { field: 'status', header: 'Status', editorType: 'list', editorOptions: [ { value: 'open', label: 'Open' }, { value: 'in_progress', label: 'In progress' }, { value: 'done', label: 'Done' }, ], } ``` ## Chips editor - `editorType: 'chips'` Removable-token picker for multi-select. The value is always an array. ```ts { field: 'tags', header: 'Tags', editorType: 'chips', editorOptions: ['frontend', 'backend', 'design', 'infra', 'docs'], } ``` ## Color editor - `editorType: 'color'` Native HTML color picker. The cell stores a `#rrggbb` string. Clicking the cell opens the OS color overlay; the value commits as soon as the overlay closes (no need to blur the cell first). ```ts { field: 'brandColor', header: 'Brand', editorType: 'color' } ``` ### Complete example A minimal grid where each row has its own swatch. The custom `cell` snippet shows the colour next to the hex value while not editing; the editor takes over on double-click. ```svelte {#snippet Swatch(props: { row: Brand })} {props.row.color} {/snippet} ``` ### Theme tokens The color editor inherits the grid's editor chrome. There are no extra tokens; the swatch fills the cell so the clickable area is the whole cell, not the OS default 25 × 13 swatch. ## Rating editor - `editorType: 'rating'` A row of 5 clickable stars plus a clear button. The cell stores an integer 0-5. Clicking a star commits immediately - no blur required. ```ts { field: 'csat', header: 'CSAT', editorType: 'rating' } ``` ### Complete example ```svelte {#snippet Stars(props: { row: Review })} {#each [1, 2, 3, 4, 5] as n (n)} {/each} {/snippet} ``` ### Theme tokens The rating editor reads three optional CSS custom properties so the star colors can match your design system: | Token | Default | Used for | |-------------------------|-------------|----------------------| | `--sg-rating-empty` | `#cbd5e1` | Unselected stars | | `--sg-rating-on` | `#f59e0b` | Selected stars | | `--sg-rating-hover` | `#fbbf24` | Hover preview | Override per theme: ```css [data-theme='dark'] { --sg-rating-empty: #475569; --sg-rating-on: #fbbf24; } ``` ## Disabling an editor on a per-cell basis `editable` accepts a callback. Return `false` to lock the cell - the editor never opens, even if you double-click. This composes with all the editor types above. ```ts { field: 'salary', header: 'Salary', editorType: 'number', // Only admins can edit salaries. editable: (ctx) => currentUser.role === 'admin', } ``` For declarative `when`-style rules across many columns, see the [Conditional form schema](../recipes.md#conditional-form-schema) recipe. ## See also - [Cell data types](../cells/cell-data-types.md) - [Parsing values](./parsing-values.md) - [Validation](./validation.md) - [Demo 80 - Cell types showcase](../../examples/src/demos/80-cell-types-showcase.svelte) - every editor in one enterprise grid - [Demo 66 - Custom cell editors](../../examples/src/demos/66-custom-cell-editors.svelte) - feature-health board using color, rating, and mood # Saving values When the user commits an edit, the new value is written into the grid's **internal data copy**. The grid does **not** mutate the array you passed in via the `data` prop - it keeps its own working copy so an undo / cancel is possible without touching your state. To round-trip edits back to your source there are two patterns. Live demo - typed editors, dirty tracking, save button:
## Pattern A - `onCellValueChange` (recommended) The wrapper fires `onCellValueChange` whenever an inline edit commits. The payload contains everything you need to forward the edit to a server, update a cached aggregate, or push to an undo stack: ```svelte ``` The wrapper has already written the parsed value into the row by the time the callback fires, so `event.row` reflects the post-edit state. The [`18-cascade-editing` demo](../../../examples/src/demos/18-cascade-editing.svelte) wires this into a recompute pipeline. ## Pattern B - read a snapshot from the API Useful for batch saves, "click Save to commit" UIs, or diffing against a before-snapshot: ```svelte (api = next)} /> ``` The [`05-inline-editing` demo](../../../examples/src/demos/05-inline-editing.svelte) shows this pattern with dirty-cell tracking against an `initial` snapshot. ## Sync data both ways Replacing the `data` prop forces the grid to re-read it. To mirror the grid's internal copy back into a parent `$state` so other UI can react, use `onCellValueChange`: ```svelte ``` ## See also - [Parsing values](./parsing-values.md) - [Validation](./validation.md) - [Undo / redo](./undo-redo.md) # Start / stop editing ## Start
| Trigger | Behaviour | | ------- | --------- | | Double-click a cell | Opens the editor with all text selected. | | `Enter` on a focused cell | Same as double-click. | | `F2` on a focused cell | Opens the editor with the caret at the end (no text selection). | | Typing a character | Opens the editor and replaces the value with the typed character. | The cell must be **focused** (the grid's active cell). Click any cell or use the arrow keys to set focus. The cell must also be **editable** - its column must have an `editorType`, and the grid must have `enableInlineEditing={true}`. ## Stop | Trigger | Outcome | | ------- | ------- | | `Enter` | Commit. Move focus down one row. | | `Tab` | Commit. Move focus to the next editable cell in the same row. | | `Shift+Tab` | Commit. Move focus to the previous editable cell. | | `Esc` | Cancel. Revert to the pre-edit value. | | Click outside the cell | Commit. | ## Programmatic start/stop There is no public `api.startEditing(rowIndex, columnId)` or `stopEditing()` on `SvGridApi` today. To force-edit a cell from outside, set the active cell (also not yet exposed on the public API) and dispatch a synthetic `F2` to the grid's root. This is on the [gap list](../missing-features.md). ## See also - [Validation](./validation.md) - [Provided editors](./provided-editors.md) # Undo / redo There is **no built-in undo/redo stack** inside ``. The grid emits no per-edit event for an external stack to subscribe to, so undo/redo has to be built on top of the data-snapshot approach.
## Pattern - snapshot stack ```svelte { const meta = e.ctrlKey || e.metaKey if (meta && e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo() } if (meta && ((e.key === 'z' && e.shiftKey) || e.key === 'y')) { e.preventDefault(); redo() } }} /> ``` ## Why JSON snapshots Without a per-edit event from the grid, the safest store of "previous state" is a deep copy of the row array. For a 5,000-row grid the snapshot is a few hundred KB - fine. For a 100,000-row grid, switch to a row-diff stack: ```ts type Diff = { rowId: string; column: string; before: unknown; after: unknown } ``` …and apply it during undo/redo. The per-edit event needed to compute that cleanly is on the [gap list](../missing-features.md). ## Tracked at [Missing features](../missing-features.md) - first-class undo stack with `onCellValueChange` to drive it. ## See also - [Saving values](./saving-values.md) - [demos/05-inline-editing.svelte](../../../examples/src/demos/05-inline-editing.svelte) # Validation There is no `validate(value)` callback on `ColumnDef` today. Validation happens by intercepting committed edits and either accepting or reverting them. Live demo - per-column rules with rollback + a recent-rejections panel:
## Built-in soft validation `parseEditorValue` already does light validation: - `number`: rejects non-finite results → returns `null` - `date` / `datetime`: rejects unparseable strings → returns `null` The grid writes `null` into the cell when this happens. That is "soft" validation - the user sees the cell go blank rather than seeing their input rejected with an explanation. ## Hard validation (reject + revert) To bounce the user back to the previous value with an explanation, maintain your own snapshot and revert after the commit: ```svelte {#if error}

Row {error.row + 1}: {error.msg}

{/if} (api = next)} /> ``` This polling-based validator works but has obvious limits: - The validator runs on every reactive tick, not strictly on commit. - The user briefly sees the invalid value before it reverts. A per-column `validate(value, row, column)` returning `string | true` is on the [gap list](../missing-features.md). ## Inline error UI Render an asterisk / red border via a custom cell renderer that reads your validation state map. See [Highlighting changes](../cells/highlighting-changes.md) for the same pattern with a "dirty" indicator - substitute "invalid" for "dirty". ## See also - [Parsing values](./parsing-values.md) - [Saving values](./saving-values.md) - [demos/05-inline-editing.svelte](../../../examples/src/demos/05-inline-editing.svelte) # Error reference Every `Error` thrown by `sv-grid-community` or `sv-grid-pro` with the exact message text, the trigger condition, and the fix. If a runtime message you see isn't on this list, it's coming from your own code or a peer dependency. ## How to read this page Each entry has four pieces: 1. **Message** - the exact text after `Error:`. Searchable. 2. **Class / type** - the thrown object's `.name`. 3. **When** - what action triggered it. 4. **Fix** - the smallest change that resolves it. Error classes are part of the [API stability](./api-stability.md) contract: the `.name` and message structure are Stable; we may reword the trailing detail at the patch level if the diagnostic improves. ## sv-grid-community ### `Error: SvGrid: cannot mount inside a non-element parent` - **Class:** `Error` - **When:** You passed something other than an `Element` to `mount(SvGrid, { target })`. Common cause: passing a Svelte component reference instead of a DOM node. - **Fix:** Use `bind:this={el}` on a real `
` and pass `el` as the target. ### `Error: Column "" has no field, accessorFn, or cell renderer` - **Class:** `Error` - **When:** A `ColumnDef` was registered with neither a `field`, an `accessorFn`, nor a `cell` template. The grid has no way to produce a value for that column. - **Fix:** Add one of the three. For computed display-only columns, `accessorFn: (row) => /* ... */` is usually right. ### `Error: Duplicate column id ""` - **Class:** `Error` - **When:** Two `ColumnDef` entries resolve to the same id - either because they share a `field` and neither has an explicit `id`, or because their explicit `id` collides. - **Fix:** Give each column an explicit unique `id`. ### `Error: Sort comparator failed: ` - **Class:** `Error` - **When:** A custom `sortingFn` returned `NaN` or threw. - **Fix:** Make the comparator total (always returns a number) and null-safe. ### `RuneError: state_referenced_locally` - **Class:** Svelte runtime - **When:** Initialising one `$state` from another `$state` in the same script - not specifically a SvGrid error, but the most common one users hit when seeding the grid's data. - **Fix:** Add `// svelte-ignore state_referenced_locally` above the line, or restructure so the initial value isn't read from another `$state`. ### `each_key_duplicate` - **Class:** Svelte runtime - **When:** Two `ColumnDef` entries share the same key in the grid's internal keyed-`{#each}` over columns. Most often when you have two columns both pointing at the same `field` (e.g. one computed "Total" column that uses `field: 'qty'` to satisfy the type but duplicates an existing data column). - **Fix:** Give the computed column an explicit `id` and drop its `field`. ## sv-grid-pro - License ### `Error: sv-grid-pro: setLicenseKey() requires a non-empty string` - **Class:** `Error` - **When:** `setLicenseKey('')` or `setLicenseKey(null)`. - **Fix:** Pass a valid `SVPRO-...` string. ### `Error: sv-grid-pro: invalid license key format (expected "SVPRO-..." prefix).` - **Class:** `Error` - **When:** A key that doesn't start with `SVPRO-` was set; the first Pro call throws. - **Fix:** Use the key issued by jQWidgets. The free Community grid is the right choice if you don't have a key. ### `Error: sv-grid-pro: this license key has been revoked. Contact sales@jqwidgets.com for a replacement.` - **Class:** `Error` - **When:** A key matching an entry in the package's revoked-keys list. - **Fix:** Contact `sales@jqwidgets.com` for a replacement. ## sv-grid-pro - Export ### `Error: sv-grid-pro: export requires a browser environment` - **Class:** `Error` - **When:** `exportGrid` / `api.exportData` was called during SSR. - **Fix:** Guard with `if (typeof window === 'undefined') return` or defer to `onMount`. ### `Error: sv-grid-pro: failed to load Smart.Utilities.DataExporter` - **Class:** `Error` - **When:** The Smart exporter shim couldn't initialise. Usually a bundler config issue blocking dynamic `import()` of a vendored asset. - **Fix:** Confirm the package is installed cleanly and your bundler permits dynamic imports. ### `Error: sv-grid-pro: xlsx export requires the "jszip" peer dependency. Install it with: pnpm add jszip` - **Class:** `Error` - **When:** First xlsx export when `jszip` isn't installed. - **Fix:** `pnpm add jszip` (or `npm` / `yarn`). ### `Error: sv-grid-pro: pdf export requires the "pdfmake" peer dependency. Install it with: pnpm add pdfmake` - **Class:** `Error` - **When:** First pdf export when `pdfmake` isn't installed. - **Fix:** `pnpm add pdfmake`. ## sv-grid-pro - Import ### `Error: sv-grid-pro: importData requires a browser environment` - **Class:** `Error` - **When:** `importData` was called during SSR. - **Fix:** Guard with `if (typeof window === 'undefined') return`. ### `Error: sv-grid-pro: xlsx import requires the "jszip" peer dependency. Install it with: pnpm add jszip` - **Class:** `Error` - **When:** First xlsx import when `jszip` isn't installed. - **Fix:** `pnpm add jszip`. ### `Error: sv-grid-pro: xlsx import expects a File or Blob, not a string. Use format: "csv" or "tsv" for inline text.` - **Class:** `Error` - **When:** Passing a string with `format: 'xlsx'`. - **Fix:** Either give the helper a `File` / `Blob` of the xlsx data or switch the format to `csv` / `tsv` for inline text. ### `Error: sv-grid-pro: could not locate sheet1.xml in the .xlsx archive` - **Class:** `Error` - **When:** The uploaded `.xlsx` is malformed or non-standard (e.g. a workbook saved by an exotic tool that places the first sheet somewhere other than `xl/worksheets/sheet1.xml`). - **Fix:** Open the file in Excel or LibreOffice and re-save. If the file is correct and the grid still fails, please file an issue with a redacted sample. ### `Error: sv-grid-pro: JSON import expects a top-level array` - **Class:** `Error` - **When:** The JSON parses to an object (or any non-array) instead of an array of records. - **Fix:** Wrap the data in `[]` or write a wrapper that extracts the array from the response. ## sv-grid-pro - AI ### `NoProviderError: sv-grid-pro/ai: no AI provider registered. Call setAIProvider(fn) with an adapter that talks to OpenAI / Anthropic / your proxy.` - **Class:** `NoProviderError` (extends `Error`) - **When:** Any `api.ai.*` call before `setAIProvider(fn)` ran. - **Fix:** Wire your adapter at app boot. For demos, use `setAIProvider(mockAIProvider)`. ### `BadJsonError: sv-grid-pro/ai: provider returned non-JSON for a json-format request. First 200 chars: ` - **Class:** `BadJsonError` (extends `Error`) - **When:** A helper asked the provider for JSON and got prose back. The first 200 characters of the response are included so you can see what the model actually returned. - **Fix:** Make sure your provider passes `responseFormat: 'json'` to the model, or instruct the model in your system prompt to return JSON only. The package strips a single markdown code fence automatically before parsing; multiple fences or surrounding prose trip this error. ### `Error: sv-grid-pro/ai: aiSmartFill requires at least one example.` - **Class:** `Error` - **When:** `aiSmartFill({ examples: [] })`. - **Fix:** Pass at least one worked example. Two or more locks the pattern more reliably. ## How to report a missing entry If you hit a thrown message that isn't on this page: 1. Copy the exact message text and class name. 2. File an issue with `[error reference]` in the title. 3. We treat this list as **canonical** - every new error added in a release lands here in the same PR. ## See also - [API stability](./api-stability.md) - the contract around these classes and messages. - [Testing and quality](./testing-and-quality.md) - the test suite that produces every error here as part of its expected-throw assertions. # Data export and printing - Pro Export the grid to **Excel (xlsx)**, **PDF**, **CSV**, **TSV**, **HTML**, or open a printable view in a new window. Ships in the paid **[sv-grid-pro](https://www.npmjs.com/package/sv-grid-pro)** add-on; the Community build does not include these features. Try the export bar below - downloads run in your browser; the bundled license key removes the unlicensed watermark:
## What it is `sv-grid-pro` augments the `SvGridApi` you already get from `` with two methods: - `api.exportData({ format, filename?, columns?, rows?, pageOrientation? })` - `api.print({ title?, columns?, rows?, orientation? })` Both methods default to **the currently displayed rows** - sort, filter, or paginate the grid, and the export reflects that view automatically. ## When to use it - Reporting flows where users want to take the grid offline (spreadsheets, emailed PDFs). - Compliance / audit trails that require a printable artifact. - Quick CSV/TSV pulls for downstream pipelines. If you only need machine-readable data, prefer CSV / TSV - they have no peer dependencies and produce the smallest files. Use xlsx / PDF only when the recipient expects formatted documents. ## Minimal example ```svelte ``` ## Install ```bash pnpm add sv-grid-pro # Optional - install only the peers you actually use: pnpm add jszip # required for xlsx pnpm add pdfmake # required for pdf ``` CSV, TSV, HTML, and Print have **no extra dependencies**. The peer dependencies are lazy-loaded only when you call the format that needs them. ## Licensing `sv-grid-pro` has a tiered license gate: | Key state | Behavior | | ------------------------------------------ | -------- | | No key set (`setLicenseKey()` not called) | Feature works. Grid shows an unlicensed watermark linking to jqwidgets.com; console.log emits a one-time nudge. | | Key doesn't start with `SVPRO-` | Throws - programmer error. | | Key is in the revoked list | Throws - contact support for a replacement. | | `SVPRO-DEV-...` or `SVPRO-EVAL-...` | Works. One-time console.info notice. No watermark. | | Any other `SVPRO-...` | Works silently. | Buy a production key at ($599 / developer / year). `SVPRO-DEV-...` and `SVPRO-EVAL-...` keys cover local development and 14-day trials respectively. ## Reference ### `setLicenseKey(key: string): void` Stores the key in module state. Call once at app startup (e.g. in `main.ts`). Subsequent calls overwrite. ### `clearLicenseKey(): void` · `hasValidLicense(): boolean` · `dismissUnlicensedNudge(): void` Programmatic helpers. `hasValidLicense()` is useful when you want UI to branch on license status. `dismissUnlicensedNudge()` removes the watermark and stops the MutationObserver - call it after setting a valid key if you toggled the soft-gate during testing. ### `installPro(api): ProGridApi` Mutates the given `SvGridApi` to add `exportData` and `print`. Returns the same object with the augmented type, so existing references keep working. ### `api.exportData(opts)` - `Promise` | Option | Type | Default | Notes | | ----------------- | ----------------------------------------------------- | -------------------- | ----- | | `format` | `'xlsx' \| 'pdf' \| 'csv' \| 'tsv' \| 'html'` | required | `xlsx` needs `jszip`; `pdf` needs `pdfmake`. | | `filename` | `string` | `"grid"` | Extension is appended if missing. | | `columns` | `{ field: string; header?: string }[]` | every key of row[0] | Drives both column selection and header labels. | | `rows` | `ReadonlyArray` | `api.getDisplayedRows()` | Override to export the full dataset instead of the visible view. | | `pageOrientation` | `'portrait' \| 'landscape'` | `"portrait"` | PDF only. | Throws on missing peer (`jszip` / `pdfmake`), revoked / malformed license, or empty result set. With no license set, it runs but the grid is watermarked. ### `api.print(opts?)` - `Promise` Opens a new window with a paginated, printable HTML rendering of the grid and triggers the browser print dialog. | Option | Type | Default | | ------------- | -------------------------------------- | ----------- | | `title` | `string` | `"Grid"` | | `columns` | `{ field: string; header?: string }[]` | all keys | | `rows` | `ReadonlyArray` | `api.getDisplayedRows()` | | `orientation` | `'portrait' \| 'landscape'` | `"portrait"` | Browsers may block the popup unless `print()` is called from a user gesture (a click handler is fine - automatic on-load print is not). ## Gotchas - **Empty grids** - both `exportData` and `print` throw if there are no displayed rows. Catch the error and show a notice in the UI. - **Column ordering** - if you don't pass `columns`, the export uses `Object.keys(rows[0])` order. Pass `columns` explicitly when the row shape doesn't match the column order you want. - **Cell formatters** - only column-level format hints (date, number, currency) carry into xlsx / pdf. Custom snippet renderers are not serialized to file formats - provide a plain field for those rows instead. - **Print popup blocked** - `print()` resolves but the browser silently blocks the new window. Always trigger from a user click, and surface the thrown error. - **Bundle size** - the vendored exporter is ~50 KB minified. It is loaded lazily on first call so it does not bloat the initial bundle for users who never export. ## Frequently asked questions ### How do I export a Svelte data grid to Excel? Install `sv-grid-pro`, call `installPro(api)`, then call the export helper with `format: 'xlsx'`. The exporter writes a real OOXML workbook in the browser - no server round-trip. CSV, TSV, PDF, and HTML use the same call with a different format. ### Is export part of the free Community package? No. Export and printing ship in the paid `sv-grid-pro` add-on. The free `sv-grid-community` package covers the full grid (sorting, filtering, grouping, editing, virtualization) but not export/print/pivot/import. ### Does exporting bloat my bundle? No. The ~50 KB exporter is lazy-loaded on the first export call, so users who never export never download it. # Applying filters "Applying" a filter means running it against the data. SvGrid applies filters **eagerly** by default: every keystroke updates the row model and the visible rows (with a 150ms debounce inside the column menu's value input).
## Apply on Enter (manual) If you want the classic "apply"-button pattern (no filter runs until the user confirms), run in `externalFilter` mode and only push a new pre-filtered `data` array on commit: ```svelte e.key === 'Enter' && commit()} /> ``` The grid's own column-menu filter applies as the user types - there is no built-in "Apply" button. Use the pattern above when you need explicit commit semantics. ## Apply against the server When data is server-side, filtering happens on the backend. Mirror the state pattern but issue a request from the change handler. See [demos/09-server-side.svelte](../../../examples/src/demos/09-server-side.svelte) for the canonical implementation with debounce + abort. ## Apply once, then freeze To take a snapshot: ```ts const filteredData = api.getData().filter(myPredicate) // pass filteredData into a separate with filterMode="none" ``` This works when you want to render the filtered result inside an export-friendly grid that's independent of the user's current filters. ## See also - [Applying filters - server-side variant](../../getting-started.md#11-server-side-data) - [Filter API](./filter-api.md) # Custom column filters When the built-in operators and the set-filter pattern are not enough, take over the filter pipeline.
## Option A - supply a `filterFn` The simplest extension: register your own filter function and reference it per column. ```ts // types declare module 'sv-grid-community' { interface FilterFnsRegistry { inListCSV: (value: unknown, query: string) => boolean } } // register import { filterFns } from 'sv-grid-community' ;(filterFns as any).inListCSV = (value: unknown, query: string) => { const items = String(query).split(',').map((s) => s.trim().toLowerCase()) return items.includes(String(value ?? '').toLowerCase()) } // use const columnFilters = [{ id: 'status', value: 'active,pending', fn: 'inListCSV' }] ``` `filterFns` is a plain object so monkey-patching works - but the `FilterFnsRegistry` module augmentation is what tells TypeScript about the new key. ## Option B - control the entire filter pipeline Pass `externalFilter={true}` and feed the grid a pre-filtered array. The grid still records the in-UI filter state (so the menu and chips light up correctly) but does **not** filter rows itself - you do, in response to the `onFiltersChange` callback. This is the path server-side data sources, large remote datasets, and tree data take. ```svelte (filters = next.columns)} /> ``` The same pattern exists for sort (`externalSort` + `onSortingChange`) and works on the same grid - see the [server-side demo](../../../examples/src/demos/09-server-side.svelte). If you only want a single pre-filtered array and don't need the grid's filter UI at all, pass `filterMode="none"` instead and skip the callbacks entirely. ## Option C - custom column UI Render a custom header (via `header: () => renderSnippet(...)`) that includes your own filter widget. Update controlled state from the widget; the grid will react. See [Custom header components](../columns/custom-header-components.md). ## See also - [Filter API](./filter-api.md) - [Set filter](./set-filter.md) # Date filter A column with `editorType: 'date'` (or `'datetime'`) gets the **date** filter operator set: `equals`, `lessThan`, `greaterThan`, **`between`**, `isBlank`.
```ts const columns: ColumnDef<{}, Person>[] = [ { field: 'joinedAt', header: 'Joined', editorType: 'date', format: { type: 'date', pattern: 'y-m-d' }, }, ] ``` ## Value format Store dates as **ISO date strings** (`YYYY-MM-DD`) or as `Date` objects. The grid compares them via `Date.parse()` so both forms work, but stick to one for sort stability. For `'datetime'`, use full ISO 8601: `2026-05-27T14:32:00Z`. ## Date range Pick **Between** in the column menu's operator picker. The wrapper renders two date inputs (From / To); both endpoints are **inclusive**. ```ts api.setFilter('joinedAt', { operator: 'between', value: '2026-01-01', valueTo: '2026-12-31', }) ``` The headless filter helper works the same: ```ts import { applyExcelFilter } from 'sv-grid-community' applyExcelFilter('2026-05-27', { id: 'joinedAt', operator: 'between', value: '2026-01-01', valueTo: '2026-12-31', }) // → true ``` The filter is inactive until both endpoints are non-empty. ## Today / yesterday / last 7 days Not built in. Apply via the imperative API: ```ts function lastNDays(api: SvGridApi<{}, Person>, columnId: string, n: number) { const cutoff = new Date(Date.now() - n * 86_400_000).toISOString().slice(0, 10) api.setFilter(columnId, { operator: 'greaterThan', value: cutoff }) } lastNDays(api, 'joinedAt', 7) ``` ## Timezones The grid does not adjust dates for the user's timezone. If you store `'2026-05-27'` and the user is in UTC-08, the cell displays as `2026-05-27` (no shift), and a `greaterThan: '2026-05-26'` filter matches. That is usually what people want for **calendar** dates. For timezone- sensitive datetimes, store with `Z` suffix and use the `datetime` editor type. ## See also - [Filter conditions](./filter-conditions.md) - [Date editor](../editing/provided-editors.md#date-editor) # Filter API Two surfaces - pick based on whether the caller is inside or outside the component that owns the grid state.
## `SvGridApi` (imperative) Available via ` /* … */}>`. ```ts api.setFilter( columnId: string, filter: { operator: SvGridFilterOperator; value?: string } | null, ): void api.clearFilter(columnId: string): void ``` `SvGridFilterOperator` is the union: `'contains' | 'equals' | 'startsWith' | 'greaterThan' | 'lessThan' | 'isBlank'`. Passing `null` clears the filter on that column. ```ts api.setFilter('status', { operator: 'equals', value: 'active' }) api.setFilter('age', { operator: 'greaterThan', value: '30' }) api.setFilter('email', { operator: 'contains', value: '@example.com' }) api.setFilter('department', null) // clear api.clearFilter('age') // same effect, sugared ``` The wrapper internally writes into the `columnFilters` state slice and re-runs the filtered row model. ## Observe filter state For history, persistence, or server-side, subscribe via `onFiltersChange`: ```svelte { filters = next.columns; globalFilter = next.global }} /> ``` The callback receives a consolidated `{ global, columns }` shape so the three internal stores (global search, per-column operator filter, facet checklist) collapse into one payload you can serialise. For server-side / external pipelines, also set `externalFilter={true}` so the grid doesn't filter the rows locally - see [Custom column filters](./custom-column-filters.md). ## Read the current filters From the imperative API there is no `getFilters()` getter today - read the underlying state via the grid instance: ```ts const grid = api as unknown as { /* internals are not part of the public type */ } ``` This is not a stable surface. If you need to read filters from outside, control the state. ## See also - [Filter conditions](./filter-conditions.md) - [Applying filters](./applying-filters.md) - [Missing features](../missing-features.md) - `api.getFilters()` would be nice. # Filter conditions A "filter condition" is a `(column, operator, value)` triple. The grid stores filter conditions in `state.columnFilters`.
## Shape ```ts type ColumnFilter = { id: string // column id value: unknown // operator-specific value fn?: keyof typeof filterFns // optional explicit filter function } type ColumnFiltersState = ColumnFilter[] ``` Through the wrapper, the menu uses a richer per-column representation (operator + value) - the wrapper converts between the two when state crosses the boundary. ## AND vs. OR Multiple filter conditions for **different columns** AND together. Two filters on the **same column** are not natively supported - the second write overwrites the first. To express OR within a column (`status = active OR pending`), use the [set filter](./set-filter.md) pattern. To express OR across columns (`firstName = ada OR lastName = lovelace`), do it outside the grid by filtering the data array before passing it in. ## Conditions via the imperative API Apply or update conditions through the `SvGridApi` once it's available: ```svelte (api = next)} /> ``` ## Clearing ```ts api.clearFilter('department') // single column ``` A `clearAllFilters()` helper is on the [Missing features](../missing-features.md) list. Until then, iterate the columns you know are filtered and call `clearFilter` per id - the list of currently-filtered columns is reported by `onFiltersChange`. ```ts columnFilters = [] ``` ## See also - [Applying filters](./applying-filters.md) - [Filter API](./filter-api.md) # Floating filters "Floating filters" are the always-visible filter inputs that sit **between** the header row and the body - same idea as a filter row, but matched per-operator to the underlying column menu.
## Status SvGrid has a **filter row** (`filterMode="row"`) that gives you a single input per text column. It does not yet have full "floating filter" parity where the inline input mirrors the column menu's operator - e.g. a number column showing a `>` icon next to the filter input. The filter row uses `contains` for text columns and `equals` for number / date / checkbox columns. ## Enable the filter row ```svelte ``` If you also want the column menu icon present, set both surfaces explicitly: ```svelte ``` ## Per-operator floating filter Today this needs a custom header. Render your own input inside the column header via `header: (ctx) => renderSnippet(...)` and write into controlled `columnFilters` state when the user types. ```svelte {#snippet RangeHeader(p: { id: string })}
{p.id} applyMin(p.id, +e.currentTarget.value)} class="w-20 rounded border border-slate-300 px-1 py-0.5 text-xs" />
{/snippet} ``` ## Tracked at [Missing features](../missing-features.md) - first-class floating filters with operator parity. ## See also - [Overview](./overview.md) - [Custom header components](../columns/custom-header-components.md) # Number filter A column with `editorType: 'number'` gets the **number** filter operator set: `equals`, `greaterThan`, `lessThan`, **`between`**, `isBlank`. The default operator is `equals`.
```ts const columns: ColumnDef<{}, Person>[] = [ { field: 'age', header: 'Age', editorType: 'number' }, { field: 'salary', header: 'Salary', editorType: 'number', format: { type: 'currency', currency: 'USD' }, }, ] ``` ## Range - `between` Pick **Between** in the column menu's operator picker and the second input ("To") appears next to the first ("From"). Both endpoints are **inclusive**. Programmatically: ```ts api.setFilter('age', { operator: 'between', value: '18', valueTo: '65' }) ``` Or via the headless filter helper: ```ts import { applyExcelFilter } from 'sv-grid-community' applyExcelFilter(72, { id: 'age', operator: 'between', value: 18, valueTo: 65 }) // → false (72 > 65) ``` The grid treats the filter as inactive when either endpoint is empty. That way the user can type `from = 18` and the grid keeps showing every row until they finish typing the `to` value too. ## Numeric input parsing The filter value comes in from the DOM as a string. Use `parseEditorValue` to convert: ```ts import { parseEditorValue } from 'sv-grid-community' parseEditorValue('number', '4.5') // 4.5 parseEditorValue('number', 'abc') // NaN - reject ``` The grid does this for you when the user types into a header filter. ## Locale Number filters compare raw `Number(cellValue)` against `Number(filter.value)`. They do not parse "1,234.50" or "1 234,50" - feed the column raw numbers, and use a `format` on the column for display. ## See also - [Filter conditions](./filter-conditions.md) - [Number editor](../editing/provided-editors.md#number-editor) # Filtering - overview Click any column header's filter icon to open the operator + value popover; numeric and date columns range-bucket their distinct values so the menu stays usable on big datasets:
SvGrid offers four filtering surfaces. You opt into the one(s) you need through the `filterMode` prop on ``: | `filterMode` | What it shows | | ------------ | ------------- | | `'menu'` (default) | A "filter icon" in each header opens a per-column operator + value popover. | | `'row'` | A filter row under the header - one input per column. | | `'global'` | A single search box above the grid that searches all visible columns. | | `'none'` | No filter UI. Drive filters programmatically only. | ```svelte ``` Per-surface props (`showColumnFilters`, `showFilterRow`, `showGlobalFilter`) override `filterMode` when set explicitly - useful when you want two surfaces simultaneously. ## Feature registration Filtering is gated by `columnFilteringFeature` plus `createFilteredRowModel`. Both must be registered for the column filter UI to actually filter rows: ```ts import { tableFeatures, columnFilteringFeature, createFilteredRowModel, } from 'sv-grid-community' const features = tableFeatures({ columnFilteringFeature }) ``` The wrapper auto-registers `createFilteredRowModel` when the feature is present. ## Operators All built-in operators: | Operator | Applies to | Behaviour | | ------------- | ---------- | --------- | | `contains` | text | case-insensitive substring | | `equals` | text, num, date, bool | strict equality (numeric where possible) | | `startsWith` | text | case-insensitive prefix | | `greaterThan` | num, date | strict `>` | | `lessThan` | num, date | strict `<` | | `between` | num, date | inclusive range - requires `valueTo` | | `isBlank` | any | empty / null / undefined / whitespace | The set of operators offered per column depends on `editorType`: | `editorType` | operators | | ------------ | --------- | | `'text'` (default) | contains, equals, startsWith, isBlank | | `'number'` | equals, greaterThan, lessThan, isBlank | | `'date'` / `'datetime'` | equals, lessThan, greaterThan, isBlank | | `'checkbox'` | equals, isBlank | ## Built-in `filterFns` For programmatic filtering (without the menu), pass a `filterFn` on the column or use the headless `createFilteredRowModel` directly. ```ts import { filterFns } from 'sv-grid-community' filterFns.includesString(cellValue, query) filterFns.equals(cellValue, query) ``` ## See also - [Text filter](./text-filter.md) - [Number filter](./number-filter.md) - [Date filter](./date-filter.md) - [Set filter](./set-filter.md) - [Filter API](./filter-api.md) - [demos/03-excel-filters.svelte](../../../examples/src/demos/03-excel-filters.svelte) ## Frequently asked questions ### How do I filter a column in SvGrid? Click a column header's filter icon to open the operator + value popover. Text columns get `contains` / `equals` / `startsWith` / `isBlank`; number and date columns add `greaterThan` / `lessThan` / `between`. Filtering is on by default once the filtering feature is registered. ### Does SvGrid have Excel-style set filters? Yes. A set filter shows a checklist of a column's distinct values so users can include "active OR pending" with checkboxes. Numeric and date columns range-bucket their values so the list stays usable on large datasets. ### Can I filter on the server? Yes. Set `externalFilter` so the grid records filter state but your API does the filtering. The grid emits the consolidated filter payload via `onFiltersChange` for you to forward to the server. # Set filter A "set filter" (a.k.a. value filter, list filter) shows a checklist of all distinct values in a column and lets the user pick which to include. It's what you reach for to filter `status` to "active OR pending", or `department` to a few specific teams.
Three patterns are supported, all wired through the imperative `api.setFacetFilter(columnId, values | null)`: | Mode | UI | Built-in? | When to use | | --- | --- | --- | --- | | **Excel-style** | Column-menu Values tab | Yes | Most columns. Distinct values are enumerated from the loaded data; search box + select-all + clear ship out of the box. | | **Async** | Side panel that loads values from a server endpoint | User-land (one screen of code) | The column has too many distinct values to list at full data load. Lazy-fetch on demand. | | **Tree-list** | Hierarchical checkboxes (parent ↔ descendants) | User-land (cascade logic) | Nested taxonomies: Region → Country → City; Department → Team → Employee. | ## 1. Excel-style (built-in) The column menu's Values tab is the default set filter. Click the funnel icon on any header to open it. What you get without writing any code: - Distinct values from the current dataset. - Type-ahead search. - Select-all / clear toggle. - Mixed-state preserved as the user scrolls. Programmatic equivalent for "remember and restore": ```ts // Capture the current set const filters = api.getFilters() // includes selectedValues per column // Restore later (e.g. saved view, URL persistence) api.setFacetFilter('status', ['active', 'pending']) api.setFacetFilter('status', null) // clear ``` ## 2. Async values (server-loaded) When a column has tens of thousands of distinct values, you don't want to pre-render them all. Pattern: render a side panel beside the grid, load values from the server on first open, drive the grid via `api.setFacetFilter`: ```ts let state = $state<{ status: 'idle' | 'loading' | 'ready' | 'error' values: string[] }>({ state: 'idle', values: [] }) let selected = $state>(new Set()) async function loadValues() { state = { state: 'loading', values: [] } const res = await fetch('/api/orders/customers') const values = await res.json() state = { state: 'ready', values } } function toggle(value: string) { const next = new Set(selected) if (next.has(value)) next.delete(value); else next.add(value) selected = next api.setFacetFilter('customer', next.size === 0 ? null : Array.from(next)) } ``` Key benefits vs the Excel tab: - **Lazy load** - no client-side enumeration for millions of distinct values. - **Server can apply policy** - hide values the current user shouldn't see. - **Static label / dynamic value** - the panel can show pretty labels while the filter applies on the underlying id. See demo 111 ("Async values" card) for a complete implementation with loading state, retry, and search. ## 3. Tree-list (hierarchical) For nested taxonomies, render a tree of checkboxes. Parent checked = all descendants checked. Some descendants checked = parent shows the "indeterminate" state. On any change, compute the leaf set and apply it to the column. ```ts // Taxonomy: Region → Country → City const GEO = { Americas: { 'United States': ['New York', 'San Francisco'], Canada: ['Toronto'] }, EMEA: { Germany: ['Berlin', 'Munich'], France: ['Paris'] }, } let selectedCities = $state>(new Set()) function toggleNode(node: TreeNode, on: boolean) { const next = new Set(selectedCities) for (const city of node.cities) { // pre-computed leaf set if (on) next.add(city); else next.delete(city) } selectedCities = next api.setFacetFilter('city', next.size === 0 ? null : Array.from(next)) } function isChecked(node: TreeNode): boolean { return node.cities.every((c) => selectedCities.has(c)) } function isPartial(node: TreeNode): boolean { const hits = node.cities.filter((c) => selectedCities.has(c)).length return hits > 0 && hits < node.cities.length } ``` The grid is unaware of the hierarchy - it just receives a flat list of allowed leaf values. The hierarchy lives in your panel UI. See demo 111 ("Tree" card) and [demo 102: Tree checkbox cascade](#/demos/102-tree-checkbox-cascade) for the cascade-logic recipe. ## API surface ```ts type SvGridApi<…> = { // Set a multi-select set filter. Pass null or [] to clear. setFacetFilter(columnId: string, values: ReadonlyArray | null): void // Read the current filters (includes the facet selection per column). getFilters(): Record // Snapshot of the rows the grid currently displays - useful when your // filter UI needs to count matches without re-running the search. getDisplayedRows(): ReadonlyArray } ``` ## See also - Demo 111: [Set filter - tree / async / Excel mode](#/demos/111-set-filter-advanced) - Demo 102: [Tree checkbox cascade](#/demos/102-tree-checkbox-cascade) - the cascade recipe used inside the tree filter - [`api.setFacetFilter`](../api-reference.md#setfacetfilter) - [Filter API overview](./filter-api.md) # Text filter A column with no `editorType` (or `editorType: 'text'`) gets the **text** filter operator set: `contains`, `equals`, `startsWith`, `isBlank`. The default operator is `contains`.
## Through the column menu Click the filter icon in the header → pick an operator → type a value → press Enter. The grid filters as you type (with a 150ms debounce). ## Through the filter row ```svelte ``` Each text column shows a single input. The applied operator is `contains`. ## Programmatically ```ts api.setFilter('firstName', { operator: 'contains', value: 'ada' }) api.clearFilter('firstName') ``` ## Case + accent sensitivity (locale-aware filtering) All built-in text operators are **case AND accent insensitive** out of the box. The grid normalises both the query and each cell value with NFD-decompose → strip combining marks (diacritics) → locale-aware lowercase, then runs `includes` / `equals` / `startsWith`. ```ts applyExcelFilter('Café Genève', { id: 'name', operator: 'contains', value: 'cafe geneve' }) // → true applyExcelFilter('München', { id: 'city', operator: 'startsWith', value: 'munch' }) // → true ```
### `filterLocale` prop For locale-sensitive lowercasing (Turkish dotted-I vs dotless-i, German ß, etc.), thread a BCP-47 tag through `filterLocale`: ```svelte ``` With `filterLocale="tr-TR"`: - `"istanbul"` matches `"İstanbul"` (Turkish capital dotted-I → "i") - `"izmir"` matches `"İzmir"` Without a locale, `String.prototype.toLowerCase()` is used (Unicode default casing). 90 % of the time this is fine; the locale prop is for the edge cases. ### Re-using the normaliser The same `normalizeForFilter` helper that powers the built-in operators is exported, so you can use it in user-land code (e.g. a custom `externalFilter` pipeline): ```ts import { normalizeForFilter } from 'sv-grid-community' const filtered = rows.filter((r) => normalizeForFilter(r.name, 'de-DE').includes( normalizeForFilter(query, 'de-DE'), ), ) ``` ### Opting out If you need case-sensitive comparison, run in `externalFilter` mode and filter the data yourself before passing it in: ```svelte ``` ## See also - Demo 110: [Locale-aware text filter](#/demos/110-locale-aware-filter) - [Filter conditions](./filter-conditions.md) - [Custom column filters](./custom-column-filters.md) - [excel-filters.ts](../../../packages/sv-grid-community/src/filtering/excel-filters.ts) # Glossary Terminology used across the docs and the source. Sorted A-Z. If a term is unclear in a topic page and isn't on this list, please file an issue. ## A **Accessor.** A function on a `ColumnDef` (`accessorFn`) that computes a cell value from the row instead of reading a property by `field`. Used heavily by pivot tables and computed columns. **Active cell.** The cell with focus. Tracked through every keyboard move + click; exposed via `onActiveCellChange`. At most one cell is active per grid. Has `tabindex="0"`; every other cell has `tabindex="-1"` (roving-tabindex pattern). **Aggregator.** A function that reduces a group's values to a single cell value (sum, avg, count, min, max, custom). Used by `columnGroupingFeature` and the pivot engine. **API (`SvGridApi`).** The imperative interface exposed via ``. Methods like `setSort`, `setFilter`, `addRow`, `getDisplayedRows`. See [API reference](./api-reference.md). ## C **Cell context.** The `ctx` object passed to `cell`, `editable`, and `formatter` callbacks. Contains `cell`, `row`, `column`, `table`, `getValue`. Used inside custom cell snippets to access surrounding state. **Column definition (`ColumnDef`).** The plain object that describes one column: how to read its value (`field` / `accessorFn`), how to render it (`cell`, `header`), and which features apply (`editable`, `format`, `editorType`). See [Column definitions](./columns/column-definitions.md). **Column group.** A `ColumnDef` whose `columns` array contains child column defs. The grid emits one header row per nesting depth with proper `colSpan`. See [Column groups](./columns/column-groups.md). **Controlled vs uncontrolled state.** *Controlled*: the consumer owns the state (a `$state` in your component) and listens to change events. *Uncontrolled*: the engine owns the state internally. Most grid state is uncontrollable-by-default; opt in via the `onXxxChange` props. ## D **Density.** The vertical compaction of rows. Controlled via the `--sg-row-height` CSS variable + the `rowHeight` prop. Default is 36 px ("comfortable"); 28 px is "compact"; 48 px is "loose". **Display rows.** The rows the grid is currently showing AFTER the pipeline runs (filter -> sort -> group -> page). Accessible via `api.getDisplayedRows()`. NOT the same as the raw `data` prop. ## E **Editor type.** A string on the `ColumnDef` that picks which built-in editor the grid uses when a user edits the cell. Values: `'text'` / `'number'` / `'date'` / `'datetime'` / `'checkbox'` / `'list'` / `'chips'`. A column without `editorType` is read-only even when `enableInlineEditing` is true. **Engine.** Layer 2 in the [architecture](./architecture.md). The pure-function row-and-column model pipeline. Lives in `packages/sv-grid-community/src/core.ts` + the `row-models/` folder. ## F **Feature.** A bundle of row-model + state + behaviour you register via `tableFeatures({...})`. Examples: `rowSortingFeature`, `columnFilteringFeature`. Features compose - registering five of them is normal. **Field.** The row property a column reads + writes by default. A shortcut for `accessorFn: (row) => row[field]`. When you can use `field`, prefer it - the engine has a fast path for property-keyed columns. **Filter mode.** A single prop on `` that picks which filter UI the grid renders: `'menu'` (icon in each header), `'row'` (input under each header), `'global'` (one search box), or `'none'`. **Format / formatter.** `format` is a declarative config (`{ type: 'currency', currency: 'USD' }`) that the grid hands to `Intl`. `formatter` is a free-form callback that returns a string. Use `format` for standard types; `formatter` for custom output. ## G **Group by.** A list of column ids whose unique values become rollup group rows. Set via `api.setGroupBy([...])` or the column menu's "Group by this column" entry. Drives `columnGroupingFeature`. ## H **Header context.** The `ctx` object passed to `header` and `footer` templates. Contains `header`, `column`, `table`. Used inside custom header snippets. **Headless.** Layer 2 without the renderer. `createSvGrid(...)` returns a headless instance that emits the same row model + state but doesn't paint anything. See [Why headless?](../why-headless.md). ## I **Imperative API.** See *API (`SvGridApi`)*. **Inline editing.** Editing that happens inside the cell itself, vs in a side form. Toggled via `enableInlineEditing` on `` + `editorType` on each column. ## L **Layer 1, 2, 3.** See [Architecture overview](./architecture.md). Layer 1 is your data; Layer 2 is the engine; Layer 3 is the `` renderer. **License key.** A string starting with `SVPRO-` set via `setLicenseKey(...)`. Removes the unlicensed watermark + console nudge. See [API stability](./api-stability.md) for license-related errors. ## P **Pinned column.** A column sticky-positioned to the left or right edge while the rest scroll. Set via the column menu or `api.pinColumn(id, 'left' | 'right' | null)`. **Pipeline.** The chain of row-model transformations: core -> filtered -> sorted -> grouped -> expanded -> paginated. Runs once per state change, NOT per scroll frame. **Pivot.** A reshape of facts into a grid of intersections - row dimensions cross column dimensions, with measures (aggregated values) at each intersection. SvGrid doesn't have a `pivot` prop; the pivot engine in [pivot.md](./pivot.md) builds the pivoted data + nested ColumnDef tree the renderer needs. **Placeholder row.** A frozen "stand-in" row used by sparse infinite scroll. Renders skeleton cells until the real row loads. See [Server-side data](./server-side-data.md). **Pro.** The paid `sv-grid-pro` companion package adding export, import, print, pivot helpers, and the AI assistant. Installed separately; activated by `installPro(api)`. ## R **Render template.** A `cell` / `header` / `footer` value that returns a Svelte snippet via `renderSnippet(SnippetRef, props)`. The grid mounts the snippet inside the cell. **Roving tabindex.** The pattern of giving only the active cell `tabindex="0"` while every other cell has `tabindex="-1"`. Puts the grid in the tab order exactly once. See [Accessibility](./accessibility.md). **Row data.** The `data` prop. Anything assignable to `RowData[]` (which is `Record[]`). Plain objects; the grid never introspects beyond `field` reads. **Row model.** A pipeline stage that takes rows in and emits rows out. Examples: `createSortedRowModel`, `createFilteredRowModel`. Pipe them in via `tableFeatures(...)`. See [Architecture](./architecture.md). ## S **Selection range.** A rectangular cell selection (anchor + focus cells), enabled by `enableCellSelection={true}`. Copy + paste (TSV) and the fill handle both operate on this range. **Snippet.** A Svelte 5 `{#snippet Foo(props)}` block. Used by the grid as the render template format for cells, headers, and editors. `renderSnippet(Foo, props)` is the wrapper the column passes to `cell`. **Soft-gate.** The unlicensed Pro behaviour: features still run; a small watermark + one-time console message appear. Removed by `setLicenseKey('SVPRO-...')`. ## T **Table features.** The bag returned by `tableFeatures({...})` - a typed object identifying which row models + state slices the grid should enable. Pass to `` and use as the first generic of `ColumnDef<...>`. **Tree row.** A row with a `depth` field + `childIds` (or equivalent). Renders with indented chevron + connector lines. The grid doesn't have a `treeData` prop; you derive `visibleRows` from `allRows` + `expanded`. See [Tree rows](./rows/tree-rows.md). ## V **Virtualization.** Rendering only the rows + columns currently in view + a small overscan buffer. Enabled by default for any grid with more than a few hundred rows. Bypassed in jsdom (zero layout metrics) - test against Playwright for virtualization behaviour. **Visible rows.** Same as *Display rows*. ## W **Watermark.** The "Unlicensed sv-grid-pro" badge that appears bottom-right when a Pro feature runs without a valid license. Removed by `setLicenseKey('SVPRO-...')` or `dismissUnlicensedNudge()` (the nudge only). ## See also - [Architecture overview](./architecture.md) - where each piece above sits. - [API reference](./api-reference.md) - every export with its tier badge. # Grouping & aggregation Roll rows up by one or more columns and compute aggregates (sum, avg, count, min, max, custom) at each group level. Powered by `columnGroupingFeature` plus per-column `aggregator` config. Try it: drag a column into the group-by lane, then change aggregators per column:
## Minimal example ```svelte ``` The grid emits one group row per unique department value, with the group cell showing the rolled-up salary sum + average performance. Click the chevron on a group row to expand its children. ## Setting the group-by Three ways, ranked by ergonomic order: 1. **The column menu.** When the user opens a header's menu, "Group by this column" toggles that column in/out of the group-by list. 2. **The `groupBy` prop.** Initial state for the group-by list. 3. **The imperative API.** `api.setGroupBy(['department', 'role'])` for toolbars / saved views. The group order is significant - `['region', 'country']` rolls up country inside region; reverse the array to flip the hierarchy. ## Built-in aggregators | Aggregator | Returns | Behaviour on empty groups | | ---------- | ---------------------------------------------------- | ------------------------- | | `'sum'` | Sum of numeric cell values | `0` | | `'avg'` | Arithmetic mean (with safe divide-by-zero) | `null` | | `'count'` | Number of leaf rows | `0` | | `'min'` | Smallest value (numeric or `Intl.Collator`-comparable) | `null` | | `'max'` | Largest value | `null` | `'sum'` / `'avg'` / `'min'` / `'max'` cast values to `Number`. If the column has non-numeric values mixed in, those rows are skipped. ## Custom aggregator Pass a function instead of a string for any group-aware computation: ```ts { field: 'orders', header: 'Top customer', aggregator: (rows) => { const top = rows.reduce( (acc, r) => !acc || r.orders > acc.orders ? r : acc, null, ) return top?.name ?? '-' }, } ``` The callback gets every leaf row in the group (already filtered). Return whatever the cell should display - string, number, or a formatted value. ## Custom group cell rendering By default the group cell shows `key (n)` - e.g. "Engineering (12)". Override via the column's `cell` template: ```svelte {#snippet GroupCell(props: { row: GroupRow })} {props.row.groupKey} ({props.row.subRows.length} reports) {/snippet} ``` The `row.groupKey` is the unique group value (the department name in the example). `row.subRows` is the children. `row.depth` is the nesting level (useful for indentation when you group by multiple columns). ## Aggregating string columns Strings work with `'count'`, `'min'`, `'max'`, and any custom aggregator. For sum / avg you'll get `NaN` because the cast to `Number` fails - the grid renders this as `-` by default. A useful custom aggregator for strings: ```ts { field: 'tags', aggregator: (rows) => { const set = new Set() for (const r of rows) for (const t of r.tags) set.add(t) return Array.from(set).join(', ') }, } ``` ## Performance Aggregation runs once per group-by change, NOT per scroll frame. The cost is O(n) for `count` / `sum` / `avg`, O(n log n) for `min` / `max` because the engine sorts to find the extreme. For a 100k-row dataset grouped by two columns with three aggregators, the pipeline adds ~36 ms to the initial paint (see [Performance benchmarks](./benchmarks.md)). After that, scroll is unaffected - the renderer hands each visible group its precomputed value. ## Group expansion state `expanded` is owned by the engine by default; you can hoist it for saved-views purposes: ```svelte (controlledExpanded = next)} /> ``` The shape is `Record` where `groupId` is the path through the hierarchy (`'Engineering > Senior'`). ## Group sort vs leaf sort The sort UI sorts within the active sort scope: - When grouping is OFF, sort applies to all rows. - When grouping is ON, sort applies WITHIN each group - groups stay in alphabetical (or group-aggregator) order; only the leaves inside each group reorder. To sort the groups themselves by their rolled-up value, set the sort on the aggregated column AFTER setting the group-by. The grid recognises that the column is aggregated and sorts the group rows instead of the leaves. ## Filtering vs grouping Filters run BEFORE grouping (see [Architecture](./architecture.md) for the pipeline order). The aggregator only sees rows that passed the filter. This is what makes "department salary sum, filtered to active employees only" work without any extra config. ## Pivot vs group-by When the question is "group by row dimensions, also group by column dimensions, also pick aggregators per measure" - that's a pivot. The [pivot helpers](./pivot.md) build a different data structure optimised for that shape. Use group-by when you only roll up rows; use pivot when you also roll up columns. ## See also - [Architecture overview](./architecture.md) - where grouping sits in the pipeline. - [Pivot tables](./pivot.md) - the column-axis version. - [Row pagination](./rows/row-pagination.md) - the paging stage runs AFTER grouping, so group rows count toward the page size. - [Demo #07 Grouping + aggregation](https://svgrid.com/#/demos/07-grouping-aggregation) - the source for the example above. ## Frequently asked questions ### How do I group rows in SvGrid? Register `columnGroupingFeature` and group by one or more columns. Each group renders a collapsible header row, and you attach an `aggregator` per column to compute sum, avg, count, min, max, or a custom reducer at every group level. ### What aggregation functions does SvGrid support? Built-in `sum`, `avg`, `count`, `min`, and `max`, plus custom aggregators - any function that reduces a group's rows to a single value. Aggregates compute at each group level and at the grand-total footer. ### Is grouping the same as a pivot table? No. Grouping rolls rows up along the row axis. A pivot table also spreads a field across the column axis with nested headers - that is the `sv-grid-pro` pivot model. See [Pivot tables](./pivot.md) for the column-axis version. # Group aggregators When grouping is active, each group row can show a rolled-up value per column - the sum of revenue, the average score, the count of deals. SvGrid does this declaratively: set `aggregate` on a column and the group header shows the result, formatted with that column's own `format`. ```svelte ``` ## Built-in reducers | `aggregate` | Result | | ----------------- | --------------------------------------------------- | | `'sum'` | Sum of the finite numeric values. | | `'avg'` | Mean of the finite numeric values. | | `'min'` / `'max'` | Smallest / largest value. | | `'count'` | Number of leaf rows in the group. | | `'countDistinct'` | Number of distinct values. | | `'extent'` | `"min – max"` range string. | | `'first'` | The first leaf row's value (e.g. a shared label). | Non-numeric and empty cells are ignored by the numeric reducers; an all-empty group yields no value (the header chip is hidden). ## Custom aggregators Pass a function for anything the built-ins don't cover - weighted average, median, percentile, distinct-with-rules. It receives the finite numeric values and the raw leaf rows: ```ts const median = (vals: number[]) => vals.length ? [...vals].sort((a, b) => a - b)[Math.floor(vals.length / 2)] : 0 const columns = [ { field: 'score', header: 'Median score', aggregate: median }, // weighted average using two columns off the raw rows: { field: 'rate', header: 'Blended rate', aggregate: (_vals, rows: Row[]) => { const w = rows.reduce((s, r) => s + r.weight, 0) return w ? rows.reduce((s, r) => s + r.rate * r.weight, 0) / w : 0 }, }, ] ``` ## Notes - Aggregates roll up **all leaf rows** under a group, at every nesting level. - The aggregated value is stored on the group row, so `api.getDisplayedRows()` and `row.getCellValueByColumnId(id)` return it too - handy for exporting group totals. - The reducer is exported as `applyGroupAggregate(agg, columnId, rows)` for reuse outside the grid. See the live [Group aggregators](https://sv-grid.com/demos/142-group-aggregators) demo. # Internationalisation & RTL The grid is locale-agnostic by design: every text label is yours to translate, every formatted value goes through `Intl`, and right-to-left layout flips via standard CSS logical properties. Try the live RTL + i18n demo - switch between six locales (en, de, fr-CA, ja, ar, he); the headers, dates, currencies, and the grid's own scrollbar position all flip:
## What ships translated **Nothing.** The grid has no built-in strings to translate. Every visible string in the grid comes from one of three places: 1. Your column `header` strings. 2. Your cell snippet content. 3. The `Intl.NumberFormat` / `Intl.DateTimeFormat` output the grid uses for `format: { type: 'number' | 'currency' | 'percent' | 'date' }`. If you see an English string in the grid that isn't covered by one of the three, it's a bug - please file it. (The unlicensed Pro watermark and the console nudge are the only literal strings the package itself emits, and both are off when a license key is set.) ## Number, currency, date formatting The built-in `format` config calls `Intl` under the hood. Locale is inherited from `` by default; override per-column: ```ts const columns: ColumnDef[] = [ // Inherits the document locale (most apps want this). { field: 'total', header: 'Total', format: { type: 'currency', currency: 'USD' } }, // Pin to a specific locale - useful for tables of money that should // ALWAYS read as USD-grouped regardless of viewer. { field: 'usdTotal', header: 'Total (USD)', format: { type: 'currency', currency: 'USD', locales: 'en-US' } }, // Custom Intl options. { field: 'createdAt', header: 'Created', format: { type: 'date', pattern: 'long', options: { dateStyle: 'medium', timeStyle: 'short' }, }, }, ] ``` `locales` accepts the same string or array the `Intl` constructors do. For mixed-locale columns inside a single grid, pass an explicit `locales`. ## Translating headers and cell content Use whatever i18n library your app already runs. The grid's only expectation is "a string, or a render template that returns one": ```svelte ``` Because `header` is a string (or a snippet), the column definition re-evaluates whenever your translation store updates - no extra re-mount required. For dynamically-translated cells, use `renderSnippet`: ```svelte {#snippet StatusCell(props: { row: Order })} {$t(`order.status.${props.row.status}`)} {/snippet} const columns = [ { field: 'status', header: $t('grid.status'), cell: (ctx) => renderSnippet(StatusCell, { row: ctx.row.original }) }, ] ``` ## Right-to-left (RTL) RTL is a single attribute. Set `dir="rtl"` on the `` element (or on the grid's container) and the layout flips: ```svelte ``` The grid uses logical properties (`inset-inline-start`, `inset-inline-end`, `padding-inline-start`, etc.) for every position that depends on direction, including: - The vertical scrollbar (slides to the left in RTL). - Column pinning sticky positions. - The filter row inputs. - The fill handle on the active cell. - Sort indicator + filter menu button alignment in headers. No prop flips; no JS branch. If your layout doesn't flip, look at your own CSS first - explicit `left` / `right` in your styles will override the grid's logical-property choices. ### Mixed-direction text Numbers, currency, and dates inside RTL cells often need an `` wrap so the writing direction doesn't get scrambled by the surrounding RTL context: ```svelte {#snippet AmountCell(props: { row: Order })} {fmtMoney(props.row.amount)} {/snippet} ``` The built-in `format` configs already wrap their output - this only matters for your custom snippets. ## Pluralisation + cardinal rules `Intl.PluralRules` covers the cases ICU MessageFormat usually handles. The grid never pluralises on your behalf - your i18n library should. A common pattern in the gallery demos: ```ts const status = api.getDisplayedRows().length === 1 ? '1 result' : `${api.getDisplayedRows().length} results` ``` For more languages than "English", use a real plural-rules library: ```ts const pr = new Intl.PluralRules(locale) const key = pr.select(count) // 'one' | 'other' | ... const message = i18n[locale][`result_${key}`] ``` ## Column ordering by locale A column called "Cancel" in English might be "Annuler" in French - or "Stornieren" in German. If users want to find "Cancel" first when sorting columns alphabetically, sort the column list with `Intl.Collator`: ```ts const collator = new Intl.Collator(locale) const sorted = [...columns].sort((a, b) => collator.compare(String(a.header ?? a.id), String(b.header ?? b.id)), ) ``` The grid never reorders columns on its own (except through `api.addColumn(..., position)`), so locale-aware sorting is your call. ## CJK column widths CJK glyphs (Chinese, Japanese, Korean) are roughly double the width of Latin characters at the same point size. If your default `columnWidth` was tuned for English, CJK columns will look cramped. Two options: 1. **Set wider defaults when a CJK locale is active.** A `columnWidth` of 180 reads ~10 characters in CJK, vs ~20 in Latin. ```ts const isCJK = ['ja', 'zh', 'ko'].some((l) => locale.startsWith(l)) const columns: ColumnDef[] = baseColumns.map((c) => ({ ...c, width: c.width ? c.width * (isCJK ? 1.3 : 1) : undefined, })) ``` 2. **Let the user resize.** All grids ship with column resize handles; if you let the user save their layout (see [Saved views](./saved-views.md)) you only need to get the default close, not perfect. ## Date / time picker editors The `editorType: 'date'` editor uses the browser's native ``. Locale + first-day-of-week + 24h vs 12h come from the browser. If you need locked-down behaviour across locales, swap in a custom editor via [Custom header components](./columns/custom-header-components.md) + a custom cell snippet for the editing path. ## A11y interaction with i18n - Update `` whenever the locale changes. Screen readers pick the right voice from this attribute. - Update `` whenever the script flips. - Keep `aria-label`s in the user's locale; the grid never overrides yours. ## See also - [Tailwind integration](./tailwind.md) - the `--sg-*` tokens and how they interact with `dir="rtl"`. - [Accessibility](./accessibility.md) - all of the above respects forced-colors / reduced-motion / screen-reader announcements per locale. - [Demo #38 RTL + i18n](https://svgrid.com/#/demos/38-rtl-i18n) - full locale + direction toggle reference. ## Frequently asked questions ### Does SvGrid support right-to-left (RTL) languages? Yes. Layout flips via standard CSS logical properties, so setting `dir="rtl"` on a container reverses the grid - columns, scrolling, and pinning included - with no special configuration. ### How do I localize the grid? The grid is locale-agnostic: every text label is yours to translate, and every formatted value (number, currency, percent, date) goes through `Intl` with the locale you pass. Wire your own i18n strings into headers and custom cells. ### Does SvGrid format numbers and dates per locale? Yes, through cached `Intl` formatters. Set a column's `format` and the locale, and values render with the correct separators, currency symbols, and date patterns automatically. # Data import - Pro The sister to [data export and printing](./export.md). Read an Excel file, CSV/TSV blob, or JSON array in the browser and produce a typed preview of every parsed row - including per-cell validation errors - before any data lands in the grid. Ships in the paid **[sv-grid-pro](https://www.npmjs.com/package/sv-grid-pro)** add-on. Click **Preview from text** below to run the bundled sample through the parser + validator, then **Commit** to push the clean rows into the grid:
## What it is `installPro(api)` adds one async method to your `SvGridApi`: ```ts api.importData(opts): Promise> ``` The call: 1. Parses the file or text into a typed row set. 2. Maps source columns to your grid's fields via an optional `columnMap`. 3. Runs each row through your validator (if you give one). 4. Either **returns the result for you to preview**, or **commits the rows into the grid** via `api.addRows(...)` when `commit: true`. The grid never tries to be a full Excel reader; it handles the shape Excel, Numbers, Google Sheets, and Apache POI produce by default. For exotic features (pivot tables embedded in the file, multi-sheet workbooks, conditional formatting), pre-process server-side and feed the result through this API. ## When to use it - **Onboarding flows** where customers upload a spreadsheet to seed the app. - **Bulk edit** workflows where the user downloads a CSV via `api.exportData(...)`, edits in Excel, and re-uploads. - **Pipeline integrations** where another tool dumps an xlsx and your app surfaces it for review. If you control the file format end-to-end and just need server -> grid data, skip the importer and call `api.addRows(...)` directly with the parsed rows. The importer's value is in the **review UX** - column-mapping, validation, error preview - not the parser itself. ## Minimal example ```svelte (api = installPro(next))} /> ``` ## Supported formats | Format | Source type | Peer dependency | Notes | | ------ | ---------------------- | --------------- | ------------------------------------------------------------------ | | `xlsx` | `File` / `Blob` | `jszip` | First sheet only. Strings, numbers, booleans, dates as ISO strings. | | `csv` | `File` / `Blob` / `string` | none | RFC 4180-ish: quoted fields, embedded newlines, escaped quotes. | | `tsv` | `File` / `Blob` / `string` | none | Same as CSV with `\t` as separator. | | `json` | `File` / `Blob` / `string` | none | Top-level array of objects. Column union taken across first ~50 rows. | | `auto` | any | maybe `jszip` | Format is sniffed from file extension or first character of text. | The xlsx parser shares the `jszip` peer dependency with xlsx export, so if you already export to Excel you don't add a second peer for import. ## Column mapping Pass a `columnMap` from **source header** to **target field**: ```ts await api.importData({ file, columnMap: { 'Order #': 'orderId', // rename 'Customer Name': 'customer', 'Customer Email': 'email', 'Internal Note': null, // drop this column entirely }, }) ``` Source headers not listed in `columnMap` fall through to a default mapping: lowercase + collapse whitespace to underscores + strip non-alphanumerics. So `"Order ID"` becomes `order_id`. If that's not what you want, list it explicitly. Set a column's map entry to `null` to drop it from the parsed rows entirely - useful for stripping PII you don't want to land in the client-side grid. ## Type coercion The parser walks every cell value through a small set of regex-based heuristics: | Source value | Becomes | | ---------------------- | ------------ | | `true` / `false` | boolean | | `123`, `-45.6`, `1e3` | number | | `$1,234.56` | `1234.56` | | `"1,234,567"` | `1234567` | | `2024-03-15` | string (ISO date) | | `2024-03-15T12:30:00Z` | string (ISO datetime) | | empty cell | `''` | The grid columns then apply their own format / parsing on top. If you want fully strict types, run them through your `validator` and reject anything that didn't coerce the way you expected. ## Validation Validators receive each parsed row plus its index. Return an array of `{ field, message }` errors: ```ts function validator(row: Order, rowIndex: number) { const errs = [] if (row.total < 0) errs.push({ field: 'total', message: 'must be >= 0' }) if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(row.email)) errs.push({ field: 'email', message: 'invalid email' }) return errs } ``` The errors land in `result.errors` with `rowIndex` (0-based in the output, *excluding* the header row) and `field` so your preview UX can highlight the offending cell. ## Preview vs commit The default is **preview**: you get `{ headers, rows, errors, skipped, total, format }` back and decide what to do. Pass `commit: true` to skip the preview and append the rows directly, with an optional `commitAt` ('top' | 'bottom' | numeric index). If there are any validator errors, `commit: true` **silently refuses** to write - your UI should always render `result.errors` regardless. ```ts const r = await api.importData({ file, commit: true, commitAt: 'top' }) if (r.errors.length > 0) { // The commit was skipped. Re-render the import dialog with errors. } ``` ## Result shape ```ts type ImportResult = { headers: string[] // source headers verbatim rows: TData[] // parsed, mapped, type-coerced rows errors: Array<{ rowIndex: number; field: string; message: string }> skipped: number // rows skipped because they were entirely blank total: number // total source rows (incl. blanks + bad rows) format: 'xlsx' | 'csv' | 'tsv' | 'json' } ``` ## Performance The browser-side parser is O(file size). It walks the bytes once, no regex backtracking, and pays one `JSON.parse` for JSON imports. For files up to ~100k rows the parse + validate cycle stays under a few hundred milliseconds on a typical laptop. For larger files (>500k rows) we recommend a server-side ingest: upload the file, stream it through your parser, and emit the result back via the same `addRows` call. The importer's review UX still works - just call it on a sample slice first. ## Gotchas - **First sheet only.** xlsx imports return rows from `sheet1.xml`. Pick the right sheet server-side or convert the workbook before upload. - **No formulas.** Cached formula values are read when present, but the parser doesn't evaluate uncached formulas. - **No styles, comments, conditional formatting.** Just values. - **Blank rows are skipped.** A row whose every cell is empty is counted in `skipped`, not `rows`. ## See also - [Data export and printing](./export.md) - the round-trip partner. - [Validation while editing](./editing/validation.md) - the same validator shape works for inline grid edits. - [Demo 53 - Excel / CSV import](../../examples/src/demos/53-excel-import.svelte) - the demo this page documents. ## Frequently asked questions ### How do I import an Excel or CSV file into the grid? With `sv-grid-pro`, read an xlsx file, CSV/TSV blob, or JSON array in the browser and get a typed preview of every parsed row - including per-cell validation errors - before any data lands in the grid. Nothing is uploaded; parsing happens client-side. ### Does import validate the data? Yes. Each parsed row runs through the same validator shape used for inline editing, so you can surface per-cell errors in the preview and let the user fix them before committing. ### Is import free? No. Import ships in `sv-grid-pro`, alongside export and pivot. The free Community package handles displaying and editing data you already have in memory. # SvGrid Help Topic-oriented documentation for SvGrid. Each page is a focused explanation of one feature with copy-paste code that runs against the shipping library - written for SvGrid, not translated from another grid. Start with [Getting Started](../getting-started.md) if you have not already. > **Tier badges.** Pages whose title ends with `- Pro` describe a > feature that ships in the paid `sv-grid-pro` add-on. Everything else > is part of the open-source `sv-grid-community` package. The same > visual convention is used throughout this documentation. Live reference - the trading-desk demo runs the full feature set at real-world scale:
## Background - [Why headless?](../why-headless.md) - what the headless core gives you and when to reach for it - [Architecture overview](./architecture.md) - the three-layer model: your data, the engine, the renderer - [Glossary](./glossary.md) - terminology used across the docs (accessor, snippet, row model, ...) - [Tailwind integration](./tailwind.md) - re-theming the grid via `--sg-*` tokens, dark-mode wiring, what *not* to do - [**Pro feature pack**](../pro/README.md) - landing page for the paid add-on; what's in it + how to license it - [Data export and printing - Pro](./export.md) - Excel, PDF, CSV, TSV, HTML, and Print - [Data import - Pro](./import.md) - Excel, CSV, TSV, and JSON import with column mapping + validation - [AI assistant - Pro](./ai.md) - natural-language filter, smart fill, summarise, classify; bring-your-own model adapter - [Pivot tables - Pro](./pivot.md) - `createPivotModel` + nested column headers; designer UI is a separate demo - **Migrating to SvGrid** - column / API translation guides from other grids: [AG Grid](./migrating-from-ag-grid.md) · [TanStack Table](./migrating-from-tanstack-table.md) · [MUI X DataGrid](./migrating-from-mui-x.md) · [Handsontable](./migrating-from-handsontable.md) · [Glide Data Grid](./migrating-from-glide.md) · [svelte-headless-table](./migrating-from-svelte-headless-table.md) · [SVAR Svelte DataGrid](./migrating-from-svar-svelte-datagrid.md) · [@vincjo/datatables](./migrating-from-vincjo-datatables.md) · [Flowbite / Skeleton / shadcn tables](./migrating-from-ui-kit-tables.md) · [Tabulator](./migrating-from-tabulator.md) · [Grid.js](./migrating-from-gridjs.md) · [React Data Grid](./migrating-from-react-data-grid.md) · [PrimeVue / PrimeNG / PrimeReact](./migrating-from-primevue-datatable.md) · [Kendo UI Grid](./migrating-from-kendo-grid.md) · [DevExtreme](./migrating-from-devextreme.md) · [Syncfusion](./migrating-from-syncfusion.md) · [jqxGrid](./migrating-from-jqxgrid.md) · [Smart.Grid](./migrating-from-smart-grid.md) ## Patterns & playbooks - [Recipes / Cookbook](./recipes.md) - 20+ copy-paste patterns from sort/filter/paginate to inline edit + cascading totals to streaming, each paired with a live interactive demo - [AI Smart Paste](./ai-smart-paste.md) - vCard / Markdown / signature-block / CSV parsing with email typo correction, phone normalization, and multi-language headers - [Spreadsheet formulas](./spreadsheet-formulas.md) - in-cell `=SUM` / `=IF` / `=COUNTIF` with cell refs, ranges, cycle detection - [Mobile / responsive card view](./mobile-card-view.md) - the same `$state` array driving a desktop grid and a touch-friendly card list - [Conditional form schema](./conditional-form-schema.md) - declarative `when` rules for per-cell visibility and editability - [Server-side data](./server-side-data.md) - paginated fetch, server-driven sort + filter, sparse infinite scroll (with velocity-aware chunk loading + abort guards) - [Real-time / streaming updates](./real-time.md) - poll vs WebSocket, delta merge, pause-while-editing, backpressure - [Grouping & aggregation](./grouping-aggregation.md) - built-in aggregators, custom group cells, group-vs-leaf sort, performance notes - [Columns hierarchy + manager](./columns-hierarchy.md) - side-panel column tree with drag-to-reorder, collapsible groups, summary columns - [State maintenance](./state-maintenance.md) - capture / apply, undo / redo, bookmarks, JSON IO, debounced auto-save - [Saved views & persistence](./saved-views.md) - localStorage / server-side persistence, view migration, URL sharing - [Internationalisation & RTL](./i18n-rtl.md) - locale-aware formatting, RTL layout flip, CJK column widths, mixed-direction safety ## Enterprise readiness - [Security & supply chain](./security.md) - peer-dep table, CSP guidance, SBOM generation, vulnerability handling, data residency - [Browser & runtime support](./browser-support.md) - tested browsers + versions, SSR runtimes, build tools, DOM-API requirements, mobile - [Accessibility](./accessibility.md) - WAI-ARIA 1.2 grid pattern, WCAG 2.1 AA mapping, keyboard map, forced-colors + reduced-motion - [Performance benchmarks](./benchmarks.md) - first paint, sustained scroll FPS, sort / filter / group, memory, bundle size, on a documented machine - [Testing your grid](./testing.md) - unit tests against the engine, jsdom component tests, Playwright e2e, axe-core for a11y regressions - [API stability & semver policy](./api-stability.md) - the promise we make to you about breaking changes + deprecation lifecycle - [**API reference**](../reference/index.md) - exhaustive prop/method/type tables for ``, `SvGridApi`, `ColumnDef`, features, and the Pro surface - [API stability badges](./api-reference.md) - flat index of every Stable export with its tier badge - [Changelog](../changelog.md) - reverse-chronological log of every shipped change - [Error reference](./errors.md) - every typed error this surface throws, with the trigger and the fix ## Production checklist A focused walkthrough for the questions enterprise teams ask before shipping. Each link drops you straight into the relevant topic page. | Concern | Reach for | | -------------------------------- | -------------------------------------------------------------------------------------------------- | | Performance with >10k rows | [Performance benchmarks](./benchmarks.md) + [Row pagination](./rows/row-pagination.md) | | Server-side data | [Server-side data](./server-side-data.md) - paginated fetch, server-driven sort, sparse infinite scroll | | Real-time / streaming updates | [Real-time / streaming](./real-time.md) - delta merge, pause-while-editing, backpressure | | Tree / hierarchical data | [Tree rows](./rows/tree-rows.md) - flat-array + expanded-map pattern, lazy load, keyboard nav | | Pivot / multi-level headers | [Pivot tables](./pivot.md) + [Column groups](./columns/column-groups.md) | | Grouping + aggregation | [Grouping & aggregation](./grouping-aggregation.md) - built-in aggregators + custom group cells | | Inline editing with validation | [Editing overview](./editing/overview.md) + [Validation while editing](./editing/validation.md) | | Theming + dark mode | [Tailwind integration](./tailwind.md) - the `--sg-*` token list | | Accessibility / WAI-ARIA | [Accessibility](./accessibility.md) - WCAG 2.1 AA mapping + keyboard map + forced-colors | | Internationalisation / RTL | [Internationalisation & RTL](./i18n-rtl.md) - locales, formatting, mixed-direction safety | | Saved views / layout persistence | [Saved views](./saved-views.md) - localStorage / server-side persistence + migration | | Export to Excel / PDF / CSV | [Data export and printing](./export.md) | | Excel / CSV / JSON import | [Data import](./import.md) - file picker -> column map -> preview -> commit | | Add AI to the grid | [AI assistant](./ai.md) - one provider adapter, four helpers | | Multi-app deployment licensing | [Pricing](https://svgrid.com/#/pricing) - Multiple App License covers an org | | Testing the grid | [Testing your grid](./testing.md) + [Testing and quality](./testing-and-quality.md) | If a question is missing from this table, **press `Ctrl/Cmd + K`** in the docs sidebar - the search box indexes every page's title, headings, and body and ranks matches by where they hit. ## Core features ### Columns - [Column definitions](./columns/column-definitions.md) - [Updating definitions](./columns/updating-definitions.md) - [Column state](./columns/column-state.md) - [Column headers - styling & height](./columns/column-headers.md) - [Column groups](./columns/column-groups.md) - [Column sizing](./columns/column-sizing.md) - [Column moving](./columns/column-moving.md) - [Column pinning](./columns/column-pinning.md) - [Column spanning](./columns/column-spanning.md) - [Custom header components](./columns/custom-header-components.md) ### Rows - [Row data](./rows/row-data.md) - [Row sorting](./rows/row-sorting.md) - [Row spanning](./rows/row-spanning.md) - [Row pinning](./rows/row-pinning.md) - [Row height](./rows/row-height.md) - [Styling rows](./rows/styling-rows.md) - [Row pagination](./rows/row-pagination.md) - [Accessing rows](./rows/accessing-rows.md) - [Row dragging](./rows/row-dragging.md) - [Full-width rows](./rows/full-width-rows.md) - [Tree rows (expand / collapse)](./rows/tree-rows.md) ### Cells - [Getting values](./cells/getting-values.md) - [Text formatting](./cells/text-formatting.md) - [Cell components](./cells/cell-components.md) - [Cell data types](./cells/cell-data-types.md) - [Styling cells](./cells/styling-cells.md) - [Highlighting changes](./cells/highlighting-changes.md) - [Tooltips](./cells/tooltips.md) - [Expressions](./cells/expressions.md) - [View refresh](./cells/view-refresh.md) - [Cell text selection](./cells/cell-text-selection.md) ### Filtering - [Overview](./filtering/overview.md) - [Text filter](./filtering/text-filter.md) - [Number filter](./filtering/number-filter.md) - [Date filter](./filtering/date-filter.md) - [Set filter](./filtering/set-filter.md) - [Filter conditions](./filtering/filter-conditions.md) - [Applying filters](./filtering/applying-filters.md) - [Filter API](./filtering/filter-api.md) - [Custom column filters](./filtering/custom-column-filters.md) - [Floating filters](./filtering/floating-filters.md) ### Editing - [Overview](./editing/overview.md) - [Start / stop editing](./editing/start-stop-editing.md) - [Parsing values](./editing/parsing-values.md) - [Saving values](./editing/saving-values.md) - [Edit components](./editing/edit-components.md) - [Provided cell editors](./editing/provided-editors.md) - [Undo / redo](./editing/undo-redo.md) - [Full-row editing](./editing/full-row.md) - [Validation](./editing/validation.md) ## Conventions Each topic page is structured as: 1. **What it is** - a one-sentence definition. 2. **When to use it** - the situation that calls for this feature. 3. **Minimal example** - copy-pasteable code that runs. 4. **Reference** - the relevant exports and prop names. 5. **Gotchas** - known limits, gaps, or things that surprise people. Pages explicitly note when a feature is **not yet implemented** in the community build so you know what you can rely on. The current gap list is at [missing-features.md](./missing-features.md). # Use sv-grid docs as LLM context This page is the "how do I make ChatGPT / Claude / Cursor write good sv-grid code?" guide. Three pre-built artefacts ship with the docs specifically so models can ground themselves in current, accurate information instead of hallucinating from training data. ## The four files | File | Format | Size | Use for | | ---------------------------------------- | ---------- | ------ | ---------------------------------------------------------------------- | | [`/llms.txt`](/llms.txt) | Plain text | ~10 kB | First-pass context: the topic map with one-line summaries | | [`/llms-full.txt`](/llms-full.txt) | Plain text | ~700 kB | Deep grounding: every doc page concatenated | | [`/docs.json`](/docs.json) | JSON | ~80 kB | Programmatic crawling: section tree, per-page metadata, demo links | | [`/schemas/index.json`](/schemas/index.json) | JSON | ~30 kB | Validation: machine-checkable shape of `ColumnDef`, `` props, export options | All four are regenerated on every commit by `tools/build-docs-index.mjs` and `tools/build-schemas.mjs`. They live at the docs origin (`https://svgrid.com/...`) so you can fetch them at runtime. ## Recipe 1: Drop into a custom GPT / Claude project The simplest way. Both ChatGPT (custom GPTs) and Claude (projects) let you upload reference files that ride along with every chat. 1. Save [`/llms-full.txt`](/llms-full.txt) locally. 2. In ChatGPT: *Create custom GPT → Configure → Knowledge → Upload files*. 3. In Claude: *Project → Project knowledge → Add document*. 4. Add this system instruction: ``` You are a sv-grid expert. Ground every answer in the attached llms-full.txt. If a question references an API not in the document, say so and ask the user to upgrade rather than inventing one. Prefer the smallest working example. When showing columns, follow the column-def.json schema exactly. ``` 5. (Optional) Upload `column-def.json` and `svgrid-options.json` alongside so the model can self-check generated config. That's it. The next time you ask "how do I export only selected rows to xlsx?" the model answers from the doc text, not from its year-old training cutoff. ## Recipe 2: Cursor / Continue / Cody rules file Most IDE assistants honour a `.cursorrules` / `.continuerules` / `.aider.conf.yml` file in the repo root. Drop in: ``` # .cursorrules When generating sv-grid code: - Read context from https://svgrid.com/llms.txt before answering. - For column definitions, generate against https://svgrid.com/schemas/column-def.json (Draft 2020-12 JSON Schema). - Use Svelte 5 runes ($state, $derived, $effect) - never legacy stores. - Use `editorType: 'list'` with `editorOptions` for dropdowns, not raw
`. - **Familiar editing.** Browser-native `` / `