# API reference

> Exports, DataTable props, Column options, table state, events, messages and modules on one anchored page.

Page: https://svelte-lean.vercel.app/table/api · Markdown: https://svelte-lean.vercel.app/table/api.md · Index: https://svelte-lean.vercel.app/llms.txt

One page, anchored: exports, DataTable props, Column options, table state, types, messages, the engine and the modules. Descriptions are checked against the package types by a test, so a renamed prop cannot keep a stale row.

- **Source**: types.tstable.svelte.tsmessages.tsindex.ts
- **Tests**: table-api.test.ts (this page against the types)table.test.tscore.test.ts
- **Fixture**: fixtures/table-basic

## Exports

The root entry ships the renderer, the engine, the pure helpers it is built from, and the two message sets. The ./core entry contains the pure functions only and imports nothing from Svelte.

### Root entry

import { … } from '@svelte-lean/table'

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `DataTable` | `Svelte component` | – | The renderer: a native \<table\> over createTable(). |
| # `createTable` | `(options: TableOptions<T>) => Table<T>` | – | The reactive engine below the renderer (runes; needs the Svelte compiler). |
| # `createRowModel` | `(context: ModelContext<T>) => RowModel<T>` | – | The default row model: search, filters, sorting, tree flattening, row pinning, pagination. |
| # `defaultState` | `(patch?: Partial<TableState>) => TableState` | – | A complete state object with optional overrides. |
| # `valueOf` | `(row: T, column: Column<T>) => unknown` | – | Reads a cell value through the column accessor (field, dot path or function). |
| # `visibleColumns` | `(columns, state) => Column<T>[]` | – | Leaf columns that are not hidden, in pin zone and user order. |
| # `toggleSort` | `(sorting: Sort[], id: string, multi?: boolean, descFirst?: boolean) => Sort[]` | – | Pure sort cycle none, ascending, descending, none; descFirst starts descending. |
| # `toggleSelection` | `(current: RowId[], eligible: RowId[], select?: boolean) => RowId[]` | – | Adds or removes the eligible ids; without select it selects unless all are selected. |
| # `en` | `TableMessages` | – | English messages, the default of the renderer. |
| # `tr` | `TableMessages` | – | Turkish messages. |

### Core entry

import { … } from '@svelte-lean/table/core' — pure functions over the same types; runs under plain Node.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `leafColumns` | `(columns) => Column<T>[]` | – | Flattens nested column groups to their leaves. |
| # `headerRows` | `(columns, visible) => HeaderCell<T>[][]` | – | Header rows with colSpan and rowSpan; a group split by reordering or pinning renders as separate fragments. |
| # `matchesFilter` | `(value, filter: Filter, locale?) => boolean` | – | The default filter predicate for every FilterOperator. |
| # `fold` | `(value, locale?) => string` | – | Case and accent folding used by search and text filters (Turkish dotted and dotless i included). |
| # `prepareRows` | `(context: ModelContext<T>) => RowNode<T>[]` | – | Filtered and sorted root nodes before pagination; groupBy() builds on it. |
| # `paginateNodes` | `(nodes, context) => RowModel<T>` | – | Applies row pinning, page slicing and expansion to prepared nodes. |
| # `flattenRows` | `(nodes, expanded?) => RowNode<T>[]` | – | Depth-first flattening; with an expanded set, collapsed children are skipped. |
| # `clampWidth` | `(column, width) => number` | – | Clamps a width to minWidth and maxWidth (defaults 64 and 1200). |
| # `columnWidth` | `(column, state) => number` | – | The rendered width: state.widths, then column.width, then 160, clamped. |
| # `toggleId` | `(ids: RowId[], id: RowId) => RowId[]` | – | Adds or removes one id. |
| # `sortOf` | `(sorting: Sort[], column: Pick<Column<T>, 'id' \| 'sortKeys'>) => { id; desc; index } \| undefined` | – | The first sort rule on the column id or one of its sortKeys, with its precedence. |
| # `reorder` | `(items, source, target) => T[]` | – | Moves source before target in a copy of the list. |

## DataTable props

Every capability of the renderer is a prop. The three required props establish the row contract; everything else defaults to off or to the plain table.

### Data and identity

The rows, the schema and a stable identity per row.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `data` required | `readonly T[]` | – | Rows of the current client or server page. |
| # `columns` required | `readonly Column<T>[]` | – | Typed column schema; nested children produce grouped headers. |
| # `getRowId` required | `(row: T) => RowId` | – | Returns a finite number or a string that stays stable across renders; selection, expansion, pinning and edits are keyed by it. Duplicates throw. |
| # `getSubRows` | `(row: T) => readonly T[] \| undefined` | `undefined` | Child rows for tree tables. Not combinable with groupBy(). |
| # `getRowModel` | `(context: ModelContext<T>) => RowModel<T>` | `createRowModel` | Replaces the client row model; groupBy() from the grouping entry is the shipped alternative. |
| # `id` | `string` | `undefined` | id of the root element. With selection: 'single' it also names the row radios (\<id\>-selection) so they form one native radio group. Authored, never generated. |

