# Getting started

> Install, import, the three required props, the style import, the first render, binding state, and the semantic <table> output.

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

Install one package, describe the rows and the columns, and render a native table. Three props are required; every capability after that is a prop or a separate entry point.

- **Source**: data-table.sveltetable.svelte.tscore.ts
- **Tests**: data-table.dom.test.tstable.test.tstable.spec.ts (browser)
- **Fixture**: fixtures/table-basic
- **Benchmark**: table.bench.spec.ts

## Install

`@svelte-lean/table` has one peer dependency, Svelte 5. It does not need `@svelte-lean/vite`, `@svelte-lean/core` or the primitives: the table is a Svelte component, not a behavior discovered from markup.

```bash
npm install @svelte-lean/table
```

### TypeScript

The package is ESM only and resolves its types through `exports`. It type-checks under `"moduleResolution": "bundler"` (the SvelteKit and Vite default) and under `"node16"` or `"nodenext"` from ESM: next to each component declaration the build writes a `.d.svelte.ts` file, which is how those resolvers find the types of an import that ends in `.svelte`. The legacy `"node"` (node10) resolver does not read `exports` and finds only the root entry; CommonJS `require()` is not supported. The package build runs `attw --pack` with the ESM-only profile and fails on a resolution error.

## First render

`data` is the array of rows, `columns` the schema, and `getRowId` returns a stable identity per row. Identity matters before any feature is switched on: selection, expansion, pinning and edits are all keyed by it, and a duplicate id throws at render time instead of producing silent mismatches.

`members.svelte`

```svelte
<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} />
```

## Rendered output

The component renders a native `<table>` with `<colgroup>`, `<thead>`, `scope` on header cells and `aria-sort` when a column is sorted. What follows is the server output of the example above, produced by `render()` from `svelte/server` while this page was prerendered, with Svelte's hydration comments removed and tags indented. Nothing here is hand-written.

`rendered by svelte/server`

```html
<div class="slean-table " data-layout="table" data-ssr="" data-density="comfortable" data-striped="false" data-bordered="false" data-hoverable="true">
	<div class="slean-table-scroll" role="region" aria-label="Data table">
		<table aria-label="Data table" aria-busy="false" class="slean-table-sticky" style="table-layout: fixed;">
			<colgroup>
				<col data-column="name" style="width: 160px;"/>
				<col data-column="revenue" style="width: 160px;"/>
			</colgroup>
			<thead>
				<tr>
					<th scope="col" colspan="1" rowspan="1" data-column="name">
						<div class="slean-table-heading">
							<button type="button" class="slean-table-sort">
								Member
								<span class="slean-table-sort-icon">
									<svg class="slean-table-icon" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false">
										<path d="M5 6l3-3 3 3M5 10l3 3 3-3">
									</path>
								</svg>
							</span>
						</button>
						<button type="button" class="slean-table-resize" aria-label="Resize: Member">
						</button>
					</div>
				</th>
				<th scope="col" colspan="1" rowspan="1" data-column="revenue" data-align="end">
					<div class="slean-table-heading">
						<button type="button" class="slean-table-sort">
							Revenue
							<span class="slean-table-sort-icon">
								<svg class="slean-table-icon" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false">
									<path d="M5 6l3-3 3 3M5 10l3 3 3-3">
								</path>
							</svg>
						</span>
					</button>
					<button type="button" class="slean-table-resize" aria-label="Resize: Revenue">
					</button>
				</div>
			</th>
		</tr>
	</thead>
	<tbody>
		<tr data-row-id="1" data-selected="false">
			<td data-cell="" data-column="name">
				<div class="slean-table-cell">
					Amara Okafor
				</div>
			</td>
			<td data-cell="" data-column="revenue" data-align="end">
				<div class="slean-table-cell">
					1200
				</div>
			</td>
		</tr>
		<tr data-row-id="2" data-last="" data-selected="false">
			<td data-cell="" data-column="name">
				<div class="slean-table-cell">
					Jonas Lindqvist
				</div>
			</td>
			<td data-cell="" data-column="revenue" data-align="end">
				<div class="slean-table-cell">
					9119
				</div>
			</td>
		</tr>
	</tbody>
</table>
</div>
</div>
```

The wrapper carries the `slean-table` class and the presentation props as `data-*` attributes; the scroll region is a labelled region; the header buttons sort; the cells carry `data-row` and `data-column` so keyboard navigation and range selection find them by structure, not by counting.

## Styles

The stylesheet is optional and imported once. It styles the `slean-table-` classes and reads the `--slean-*` tokens of `@svelte-lean/styles` when they are defined, with neutral fallbacks otherwise, so a table looks right on its own and follows a theme when there is one. Its size is 9086 B gzip (7896 B brotli).

`+layout.svelte or app.css`

```ts
import '@svelte-lean/table/style.css';
```

`app.css`

```css
/* Optional: the table reads @svelte-lean/styles tokens when they exist. */
@import '@svelte-lean/styles/tokens.css';

/* Or retint one table through its own properties. */
.slean-table {
	--slean-table-primary: var(--brand);
	--slean-table-radius: 0;
}
```

## Basic table

The smallest useful table: typed data, typed columns and stable row identity.

BasicTable.svelte

