Table
Svelte Lean Table is a table engine for Svelte 5 that stays a table for ordinary tables and grows into a data grid only when the application asks for it. It renders a native table, keeps its state in one plain object, and adds capabilities through separate entry points.
- Fixture
- Benchmark
On this page
At a glance
Thirty-six sample rows in the default renderer: search, selection, column controls and paging are props. Sort a column, select rows, hide a column.
Amara Okafor | Active | Engineering | Europe | $4,501 | 2026-01-07 | |
Jonas Lindqvist | Active | Design | Americas | $12,420 | 2026-06-18 | |
Mei Tanaka | Away | Operations | Asia | $20,339 | 2026-02-01 | |
Rafael Duarte | Invited | Growth | Europe | $28,258 | 2026-07-12 | |
Selin Aydın | Active | Engineering | Americas | $36,177 | 2026-03-23 | |
Noah Brenner | Active | Design | Asia | $44,096 | 2026-08-06 |
What Svelte Lean Table is
Most tables in an application need identity, sorting, a search box and a pager. Some need selection and expansion. A few need editing, grouping, ranges and clipboard, or ten thousand rows. Svelte Lean Table serves the first group without carrying the last: the renderer contains the semantic table and the interactions that fit in props, and everything larger is a module that enters a build only when it is imported.
Supporting a feature should not mean shipping the feature. The table-basic consumer fixture builds a page with DataTable and its
stylesheet in production mode and asserts, in the module graph, that the grouping, pivot,
clipboard, export, server and state modules are absent. The Svelte Lean chunk of that build is 16829 B brotli (18900 B gzip), with
Svelte's own runtime recorded separately.
The output is a <table> with the elements and attributes assistive technology
expects; no cell is a component instance unless the application supplies a snippet; state is
serializable and controlled from the outside; every string is a message; server rendering is
tested. The engine underneath, createTable(), is exported for renderers of your
own.
<script lang="ts">
import { DataTable, type Column } from '@svelte-lean/table';
import '@svelte-lean/table/style.css';
type Member = { id: number; name: string; revenue: number };
const data: Member[] = [
{ id: 1, name: 'Amara Okafor', revenue: 1200 },
{ id: 2, name: 'Jonas Lindqvist', revenue: 9119 }
];
const columns: Column<Member>[] = [
{ id: 'name', header: 'Member' },
{ id: 'revenue', header: 'Revenue', align: 'end' }
];
</script>
<DataTable {data} {columns} getRowId={(row) => row.id} />Capability levels
One product, four levels. A table does not pay for a level it does not use, and the levels are props and imports, not different components.
| Level | Adds | How | Status |
|---|---|---|---|
| Table | A native <table> with caption, colgroup, thead, tbody, tfoot, scope and aria-sort; snippets for cells; SSR. | <DataTable /> with the three required props | shipped |
| DataTable | Sorting, search and filters, selection, pagination, expansion and tree rows, pinning, resizing, visibility and order, sticky header, states, messages. | Props; state in one object | shipped |
| Virtual Table | A windowed body for very large row counts. | A future opt-in (@svelte-lean/virtual), extracted only when a reusable need is proved | not shipped |
| Data Grid | Cell focus and arrow-key navigation, inline editing, range selection, copy and paste, grouping and pivot, persisted state, server data sources. | Props that switch the keyboard model on, plus separate entry points | shipped, rows not virtualized |
Build profiles
Two profiles, read from measurement files. The first is a real consumer build; the second is what a grid adds, per entry, because no fixture builds that combination yet.
Basic DataTable
fixtures/table-basic: DataTable and style.css, Vite
7.3.6, Svelte 5.57.1, production build.
- Svelte Lean chunk
- 16829 B brotli · 18900 B gzip
- Svelte runtime chunk
- 15916 B brotli
- Rendered modules
- core.js, table.svelte.js, messages.js, editing.js, table/navigation.js, table/data-table.svelte
- Not included
- grouping.js, pivot.js, clipboard.js, export.js, server.js, state.js
- Assertions
- 46 passed
Data Grid
The renderer plus the entries a grid imports, each measured on its own with esbuild; a consumer bundler de-duplicates the shared code, so the entries are not summed here.
renderer- 17886 B brotli · 20031 B gzip
editing- 617 B brotli · 707 B gzip
clipboard- 817 B brotli · 895 B gzip
grouping- 1963 B brotli · 2148 B gzip
state- 711 B brotli · 796 B gzip
- Not included
- virtual rows (no such module exists)
Entry points and measured sizes
Every entry of @svelte-lean/table as measured by yarn size into packages/table/artifacts/size.json. Budgets are enforced in CI and sit above the measured value.
| Entry | File | Contains | gzip | brotli | Budget (gzip) |
|---|---|---|---|---|---|
| renderer | dist/index.js | the root entry: <DataTable />, createTable(), helpers, messages | 20031 B | 17886 B | 22000 B |
| headless | dist/core.js | the ./core entry: pure row model functions, no Svelte | 2475 B | 2280 B | 2700 B |
| engine | dist/table.svelte.js | createTable() on its own (runes) | 3494 B | 3204 B | 3600 B |
| grouping | dist/grouping.js | groupBy(), summarize(), aggregate() | 2148 B | 1963 B | 2400 B |
| pivot | dist/pivot.js | pivot() | 526 B | 470 B | 600 B |
| editing | dist/editing.js | applyEdit(), setValue(), parseEdit(), createHistory() | 707 B | 617 B | 800 B |
| csv | dist/export.js | toCsv(), parseDelimited(), downloadText() | 741 B | 646 B | 850 B |
| clipboard | dist/clipboard.js | copyRange(), pasteRange(), rangeBounds() | 895 B | 817 B | 1000 B |
| server | dist/server.js | createDataSource() | 369 B | 310 B | 425 B |
| state | dist/state.js | serializeState(), restoreState() | 796 B | 711 B | 900 B |
| styles | dist/style.css | style.css | 6665 B | 5704 B | 7300 B |
Each entry bundled independently with esbuild (bundle + minify, ESM); shared code is counted per entry here and de-duplicated by consumer bundlers. Svelte peer runtime excluded. gzip level 9 / brotli quality 11 (node:zlib). Budgets are gzip.
Timings
Rows are not virtualized, so a large table costs what its DOM costs. These are the medians the playground benchmark recorded with every row in the document; they are read from the benchmark file with the environment of that run and compare the table with itself, never with another library. Without a committed run the cells read "not measured".
| Subject | Median | p95 | Method |
|---|---|---|---|
| Mount, one thousand rows | 283.8 ms | 342.8 ms | component init to the second animation frame after onMount, client render of the whole table (ssr=false); median of 5 loads |
| Sort by revenue, one thousand rows | 114.1 ms | 117.7 ms | header button click (ascending by revenue) to the second animation frame; median of 5 |
| Mount, ten thousand rows | 3907.1 ms | 5984.2 ms | component init to the second animation frame after onMount, client render of the whole table (ssr=false); median of 5 loads |
| Sort by revenue, ten thousand rows | 1928.4 ms | 2142.4 ms | header button click (ascending by revenue) to the second animation frame; median of 5 |
Environment: chromium 153.0.8010.53, Apple M2, darwin 27.0.0, Node v22.23.3, production build, run 2026-09-27. Method and reproduction on Methodology.
Specimens
The 31 examples of the previous site, in their five
groups, each living on the page of its feature. Every one is a live DataTable with its
Svelte source.
Essentials
- Basic table — The smallest useful table: typed data, typed columns and stable row identity.
- Custom cells — Render product-specific content with typed Svelte snippets while the table keeps layout and semantics.
- Sorting — Single and multi-column sorting with visible priority and controlled state.
- Filtering and search — Combine global search with per-column filters and custom operators.
- Pagination — Client or server pagination with controlled page index and page size.
- Row selection — Single or multiple selection, disabled records and page or filtered select-all scope.
Layout
- Density and borders — Switch density, stripes and cell borders without changing the row model.
- Fixed header — Keep headers visible inside a constrained scrolling surface.
- Fixed columns — Pin identity and action columns while wide records scroll horizontally.
- Grouped headers — Describe multi-level headers with nested column definitions.
- Column visibility — Let users hide fields while protecting required columns.
- Resize and reorder — Resize with pointer or keyboard and persist an explicit column order.
- Ellipsis — Constrain long values with optional native title disclosure.
- Cell spans — Merge adjacent cells with row and column span callbacks.
- Right-to-left — Mirror pinned columns, navigation and resize behavior with one direction prop.
Rows
- Expandable rows — Reveal one details region per record and control which rows may expand.
- Tree data — Render nested records through getSubRows with configurable indentation.
- Row grouping — Derive group rows and aggregates with the optional grouping entry point.
- Row reordering — Move rows through a controlled callback without hidden data mutation.
- Row events — Handle click, double-click, context menu and declarative cell actions.
- Row animation — Animate application-owned row insertion and removal while respecting reduced motion.
Workflows
- Editing and history — Validate cell edits, persist them in the application and add undo only when needed.
- Range and clipboard — Select a rectangular range and translate it to or from tab-delimited spreadsheet data.
- CSV export — Export visible data with correct quoting and spreadsheet-formula protection.
- Server data — Control loading, errors, row counts and stale requests without coupling the table to a transport.
- Persisted state — Serialize presentation state and safely restore only columns that still exist.
- Loading and empty — Make waiting, retrying and zero-result states part of the component contract.
- Localization — Replace every built-in message and switch layout direction independently.
Analysis
- Summaries — Render totals aligned with visible and pinned columns.
- Pivot — Turn long-form records into a compact cross-tab using an optional pure helper.
- Performance — Repeatable size and browser measurements with the limits stated beside every number.
Compared with a grid
A grid library starts from the grid: virtualized rows, a cell model, an editing engine, and a keyboard runtime that every table pays for. Svelte Lean Table starts from the table and treats the grid as a level the application reaches by adding props and imports, with the semantics of a native table kept at every level. Where a project needs virtualized rows today, this package does not provide them and says so.
Below the component
createTable() is the engine the renderer is built on: it derives the row model, the
visible columns and the header rows from getters over your state and exposes every transition. @svelte-lean/table/core is the pure model without Svelte, for tests and servers. Both are documented under Engine in the API reference.