### Controlled state

Bind one serializable object or listen without binding.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `state` | `Partial<TableState>` | `{}` | Sorting, filters, search, paging, selection, expansion and column layout. Missing fields take the defaultState() values. Bindable. |
| # `onStateChange` | `(state: TableState) => void` | `undefined` | Runs after every table-originated change with the complete next state. |
| # `manual` | `boolean` | `false` | Treats data as already sorted, filtered and paged by the application; the client model applies none of them. |
| # `rowCount` | `number` | `undefined` | Total row count in manual mode, for the page count, the status line and aria-rowcount. Without it the status line leaves the total out. |
| # `pageCount` | `number` | `from rowCount` | Manual mode: the page count when the total is unknown, for example pageIndex + 2 while the server reports a next page. |
| # `pagination` | `boolean` | `true` | Shows the pager and slices pages. false renders the whole row model on one page. |
| # `pageSizeOptions` | `number[]` | `[10, 20, 50, 100]` | Sizes offered by the page-size select; the current size is always added. 0 is the all-rows option. |
| # `pageSizeSelect` | `'native' \| 'custom'` | `'native'` | The page-size control. native: a native \<select\>. custom: the Select primitive (a trigger button and a listbox popup drawn the same in every engine) around the same native \<select\>, which keeps the value and works without scripts; needs @svelte-lean/primitives/select/register and @svelte-lean/styles/select.css. |

### Controls

Each control is independent; none is rendered by default.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `searchable` | `boolean` | `false` | Shows the search input. state.search applies without it, so a search box elsewhere in the page can write the state. |
| # `searchDelay` | `number` | `0` | Milliseconds between the last keystroke in the search box or a filter input and the state change; clearing a field applies at once. |
| # `searchText` | `(row: T) => string` | `undefined` | The text search terms are matched against, per row. Without it: the values of the searchable, visible columns. |
| # `filters` | `boolean` | `false` | Adds a filter input under each filterable leaf header (operator contains). |
| # `columnControls` | `boolean` | `false` | Shows the visibility, order and pin controls in a \<details\> panel. |
| # `selection` | `false \| 'single' \| 'multiple'` | `false` | Adds a radio or checkbox column. 'single' needs the id prop for the radio group name. |
| # `selectionScope` | `'page' \| 'filtered'` | `'page'` | Rows that select-all and the header checkbox consider: the current page or every filtered row. |
| # `isRowSelectable` | `(row: T) => boolean` | `() => true` | Disables the selection control of rows that fail the rule. |
| # `hideSelectAll` | `boolean` | `false` | Hides the header checkbox of multiple selection; the header cell keeps the selectionHeader text for assistive technology. |
| # `rowLabel` | `(row: T) => string` | `the row id` | Accessible name of a row in its selection, expand and move controls. |

### Expansion

Details rows and tree rows share the expanded id list in the state.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `rowDetails` | `Snippet<[T, RowDetailsContext]>` | `undefined` | Renders a full-width details row under an expanded row. The second argument carries rowId, index and collapse(). A DataTable inside it keeps its own styles and keyboard behavior. |
| # `expandColumn` | `boolean` | `false` | Renders the expand toggle in its own narrow leading column (after selection) instead of the first data column. |
| # `rowExpandable` | `(row: T) => boolean` | `() => true` | Whether a row may show details. |
| # `expandRowByClick` | `boolean` | `false` | Toggles expansion when a non-interactive part of a cell is clicked. |
| # `indentSize` | `number` | `18` | Pixels added to the first cell per tree depth. |
| # `onExpand` | `(expanded: boolean, row: T) => void` | `undefined` | Runs when a data row is expanded or collapsed. |
| # `onExpandedRowsChange` | `(rows: RowId[]) => void` | `undefined` | Runs with the complete next expanded id list, group rows included. |

### Presentation