```svelte
<script lang="ts">
	import { DataTable, type Column } from '@svelte-lean/table';
	import '@svelte-lean/table/style.css';
	import { money, people, type Person } from '$lib/demo/data';

	const data = people(6);
	const columns: Column<Person>[] = [
		{ id: 'name', header: 'Member' },
		{ id: 'team', header: 'Team' },
		{ id: 'region', header: 'Region' },
		{ id: 'revenue', header: 'Revenue', align: 'end', format: (v) => money.format(Number(v)) },
		{ id: 'joined', header: 'Joined' }
	];
</script>

{#snippet title()}<span>Members ledger</span>{/snippet}
{#snippet footer()}<p class="footer">Six of the sample rows, rendered as one page.</p>{/snippet}

<DataTable
	{data}
	{columns}
	getRowId={(row) => row.id}
	label="Members"
	caption="Members by team and region"
	tableLayout="auto"
	pagination={false}
	{title}
	{footer}
/>
```

A caption, a title snippet above the toolbar and a footer snippet below the pager are all optional; label names the table for assistive technology.

- `tableLayout="auto"` lets the browser size the columns from their content.
- `pagination={false}` renders the whole row model on one page.
- Every header is a sort button by default; set `sortable: false` to opt out.

## State

Every interaction changes one plain object, `TableState`: sorting, filters, search, page, selection, expansion, hidden columns, order, widths and pinning. Bind it to own it, or listen with `onStateChange` and pass the state back in. The table stores state only when the caller does not.

`bind:state`

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

	// One plain object. Bind it, persist it, or replace it from the outside.
	let tableState = $state<Partial<TableState>>(defaultState({ pageSize: 10 }));
</script>

<DataTable {data} {columns} getRowId={(row) => row.id} bind:state={tableState} />

<pre>{JSON.stringify(tableState.sorting)}</pre>
```

`controlled from the outside`

```svelte
<DataTable
	{data}
	{columns}
	getRowId={(row) => row.id}
	state={query.state}
	onStateChange={(next) => query.setState(next)}
/>
```

## What ships

The renderer entry measures 24816 B gzip (21768 B brotli) bundled on its own, Svelte's runtime excluded. A production Vite build that imports only `DataTable` and the stylesheet ships a Svelte Lean chunk of 20841 B brotli and no grouping, pivot, clipboard, export, server or state module, asserted by the `table-basic` fixture. The [overview](https://svelte-lean.vercel.app/table.md) lists every entry point.

## API

| Name | Of | Type | Default | Description |
| --- | --- | --- | --- | --- |
| [`data`](https://svelte-lean.vercel.app/table/api.md#prop-data) | Data and identity | `readonly T[]` | – | Rows of the current client or server page. |
| [`columns`](https://svelte-lean.vercel.app/table/api.md#prop-columns) | Data and identity | `readonly Column<T>[]` | – | Typed column schema; nested children produce grouped headers. |
| [`getRowId`](https://svelte-lean.vercel.app/table/api.md#prop-getrowid) | Data and identity | `(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. |
| [`label`](https://svelte-lean.vercel.app/table/api.md#prop-label) | Presentation | `string` | `'Data table'` | Accessible name of the scroll region and the \<table\>. |
| [`caption`](https://svelte-lean.vercel.app/table/api.md#prop-caption) | Presentation | `string` | `undefined` | Visible native \<caption\>. |
| [`title`](https://svelte-lean.vercel.app/table/api.md#prop-title) | Presentation | `Snippet<[RowModel<T>]>` | `undefined` | Content above the toolbar with the current row model. |
| [`footer`](https://svelte-lean.vercel.app/table/api.md#prop-footer) | Events and composition | `Snippet<[RowModel<T>]>` | `undefined` | Content after the pager. |
| [`tableLayout`](https://svelte-lean.vercel.app/table/api.md#prop-tablelayout) | Presentation | `'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. |
| [`pagination`](https://svelte-lean.vercel.app/table/api.md#prop-pagination) | Controlled state | `boolean` | `true` | Shows the pager and slices pages. false renders the whole row model on one page. |
| [`state`](https://svelte-lean.vercel.app/table/api.md#prop-state) | Controlled state | `Partial<TableState>` | `{}` | Sorting, filters, search, paging, selection, expansion and column layout. Missing fields take the defaultState() values. Bindable. |
| [`onStateChange`](https://svelte-lean.vercel.app/table/api.md#prop-onstatechange) | Controlled state | `(state: TableState) => void` | `undefined` | Runs after every table-originated change with the complete next state. |
| [`id`](https://svelte-lean.vercel.app/table/api.md#column-id) | Data and content | `string` | – | Stable identity used by state fields and by every helper. |
| [`header`](https://svelte-lean.vercel.app/table/api.md#column-header) | Data and content | `string` | – | Header text and the label of its controls. |
| [`align`](https://svelte-lean.vercel.app/table/api.md#column-align) | Layout | `'start' \| 'center' \| 'end'` | `'start'` | Logical text alignment. |
| [`format`](https://svelte-lean.vercel.app/table/api.md#column-format) | Data and content | `(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. |
| [`defaultState`](https://svelte-lean.vercel.app/table/api.md#export-defaultstate) | Root entry | `(patch?: Partial<TableState>) => TableState` | – | A complete state object with optional overrides. |

## Related

- [Columns](https://svelte-lean.vercel.app/table/columns.md)
- [Sorting](https://svelte-lean.vercel.app/table/sorting.md)
- [API reference](https://svelte-lean.vercel.app/table/api.md)
