Table
On this page
Pass columns and rows and the table builds itself, header included. It is wrapped in a horizontal scroll container, so a table with more columns than a phone can show scrolls on its own rather than stretching the page. Exported as both table and dataTable.
Basic table
| Page | Size | Render time |
|---|---|---|
| / | 4.1 kB | 12 ms |
| /docs | 12.7 kB | 31 ms |
| /examples | 9.4 kB | 24 ms |
table({
columns: [
{ key: 'page', header: 'Page' },
{ key: 'size', header: 'Size' },
{ key: 'time', header: 'Render time' },
],
rows: [
{ page: '/', size: '4.1 kB', time: '12 ms' },
{ page: '/docs', size: '12.7 kB', time: '31 ms' },
{ page: '/examples', size: '9.4 kB', time: '24 ms' },
],
})Alignment
Numbers read better aligned to the end of their column.
| Page | Bytes | Gzip |
|---|---|---|
| / | 4,112 | 1,204 |
| /docs | 12,704 | 3,910 |
| /examples | 9,388 | 2,744 |
table({
columns: [
{ key: 'page', header: 'Page' },
{ key: 'bytes', header: 'Bytes', align: 'end' },
{ key: 'gzip', header: 'Gzip', align: 'end' },
],
rows: [
{ page: '/', bytes: '4,112', gzip: '1,204' },
{ page: '/docs', bytes: '12,704', gzip: '3,910' },
{ page: '/examples', bytes: '9,388', gzip: '2,744' },
],
})Custom cells
A column with a render function gets the whole row and returns whatever should be in the cell — a chip, a link, a formatted number.
| Page | Size | Status |
|---|---|---|
| /docs/routing | 18.2 kB | ok |
| /docs/data | 21.7 kB | ok |
| /docs/islands | 24.1 kB | failed |
table({
columns: [
{ header: 'Page', render: (row) => link({ href: row.href }, row.page) },
{ key: 'size', header: 'Size', align: 'end' },
{ header: 'Status', align: 'end', render: (row) =>
chip({ size: 'sm', dot: true, color: row.ok ? 'success' : 'danger' }, row.ok ? 'ok' : 'failed') },
],
rows: [
{ page: '/docs/routing', href: '/docs/routing', size: '18.2 kB', ok: true },
{ page: '/docs/data', href: '/docs/data', size: '21.7 kB', ok: true },
{ page: '/docs/islands', href: '/docs/islands', size: '24.1 kB', ok: false },
],
})Styles
striped bands alternate rows, hover highlights the row under the pointer, and dense tightens the padding for a table with many rows.
| Name | Value |
|---|---|
| pages | 169 |
| assets | 208 |
| total | 9.5 MB |
| Name | Value |
|---|---|
| pages | 169 |
| assets | 208 |
| total | 9.5 MB |
stack({ gap: 'lg' },
table({
striped: true,
caption: 'striped',
columns: [{ key: 'name', header: 'Name' }, { key: 'value', header: 'Value', align: 'end' }],
rows: [{ name: 'pages', value: '169' }, { name: 'assets', value: '208' }, { name: 'total', value: '9.5 MB' }],
}),
table({
hover: true,
dense: true,
caption: 'hover and dense',
columns: [{ key: 'name', header: 'Name' }, { key: 'value', header: 'Value', align: 'end' }],
rows: [{ name: 'pages', value: '169' }, { name: 'assets', value: '208' }, { name: 'total', value: '9.5 MB' }],
}),
)Caption
A caption names the table for anyone who arrives at it without the surrounding text — worth adding whenever the table is not directly under a heading that already says what it is.
| Commit | When | Pages |
|---|---|---|
| 94a837a | 4 minutes ago | 169 |
| dcfaaae | 2 hours ago | 161 |
table({
caption: 'Build output, newest first',
columns: [
{ key: 'commit', header: 'Commit' },
{ key: 'when', header: 'When' },
{ key: 'pages', header: 'Pages', align: 'end' },
],
rows: [
{ commit: '94a837a', when: '4 minutes ago', pages: '169' },
{ commit: 'dcfaaae', when: '2 hours ago', pages: '161' },
],
})From data
Rows are an ordinary array, so they are usually whatever data() already loaded — no adapter in between.
| Post | Published | Reads |
|---|---|---|
| Hello world | 2026-01-14 | 1,204 |
| Static first | 2026-02-02 | 890 |
| No runtime | 2026-03-19 | 2,317 |
return (() => {
const posts = [
{ title: 'Hello world', date: '2026-01-14', reads: 1204 },
{ title: 'Static first', date: '2026-02-02', reads: 890 },
{ title: 'No runtime', date: '2026-03-19', reads: 2317 },
]
return table({
hover: true,
columns: [
{ key: 'title', header: 'Post' },
{ key: 'date', header: 'Published' },
{ header: 'Reads', align: 'end', render: (post) => post.reads.toLocaleString('en') },
],
rows: posts,
})
})()Writing the markup yourself
Omit columns and the table renders its children instead, so a table with a footer row or grouped headers can be hand-built and still get the styling and the scroll container.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | TableColumn[] | — | { key, header, align, render } per column. Omit to hand-write the rows. |
rows | object[] | [] | One object per row. |
caption | Child | — | A caption above the table. |
striped | boolean | false | Band alternate rows. |
hover | boolean | false | Highlight the row under the pointer. |
dense | boolean | false | Tighter cell padding. |