Structure and naming stay explicit props; colors and spacing are CSS custom properties.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `label` | `string` | `'Data table'` | Accessible name of the scroll region and the \<table\>. |
| # `caption` | `string` | `undefined` | Visible native \<caption\>. |
| # `title` | `Snippet<[RowModel<T>]>` | `undefined` | Content above the toolbar with the current row model. |
| # `showHeader` | `boolean` | `true` | Renders the \<thead\>. |
| # `tableLayout` | `'auto' \| 'fixed'` | `'fixed'` | The CSS table-layout of the \<table\>. fixed applies every column width; auto leaves columns without width, resize or pin to the browser, which sizes them from content. |
| # `layout` | `'table' \| 'grid'` | `'table'` | table: native table layout. grid: the same elements laid out as CSS grid rows with explicit ARIA roles; body rows stand in sections the browser skips off screen (content-visibility: auto), and column widths are measured from the content once and kept. width is a minimum, maxWidth caps a measured width (default 480), a wrap column narrows below its content before the rows scroll, span and tableLayout do not apply. For pages of hundreds of rows. |
| # `density` | `'compact' \| 'comfortable' \| 'spacious'` | `'comfortable'` | Cell padding and font size (compact: 13px), as data-density on the root. |
| # `striped` | `boolean` | `false` | Alternate row backgrounds. |
| # `bordered` | `boolean` | `false` | Vertical cell borders. |
| # `stickyHeader` | `boolean` | `true` | Keeps the \<thead\> in view. With height it sticks inside the scroll region; without, it sticks to the page while the table fits its width (offset by --slean-table-sticky-top). A table wider than its container scrolls sideways in the region, which captures sticky positioning: the header then scrolls with the rows unless the region also scrolls down. |
| # `pinBudget` | `number \| false` | `0.4` | The share of the visible region the pinned columns may take. Past it pins are let go by pinPriority, then innermost first, so a narrow screen keeps room to scroll; false keeps every pin. |
| # `height` | `string` | `undefined` | max-height of the scroll region; with it the region is a Tab stop. |
| # `progressive` | `boolean \| number` | `false` | Progressive mounting: when at least a first batch of rows the table has not drawn arrives (true = one screenful, or the number given), that batch renders at once and the rest follow in slices sized from the measured cost of a row, one macrotask apart, so a large page never blocks the main thread. A first render in the browser mounts the same way; server output and its hydration are always complete. |
| # `rowHoverable` | `boolean` | `true` | Row hover background. |
| # `dir` | `'ltr' \| 'rtl'` | `undefined` | Text direction of the table. Without it the root carries no dir attribute and keyboard, resize and pinning follow the nearest dir attribute in the document. |
| # `locale` | `string` | `undefined` | Locale of the collator used for sorting and of case folding in search and filters. |
| # `messages` | `Partial<TableMessages>` | `en` | Overrides of the visible and accessible strings; missing keys fall back to en. |
| # `placeholder` | `string` | `''` | Text of an empty value (null, undefined, empty string, or a format result of null or empty string). Not applied to cell snippets. |
| # `class` | `string` | `''` | Extra class on the root element. |

### States

Network ownership stays with the application; the table announces what it is told.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `loading` | `boolean` | `false` | Announces the loading status and marks the table aria-busy; without rows it draws skeletonRows placeholder rows. |
| # `fetching` | `boolean` | `false` | Background refresh: status, aria-busy and data-fetching on the root (the body dims) while rows stay visible. |
| # `refreshing` | `boolean` | `false` | Quiet refresh (a poll, an invalidation): data-refreshing on the root draws a thin line along the top edge. Rows are not dimmed or replaced, no aria-busy, no status, page clamping stays on. Ignored while loading or fetching. |
| # `skeletonRows` | `number` | `page size (at most 25) when paginated, else 5` | Placeholder rows drawn while loading without rows; 0 shows the loading message instead. |
| # `measureKey` | `unknown` | `undefined` | Change it when the cells change width with nothing in their DOM changing (a style, a class on an ancestor): the grid measures every row on the page again, when idle; a column only grows. A cell whose text or children change is measured again without it. |
| # `error` | `string` | `undefined` | Text of an alert rendered above the table. |
| # `onRetry` | `() => void` | `undefined` | Adds the retry button to the alert. |
| # `empty` | `Snippet<[EmptyContext]>` | `undefined` | Content of the single body cell when the row model has no rows. Receives search, filtered and loading to tell no matches from no data. |

### Events and composition

