Via the amazing capabilities braught to us by Svelte 5 — a performant, dynamic, flexible, feature rich table. It's as simple, or as flexible as you need it to be.
Simple example on Svelte 5 Playground
Fledged out example on Svelte 5 Playground
- Columns
- Sticky
- Show/hide
- Re-order
- Resize
- Data manipulation
- "Virtual" data
- Sorting
- Select
- Filtering
- Reorderable
- Statusbar
- Panels
- Row context
- Expandable rows
- Virtual rendering
- To CSV
- Auto: Create columns based on data
On top of that, the library API is extensive, so the table can meet your needs.
bun add -D svelte-tably
<script lang='ts'>
import Table from 'svelte-tably'
const data = $state([
{ name: 'Giraffe', age: 26, email: '[email protected]' },
{ name: 'Shiboo', age: 21, email: '[email protected]' }
])
let activePanel = $state('columns') as string | undefined
let selected = $state([]) as typeof data
</script>
<!-- Auto: Generate Columns based on data properties -->
<Table auto {data} resizeable={false} filters={[...]} />
<Table {data} panel={activePanel} select bind:selected>
{#snippet content({ Column, Panel, Expandable, Row, state, table })}
<Column id='name' sticky sort value={r => r.name} filter={v => v.includes('Giraffe')}>
{#snippet header(ctx)}
Name
{/snippet}
{#snippet row(row, ctx)}
{row.name}
{/snippet}
{#snippet statusbar(ctx)}
{table.data.length}
{/snippet}
</Column>
<!-- Simplified -->
<Column id='age' header='Age' value={r => r.age} sort={(a,b) => a - b} />
<Expandable click={false}>
{#snippet content(item, ctx)}
...
{/snippet}
</Expandable>
<Row onclick={...} oncontextmenu={...}>
{#snippet contextHeader()}
<button ...> <Icon icon='add' /> </button>
{/snippet}
{#snippet context(item, ctx)}
<button ...> <Icon icon='menu' /> </button>
{/snippet}
</Row>
<Panel id='columns'>
<!-- Anything you might like -->
</Panel>
<Panel ... backdrop={false}>
...
</Panel>
{/snippet}
</Table>
For quick styling
CSS Variable | Description | Default |
---|---|---|
--tably-bg | Background color | hsl(0, 0%, 100%) |
--tably-color | Text color | hsl(0, 0%, 0%) |
--tably-border | Border for sticky columns and header | hsl(0, 0%, 90%) |
--tably-border-grid | Border for the table-grid | hsl(0, 0%, 98%) |
--tably-statusbar | background-color for the statusbar | hsl(0, 0%, 98%) |
--tably-padding-y | Padding above/below each column | .5rem |
--tably-padding-x | Padding left of each column | 1rem |
--tably-radius | Table radius | .25rem |
Tip
For the CSS variables, apply them to :global(:root) { ... }
Note
Advanced styling can be done via :global(.svelte-tably)
table > thead > tr > th, table > tbody > tr > td, table > tfoot > tr > td
All components except Table are meant to be children of the Table
component.
However, you can safely create a Component.svelte
and use these components,
and then provide <Component/>
as a child to <Table>
.
import Table from 'svelte-tably'
The table component.
<Table auto {data} />
<Table {data} ...>
{#snippet content?({ Column, Row, Expandable, Panel, table })}
...
{/snippet}
</Table>
Where table
is TableState<T>
and the rest are typed; Component<T>
.
Attribute | Description | Type |
---|---|---|
content? | The contents of the table | Snippet<[ctx: ContentCtx<T>]>? |
id? | The #id for the table |
string |
data | An array of objects for the table | T[] |
bind:selected? | The currently selected items | T[] |
bind:panel? | The currently open panel | string |
filters? | An array of filters applied to the table | ((item: T) => boolean)[] |
reorderable? | Whether the rows can be re-ordered (via runic-reorder) | boolean |
resizeable? | Whether or not the columns can be resized | boolean |
select? | Whether ot not the rows items can be selected | boolean | SelectOptions<T> |
auto? | Create missing columns automatically? | boolean |
Properties | Description | Type |
---|---|---|
show? | When to show the row-select when not selected | 'hover' | 'always' | 'never' |
headerSnippet? | Custom snippet for the header select-input | Snippet<[context: HeaderSelectCtx]> |
rowSnippet? | Custom snippet for the row select-input | Snippet<[context: RowSelectCtx<T>]> |
import { Column } from 'svelte-tably'
This component designates a column where options like sorting, filtering etc. are provided.
<Column id='...' header='...' value={row => row.value} />
<Column id='...' ...>
{#snippet header?(ctx: HeaderCtx<T>)}
...
{/snippet}
{#snippet row?(item: T, ctx: RowColumnCtx<T>)}
...
{/snippet}
{#snippet statusbar?(ctx: StatusbarCtx<T>)}
...
{/snippet}
</Column>
Attribute | Description | Type |
---|---|---|
header? | The header element/contents | string | Snippet<[ctx: HeaderCtx<T>]> |
row? | The row element. If not provided, value: V will be used. |
Snippet<[item: T, ctx: RowColumnCtx<T, V>]> |
statusbar? | The statusbar element | Snippet<[ctx: StatusbarCtx<T>]> |
sticky? | Should be sticky by default | boolean |
show? | Should be visible by default | boolean |
sortby? | Should sort by this by default | boolean |
width? | Default width | number |
value? | The value this column contains | (item: T) => V |
sort? | A boolean (localeCompare ) or sorting function |
boolean | ((a: V, b: V) => number) |
resizeable? | Whether this column is resizeable | boolean |
filter? | A filter for this columns value | (item: V) => boolean |
style? | Styling the td (row-column) element |
string |
pad? | Apply padding to the child-element of td /th instead of the column element itself |
'row' | 'header' | 'both' |
onclick? | When the column is clicked | (event: MouseEvent, ctx: RowColumnCtx<T, V>) => void |
import { Row } from 'svelte-tably'
This component can add a context-menu on the side of each row, as well as provide event handlers to the row element.
<Row ... />
<Row ...>
{#snippet context?(item: T, ctx: RowCtx<T>)}
...
{/snippet}
{#snippet contextHeader?()}
...
{/snippet}
</Row>
Attribute | Description | Type |
---|---|---|
context? | A sticky column on the right for each row | Snippet<[item: T, ctx: RowCtx<T>]> |
contextHeader? | A sticky column on the right for the header | Snippet<[item: T, ctx: RowCtx<T>]> |
contextOptions? | Options for the Context-column | ContextOptions<T> |
onclick? | When row is clicked | (event: MouseEvent, ctx: RowCtx<T>) => void |
oncontextmenu? | When row is right-clicked | (event: MouseEvent, ctx: RowCtx<T>) => void |
Properties | Description | Type |
---|---|---|
hover? | Only show when hovering? | boolean |
width? | The width for the context-column | string |
import { Expandable } from 'svelte-tably'
This component gives your rows the ability to be expanded.
<Expandable ...>
{#snippet content(item: T, ctx: RowCtx<T>)}
...
{/snippet}
</Expandable>
Attribute | Description | Type |
---|---|---|
content | The contents of the expanded row. | Snippet<[item: T, ctx: RowCtx<T>]> |
slide? | Options for sliding the expanding part | { duration?: number, easing?: EasingFunction } |
click? | Whether you can click on a row to expand/collapse it | boolean |
chevron? | Whether to show the chevron on the left fixed column | 'always' | 'hover' | 'never' |
multiple? | Can multiple rows be open at the same time? | boolean |
import { Panel } from 'svelte-tably'
This component creates a panel that can be opened on the side of the table.
<Panel id='...' ...>
{#snippet children(ctx: PanelCtx<T>)}
...
{/snippet}
</Panel>
Attribute | Description | Type |
---|---|---|
children | The contents of the panel | Snippet<[ctx: PanelCtx<T>]> |
id | The id for the panel that determines whether it's open or closed, from the Table attribute | string |
backdrop? | Whether there should be a backdrop or not | boolean |