Side effects leave through callbacks; content enters through snippets.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `onEdit` | `(event: EditEvent<T>) => void \| Promise<void>` | `undefined` | Receives a parsed and validated cell edit; enables editing and cell focus. The table never mutates rows. |
| # `onRowClick` | `(row: T, event: MouseEvent \| KeyboardEvent, index: number) => void` | `undefined` | Runs for clicks outside controls (button, a, input, select, textarea, label, summary, contenteditable, menuitem, \[data-action\], \[data-no-row-click\]). Rows become Tab stops answering Enter and Space unless the table has cell navigation. |
| # `onRowDoubleClick` | `(row: T, event: MouseEvent) => void` | `undefined` | Runs on a row double-click. |
| # `onRowContextMenu` | `(row: T, event: MouseEvent) => void` | `undefined` | Runs on the contextmenu event of a row; the table does not prevent the default. |
| # `onAction` | `(action: string, row: T, trigger: HTMLElement, index: number) => void` | `undefined` | Delegated click on any element inside a cell that carries data-action. |
| # `onRowMove` | `(source: RowId, target: RowId) => void` | `undefined` | Adds a leading grip column. A grip is dragged live (the row follows the pointer, the others slide aside, the container scrolls near its edges, Escape cancels) or moved with ArrowUp, ArrowDown, Home and End. Moves stay within the page; the grips are hidden while sorted, filtered or searched, and there is no grip column with a tree or a custom row model. |
| # `onScroll` | `(event: Event) => void` | `undefined` | Scroll of the table region. |
| # `toolbar` | `Snippet<[RowModel<T>]>` | `undefined` | Content in the toolbar beside the built-in controls. |
| # `footer` | `Snippet<[RowModel<T>]>` | `undefined` | Content after the pager. |
| # `summary` | `Record<string, string \| number>` | `undefined` | A \<tfoot\> row keyed by leaf column id; summarize() from the grouping entry produces one. |
| # `rowClass` | `(row: T, index: number) => string` | `undefined` | Class of a row, for application state or animation. A class that sets --slean-table-row-bg tints every cell of the row, pinned cells included. |
| # `cellRange` | `CellRange \| null` | `undefined` | The highlighted rectangular range; with onCellRangeChange cells become focusable. |
| # `onCellRangeChange` | `(range: CellRange \| null) => void` | `undefined` | Reports a click, Shift+click or Shift+arrow range. |

### Methods

Exported from the component instance (bind:this).

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `scrollTo` | `(config: { key?: RowId; index?: number; top?: number; offset?: number; align?: ScrollLogicalPosition }) => void` | – | Scrolls the region to a row by id or index, or to a pixel offset. |

## Column

A column reads a field, a dot path or a computed value, and declares how it sorts, filters, renders and edits. Columns are plain objects; only snippets tie them to a Svelte file.

### Data and content

Identity, the value it reads and how the value is rendered.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `id` required | `string` | – | Stable identity used by state fields and by every helper. |
| # `header` required | `string` | – | Header text and the label of its controls. |
| # `accessor` | `keyof T \| string \| ((row: T) => unknown)` | `column.id` | A field, a dot path or a function. Function accessors cannot be edited through applyEdit(). |
| # `children` | `Column<T>[]` | `undefined` | Nested columns; the parent renders as a group header. |
| # `cell` | `Snippet<[CellContext<T>]>` | `undefined` | Custom body cell with row, value, rowId, index and column. |
| # `headerCell` | `Snippet<[HeaderContext<T>]>` | `undefined` | Custom leaf header content. Receives the column, its sort, the sort list, toggleSort() and setSorting(); with sortable: false it can host a sort control of its own. |
| # `format` | `(value: unknown, row: T) => string \| null` | `String(value)` | Formats the plain text of a cell and of a CSV export without a snippet; null or an empty string renders the placeholder. |

### Layout

Widths and pinning use logical insets, so they work in both directions.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `width` | `number` | `160` | Initial width in pixels. Optional with tableLayout auto, where a column without one sizes to content; pinned columns fall back to 160 for their offsets. |
| # `minWidth` | `number` | `64` | Lower bound of resizing. |
| # `maxWidth` | `number` | `1200` | Upper bound of resizing. |
| # `wrap` | `boolean` | `false` | Grid layout: text that may break onto more lines (a description, a note). The column stands at its content while there is room and narrows down to minWidth (160 by default) before the rows scroll sideways, its text wrapping; it is never clipped. |
| # `align` | `'start' \| 'center' \| 'end'` | `'start'` | Logical text alignment. |
| # `pin` | `'left' \| 'right' \| false` | `false` | Initial sticky edge; state.pinning overrides it. |
| # `pinFrom` | `number` | `—` | The least width (px) of the table's visible region at which the pin holds; below it the column scrolls with the others. |
| # `pinPriority` | `number` | `0` | Which pins stay when they are past the table's pinBudget: a lower priority is let go first; among equals, the innermost. |
| # `hideable` | `boolean` | `true` | Whether the column controls may hide it. |
| # `resizable` | `boolean` | `true` | Whether the header shows a resize handle. |
| # `ellipsis` | `boolean \| { showTitle?: boolean }` | `false` | Clips overflow with an ellipsis; showTitle exposes the full text as a title attribute. |
| # `span` | `(context: CellContext<T>) => CellSpan \| undefined` | `undefined` | rowspan and colspan of a cell; a zero suppresses the cell covered by a span. |
| # `class` | `string \| ((row: T) => string)` | `''` | Class on every body cell of the column, or one computed per row. |
| # `headerClass` | `string` | `undefined` | Class on the leaf header cell. |

### Sort, filter and aggregate

Defaults cover numbers, dates and text; each can be replaced per column.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `sortable` | `boolean` | `true` | Header sorting; empty values sort last in both directions. |
| # `compare` | `(a: unknown, b: unknown, aRow: T, bRow: T) => number` | `numbers, dates, then a locale collator` | Custom comparator; the sort direction is applied to its result. |
| # `sortDescFirst` | `boolean` | `false` | The first activation sorts descending: descending, ascending, none. |
| # `sortKeys` | `string[]` | `undefined` | Further sort ids that count as this column sorted (aria-sort, the icon), for a headerCell that sorts by several fields. |
| # `filterable` | `boolean` | `true` | Gets a filter input when filters is on. |
| # `filter` | `(value: unknown, filter: Filter, row: T) => boolean` | `matchesFilter` | Custom predicate instead of the operator matcher. |
| # `searchable` | `boolean` | `true` | Included in the search. |
| # `aggregate` | `'sum' \| 'mean' \| 'min' \| 'max' \| 'count' \| ((values: unknown[]) => unknown)` | `undefined` | Aggregate used by groupBy() and summarize(). |

### Editing

The renderer parses and validates; the application commits.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `editable` | `boolean \| ((row: T) => boolean)` | `false` | Cell editing for the column or per row; needs onEdit on the table. |
| # `editor` | `'text' \| 'number' \| 'date' \| 'select'` | `'text'` | Native input type of the editor. |
| # `options` | `{ label: string; value: string }[]` | `[]` | Options of the select editor. |
| # `parse` | `(value: string, row: T) => unknown` | `parseEdit` | Converts the editor text to the domain value. |
| # `validate` | `(value: unknown, row: T) => string \| undefined` | `undefined` | Returns an error message that blocks onEdit and is shown in the editor. |

## Table state

TableState is one plain, serializable object. Every change passes through setState() and onStateChange; bind:state on the renderer is the same path.

### TableState

Fields and their defaultState() values.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `sorting` | `Sort[]` | `[]` | Ordered sort rules; the array order is the multi-sort precedence. |
| # `filters` | `Filter[]` | `[]` | Column filters with operator and value. |
| # `search` | `string` | `''` | The search text; terms are split on whitespace. |
| # `pageIndex` | `number` | `0` | Zero-based page. |
| # `pageSize` | `number` | `20` | Rows per page; 0 shows every row. |
| # `selection` | `RowId[]` | `[]` | Selected row ids. |
| # `expanded` | `RowId[]` | `[]` | Expanded details, tree and group row ids. |
| # `hidden` | `string[]` | `[]` | Hidden column ids. |
| # `order` | `string[]` | `[]` | Explicit leaf column order. |
| # `widths` | `Record<string, number>` | `{}` | Resized widths by column id. |
| # `pinning` | `Record<string, Pin>` | `{}` | Pin overrides by column id. |
| # `rowPinning` | `{ top: RowId[]; bottom: RowId[] }` | `{ top: [], bottom: [] }` | Rows kept at the start or end of every page. |

## Types

Event payloads and model types, all exported from the root entry.

### Events and positions

Small typed values suited to stores, query layers and command handlers.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `EditEvent<T>` | `{ row: T; rowId: RowId; columnId: string; previous: unknown; value: unknown }` | – | The parsed edit passed to onEdit and produced by pasteRange(). |
| # `CellPosition` | `{ row: RowId; column: string }` | – | A cell by row id and column id. |
| # `CellRange` | `{ start: CellPosition; end: CellPosition }` | – | An inclusive rectangle anchored by two positions. |
| # `Sort` | `{ id: string; desc: boolean }` | – | One sort rule. |
| # `Filter` | `{ id: string; operator?: FilterOperator; value: unknown }` | – | One column filter. |
| # `FilterOperator` | `'contains' \| 'equals' \| 'startsWith' \| 'gt' \| 'gte' \| 'lt' \| 'lte' \| 'between' \| 'in' \| 'empty'` | – | Operators of matchesFilter(); 'between' takes a two-element array, 'in' an array. |
| # `RowId` | `string \| number` | – | Row identity; numbers must be finite. |
| # `Pin` | `'left' \| 'right' \| false` | – | A pin edge. |
| # `Aggregate` | `'sum' \| 'mean' \| 'min' \| 'max' \| 'count' \| ((values: unknown[]) => unknown)` | – | An aggregate name or function. |

### Row model

What createRowModel() and a custom getRowModel exchange.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `ModelContext<T>` | `{ data; columns; state; getRowId; getSubRows?; manual?; rowCount?; pageCount?; searchText?; locale? }` | – | Input of a row model function. |
| # `RowModel<T>` | `{ rows: RowNode<T>[]; total: number; pageCount: number; pageIndex: number; all: RowNode<T>[] }` | – | The page to render, the filtered total and every filtered data row. |
| # `RowNode<T>` | `{ id; data?; depth; children; kind: 'row' \| 'group'; label?; count?; aggregates? }` | – | A rendered row or a group row. |
| # `CellContext<T>` | `{ row: T; value: unknown; rowId: RowId; index: number; column: Column<T> }` | – | Argument of cell snippets and span callbacks. |
| # `CellSpan` | `{ rows?: number; columns?: number }` | – | Result of a span callback. |
| # `HeaderContext<T>` | `{ column; sort: { id; desc; index } \| undefined; sorting: Sort[]; toggleSort(multi?); setSorting(sorting) }` | – | Argument of headerCell snippets. |
| # `EmptyContext` | `{ search: string; filtered: boolean; loading: boolean }` | – | Argument of the empty snippet; filtered is true while a column filter holds a value. |
| # `RowDetailsContext` | `{ rowId: RowId; index: number; collapse: () => void }` | – | Second argument of the rowDetails snippet. |
| # `HeaderCell<T>` | `{ key: string; column: Column<T>; colSpan: number; rowSpan: number }` | – | One header cell as rendered. |

## Messages

Every visible or accessible string of the renderer comes from TableMessages. Pass a partial object to override some of them; en and tr are complete sets. Defaults shown are the en values.

### TableMessages

Keys, where each one is used, and the English default.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `search` | `string` | `'Search rows'` | Label and placeholder of the search input. |
| # `columns` | `string` | `'Columns'` | Summary of the column controls panel. |
| # `selectAll` | `string` | `'Select all eligible rows'` | Label of the header checkbox. |
| # `selectRow` | `string` | `'Select row'` | Prefix of each row control label (followed by the row id). |
| # `expand` | `string` | `'Expand row'` | Label of a collapsed row toggle. |
| # `collapse` | `string` | `'Collapse row'` | Label of an expanded row toggle. |
| # `previous` | `string` | `'Previous page'` | Pager button. |
| # `next` | `string` | `'Next page'` | Pager button. |
| # `first` | `string` | `'First page'` | Pager button. |
| # `last` | `string` | `'Last page'` | Pager button. |
| # `pageSize` | `string` | `'Rows per page'` | Label of the page-size select. |
| # `loading` | `string` | `'Loading rows…'` | Status text while loading or fetching. |
| # `empty` | `string` | `'No matching rows'` | Body text when no row matches. |
| # `retry` | `string` | `'Retry'` | Retry button in the error alert. |
| # `save` | `string` | `'Save'` | Editor button. |
| # `cancel` | `string` | `'Cancel'` | Editor button. |
| # `edit` | `string` | `'Edit'` | Prefix of the per-cell edit button label. |
| # `resize` | `string` | `'Resize'` | Prefix of the resize handle label. |
| # `moveLeft` | `string` | `'Move earlier'` | Column controls: move a column earlier. |
| # `moveRight` | `string` | `'Move later'` | Column controls: move a column later. |
| # `moveRow` | `string` | `'Drag to reorder'` | Hidden header text of the grip column, and prefix of each grip label (followed by the row name). |
| # `moveUp` | `string` | `'Move up'` | Deprecated: unused since the grip column. |
| # `moveDown` | `string` | `'Move down'` | Deprecated: unused since the grip column. |
| # `pin` | `string` | `'Pin'` | Prefix of the pin select label. |
| # `unpin` | `string` | `'Unpinned'` | Option of the pin select. |
| # `pinLeft` | `string` | `'Pin start'` | Option of the pin select. |
| # `pinRight` | `string` | `'Pin end'` | Option of the pin select. |
| # `allRows` | `string` | `'All'` | The pageSize: 0 option of the page-size select. |
| # `sortOrder` | `string` | `'Sort order'` | Read to assistive technology before the multi-sort precedence number. |
| # `selectionHeader` | `string` | `'Selection'` | Visually hidden header text of the selection column when it has no select-all checkbox. |
| # `expandHeader` | `string` | `'Details'` | Visually hidden header text of the expand column (expandColumn). |
| # `filter` | `(column: string) => string` | `` (column) => `Filter ${column}` `` | Label of a column filter input. |
| # `page` | `(page: number, pages: number, rows: number \| undefined) => string` | ``(page, pages, rows) => `Page ${page} of ${pages}` + (rows === undefined ? '' : ` · ${rows} rows`)`` | The pager status line; rows is undefined when a manual table has no rowCount. |
| # `pageNumber` | `(page: number) => string` | `` (page) => `Page ${page}` `` | Accessible name of a numbered page button. |
| # `selected` | `(count: number) => string` | `` (count) => `${count} selected` `` | The live selection count in the toolbar. |

## Engine

createTable() is the engine the renderer is built on. Options are read lazily, so a getter keeps the engine reactive to the caller state; a state getter makes it controlled.

Use the engine when the renderer's markup is not the markup you need: a different table structure, a list, a chart legend, or a test that asserts the model. It is the escape hatch under `DataTable` and the same code path, so the behavior matches.

`own renderer`

```svelte
<script lang="ts">
	import { createTable, type Column } from '@svelte-lean/table';

	type Member = { id: number; name: string; revenue: number };
	let rows = $state<Member[]>(load());
	const columns: Column<Member>[] = [
		{ id: 'name', header: 'Member' },
		{ id: 'revenue', header: 'Revenue', align: 'end' }
	];

	// Options are read lazily: a getter keeps the engine reactive to $state in the caller.
	// A plain object as state makes the table own it; a state getter makes it controlled.
	const table = createTable({
		data: () => rows,
		columns,
		getRowId: (row) => row.id,
		state: { pageSize: 10 },
		onStateChange: (next) => console.log(next.sorting)
	});
</script>

<!-- A renderer of your own: derived values are getters and update with the state. -->
<table>
	<thead>
		<tr>
			{#each table.visibleColumns as column (column.id)}
				<th aria-sort={table.sortOf(column.id)?.desc ? 'descending' : table.sortOf(column.id) ? 'ascending' : undefined}>
					<button type="button" onclick={() => table.toggleSort(column.id)}>{column.header}</button>
				</th>
			{/each}
		</tr>
	</thead>
	<tbody>
		{#each table.model.rows as node (node.id)}
			<tr>
				{#each table.visibleColumns as column (column.id)}
					<td>{String(valueOf(node.data!, column) ?? '')}</td>
				{/each}
			</tr>
		{/each}
	</tbody>
</table>
<p>Page {table.model.pageIndex + 1} of {table.model.pageCount}</p>
```

`controlled engine`

```ts
// Controlled: the engine reads the caller's state and stores nothing itself.
let view = $state<Partial<TableState>>({});
const table = createTable({
	data: () => rows,
	columns,
	getRowId: (row) => row.id,
	state: () => view,
	onStateChange: (next) => (view = next)
});

table.setSearch('rhye');       // reports { …view, search: 'rhye', pageIndex: 0 } through onStateChange
table.toggleSelection(1);      // selection helpers respect the selection mode and scope options
table.model.total;             // filtered count, recomputed from the getters
```

`core entry, no Svelte`

```ts
// No Svelte at all: the pure model under plain Node, for tests or a server-side export.
import { createRowModel, defaultState } from '@svelte-lean/table/core';

const model = createRowModel({
	data: rows,
	columns,
	state: defaultState({ sorting: [{ id: 'revenue', desc: true }], pageSize: 0 }),
	getRowId: (row) => row.id
});
model.rows.map((node) => node.data);
```

### TableOptions

A MaybeGetter\<V\> is a value or a function returning it.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `data` required | `MaybeGetter<readonly T[]>` | – | Rows. |
| # `columns` required | `MaybeGetter<readonly Column<T>[]>` | – | Column schema. |
| # `getRowId` required | `(row: T) => RowId` | – | Row identity. |
| # `getSubRows` | `(row: T) => readonly T[] \| undefined` | `undefined` | Tree children. |
| # `state` | `Partial<TableState> \| (() => Partial<TableState>)` | `{}` | A plain object is the initial state of a table that owns its state; a getter makes it controlled. |
| # `onStateChange` | `(state: TableState) => void` | `undefined` | Reports every change, owned or controlled. |
| # `getRowModel` | `(context: ModelContext<T>) => RowModel<T>` | `createRowModel` | Custom row model. |
| # `manual` | `MaybeGetter<boolean \| undefined>` | `false` | Manual mode. |
| # `rowCount` | `MaybeGetter<number \| undefined>` | `undefined` | Manual row count. |
| # `pageCount` | `MaybeGetter<number \| undefined>` | `undefined` | Manual page count when the total is unknown. |
| # `searchText` | `(row: T) => string` | `undefined` | Searchable text of a row instead of the searchable columns. |
| # `locale` | `MaybeGetter<string \| undefined>` | `undefined` | Collator locale. |
| # `pagination` | `MaybeGetter<boolean \| undefined>` | `true` | false builds the model with every row on one page. |
| # `selection` | `MaybeGetter<SelectionMode \| undefined>` | `'multiple'` | false makes every selection helper a no-op. |
| # `isRowSelectable` | `(row: T) => boolean` | `() => true` | Selection rule. |
| # `selectionScope` | `MaybeGetter<'page' \| 'filtered' \| undefined>` | `'page'` | Rows that selectAll() and allSelected consider. |

### Table

Derived values are getters (reactive under runes); transitions call setState() and report through onStateChange.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `state` | `TableState` | – | The complete current state. |
| # `model` | `RowModel<T>` | – | Paginated, sorted and filtered row nodes. |
| # `columns` | `readonly Column<T>[]` | – | Column definitions as given, groups included. |
| # `leafColumns` | `Column<T>[]` | – | Every leaf column. |
| # `visibleColumns` | `Column<T>[]` | – | Leaf columns that are not hidden, in pin zone and user order. |
| # `headers` | `HeaderCell<T>[][]` | – | Header rows with spans. |
| # `selectable` | `RowId[]` | – | Ids a selection control may act on within selectionScope. |
| # `allSelected` | `boolean` | – | Every selectable row is selected. |
| # `someSelected` | `boolean` | – | At least one selectable row is selected. |
| # `isSelected` | `(id: RowId) => boolean` | – | Selection membership. |
| # `isExpanded` | `(id: RowId) => boolean` | – | Expansion membership. |
| # `sortOf` | `(column: string \| Column<T>) => { id: string; desc: boolean; index: number } \| undefined` | – | Sort rule, direction and precedence of a column; given a column, a rule on one of its sortKeys counts. |
| # `pinOf` | `(column: Column<T>) => Pin \| undefined` | – | Effective pin of a column. |
| # `columnWidth` | `(column: Column<T>) => number` | – | Effective width of a column. |
| # `setState` | `(patch: Partial<TableState>) => TableState` | – | Merges a patch, stores it when the table owns its state, reports it. |
| # `toggleSort` | `(id: string, multi?: boolean) => void` | – | Cycles none, ascending, descending, none (descending first with sortDescFirst); multi keeps other sorted columns. Resets the page. |
| # `toggleSelection` | `(id: RowId, selected?: boolean) => void` | – | Toggles one row; in 'single' mode the row replaces the selection. |
| # `selectAll` | `(ids?: readonly RowId[], selected?: boolean) => void` | – | Selects or clears many rows; defaults to selectable and to select unless all are selected. |
| # `toggleExpanded` | `(id: RowId, expanded?: boolean) => boolean` | – | Returns the new expanded state. |
| # `setSearch` | `(search: string) => void` | – | Sets the search text and resets the page. |
| # `setFilter` | `(id: string, value: unknown, operator?: FilterOperator) => void` | – | Replaces the filter of a column; undefined removes it. Resets the page. |
| # `setPage` | `(index: number) => void` | – | Clamped to the page count. |
| # `setPageSize` | `(size: number) => void` | – | 0 shows every row. Resets the page. |
| # `moveColumn` | `(id: string, delta: number) => void` | – | Moves a visible column delta places. |
| # `resizeColumn` | `(id: string, width: number) => void` | – | Stores a clamped width. |
| # `pin` | `(id: string, pin: Pin) => void` | – | Pins a column. |
| # `toggleColumn` | `(id: string, visible?: boolean) => void` | – | Hides or shows a column. |

## Modules

Separate entry points, one subpath each. They are pure functions over the same types, never import the renderer and stay out of a build until imported (proved by the table-basic fixture).

### Entry points

Import path, exports and purpose. Sizes are on the overview page and in the size file.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| # `@svelte-lean/table/grouping` | `groupBy · summarize · aggregate` | – | A grouped row model for getRowModel, a totals object for summary, and the aggregate function both use. |
| # `@svelte-lean/table/pivot` | `pivot` | – | Long-form records to a cross-tab: rows, columns and totals for a second DataTable. |
| # `@svelte-lean/table/editing` | `applyEdit · setValue · parseEdit · createHistory · editEvent` | – | Immutable edit application and a caller-owned undo history. |
| # `@svelte-lean/table/clipboard` | `copyRange · pasteRange · rangeBounds` | – | A rectangular range to tab-separated text and back to validated EditEvents. |
| # `@svelte-lean/table/export` | `toCsv · parseDelimited · downloadText` | – | CSV with quoting and formula protection, a parser, and a browser download. |
| # `@svelte-lean/table/server` | `createDataSource` | – | Sequencing and abort of a loader the application supplies. |
| # `@svelte-lean/table/state` | `serializeState · restoreState` | – | Versioned persistence of presentation state; unknown columns are dropped on restore. |
| # `@svelte-lean/table/style.css` | `stylesheet` | – | Styles for the slean-table- classes; reads --slean-\* tokens when present. |

### Module sizes

Each entry bundled on its own with esbuild, Svelte's runtime excluded; shared code is counted per entry here and de-duplicated by a consumer's bundler. Read from `packages/table/artifacts/size.json`.

| Entry point | gzip | brotli | Budget (gzip) |
| --- | --- | --- | --- |
| `@svelte-lean/table/grouping` | 2149 B | 1962 B | 2400 B |
| `@svelte-lean/table/pivot` | 526 B | 470 B | 600 B |
| `@svelte-lean/table/editing` | 707 B | 617 B | 800 B |
| `@svelte-lean/table/clipboard` | 895 B | 803 B | 1000 B |
| `@svelte-lean/table/export` | 741 B | 646 B | 850 B |
| `@svelte-lean/table/server` | 369 B | 310 B | 425 B |
| `@svelte-lean/table/state` | 796 B | 711 B | 900 B |
| `@svelte-lean/table/style.css` | 9086 B | 7896 B | 9100 B |
