@zhangt58/svelte-vtable
v0.4.1
Published
A Svelte 5 library for virtualized data tables with sorting, selection, and pagination controls.
Maintainers
Readme
@zhangt58/svelte-vtable
A Svelte 5 library providing virtualized data tables with sorting, selection, search, filtering, and pagination controls. Built with Svelte 5 runes for reactive state management.
Features
- 🚀 Virtualized rendering - Efficiently handles large datasets
- 🔄 Sorting - Click column headers to sort
- 🔍 Search - Built-in search filtering
- 🎯 Multi-select filters - Column-based filtering with OR/AND logic
- 📄 Pagination - Configurable page controls with ellipsis navigation
- ✅ Selection - Row selection with callback support
- 🎨 Styling - Tailwind CSS based with light/dark mode support
- 📱 Responsive - Flexible column widths and layouts
Installation
npm install @zhangt58/svelte-vtablePeer Dependencies
This library requires the following peer dependencies:
npm install svelte@^5.0.0 tailwindcss@^4.0.0 svelte-virtuallists@^1.0.0flowbite-svelte is optional — only needed if you use the Flowbite adapter (see Flowbite Adapter).
Quick Start
<script>
import { DataTable, DataTableControls, DataTableFilters } from '@zhangt58/svelte-vtable';
let items = $state([
{ id: 1, name: 'Alice', email: '[email protected]', dept: 'Engineering' },
{ id: 2, name: 'Bob', email: '[email protected]', dept: 'Sales' },
// ... more items
]);
const ui = $state({
searchQuery: '',
currentPage: 1,
perPage: 25,
sortKey: null,
sortDir: 'asc',
});
const visibleKeys = ['name', 'email', 'dept'];
const colWidths = { name: 1, email: 2, dept: 1 }; // stretch weights
// For filtering
let activeFilters = $state({});
const columnFilters = [
{ key: 'dept', label: 'Department', uniqueValues: ['Engineering', 'Sales'] },
];
</script>
<!-- Multi-select filters -->
<DataTableFilters
{columnFilters}
{activeFilters}
onfilter={({ allFilters }) => (activeFilters = allFilters)}
/>
<!-- Search and pagination controls -->
<DataTableControls
search={ui.searchQuery}
currentPage={ui.currentPage}
perPage={ui.perPage}
totalItems={items.length}
onpage={({ page }) => (ui.currentPage = page)}
onsearch={({ search }) => (ui.searchQuery = search)}
/>
<!-- Virtualized table -->
<DataTable
{items}
{visibleKeys}
bind:sortKey={ui.sortKey}
bind:sortDir={ui.sortDir}
onsort={({ key, dir }) => {
/* optionally trigger a server fetch here */
}}
onselect={({ item, index }) => {
/* handle selection */
}}
{colWidths}
>
{#snippet rowSnippet({ item, index, select, selected })}
<tr onclick={select}>
<td>{item.name}</td>
<td>{item.email}</td>
<td>{item.dept}</td>
</tr>
{/snippet}
</DataTable>Flowbite Adapter
By default, DataTableControls uses zero external UI-framework dependencies — it renders using plain <input>, <select>, <button>, and <nav> elements with Tailwind utility classes.
If your project already uses Flowbite, you can opt in to the Flowbite-flavoured controls by importing from the /flowbite entrypoint:
# Install optional Flowbite peer dependencies
npm install flowbite-svelte@^1.0.0<script>
// Import Flowbite-flavoured DataTableControls (Search, Badge, Select, Modal)
import { DataTable, DataTableControls, DataTableFilters } from '@zhangt58/svelte-vtable/flowbite';
</script>
<DataTableControls
search={searchQuery}
{currentPage}
bind:perPage
totalItems={items.length}
onpage={({ page }) => (currentPage = page)}
onsearch={({ search }) => (searchQuery = search)}
{columnFilters}
{activeFilters}
onfilter={handleFilterChange}
/>The Flowbite entrypoint re-exports everything from the default entrypoint, so you can use a single import for all components and utilities.
See examples/flowbite/App.svelte for a complete working example.
Components
DataTable (previously named VirtualDataTable)
A virtualized table component for efficient rendering of large datasets. The component was renamed to DataTable; VirtualDataTable is still exported for backward compatibility.
Props
| Prop | Type | Default | Description |
| --------------- | ----------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| items | Array | [] | Array of data items to display |
| columns | ColumnDef[] | undefined | Unified column definitions (recommended). When provided, visibleKeys, colWidths, and rowSnippet may be omitted. See ColumnDef below. |
| visibleKeys | Array | [] | (Legacy) Array of keys to display as column headers. Ignored when columns is provided. |
| sortKey | string \| null | null | Current sort key (column header); supports bind:sortKey |
| sortDir | 'asc' \| 'desc' | 'asc' | Sort direction; supports bind:sortDir |
| className | string | '' | Additional CSS classes |
| style | string | '' | Inline styles |
| emptyMessage | string | 'No items to display.' | Message when no items |
| colWidths | object \| Array | {} | (Legacy) Column width configuration (stretch weights or pixel values). Ignored when columns is provided. |
| selected | any | null | Currently selected item |
| onselect | function | undefined | Callback when a row is selected: ({item, index}) => void |
| onsort | function | undefined | Callback when sort changes: ({key, dir}) => void. When provided, local sorting is skipped (server-side sort pattern). Omit entirely (do not pass () => {}) to enable local sorting. |
| inlineFilters | boolean | false | Renders column filter buttons in table headers. Without onfilter, filters are applied locally to items. |
| columnFilters | Array \| null | null | Optional explicit filter configurations, usually from buildColumnFilters(rows, columns). Overrides derived filter options. |
| filterItems | Array \| null | null | Base data source used to derive inline filter options when columnFilters is omitted. Defaults to items; pass the unpaged/search-scoped rows when items is paged. |
| activeFilters | Object | {} | Controlled filter state for inline header filters. |
| onfilter | function | undefined | Callback when inline filters change: ({key, values, allFilters}) => void. When provided, parent code is responsible for applying filters. |
| rowSnippet | Snippet | undefined | Svelte 5 snippet for rendering rows. Optional when columns is provided (a default row is rendered using each column's cellSnippet or raw value). |
Row Snippet Parameters
The rowSnippet receives an object with:
item- The current row dataindex- Row index in the current pageselect- Function to call to select this rowselected- Currently selected item (for comparison)
ColumnDef
ColumnDef is the unified column configuration object. Import the JSDoc type via:
// @type {import('@zhangt58/svelte-vtable').ColumnDef}| Field | Type | Default | Description |
| --------------- | --------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------- |
| key | string | required | Data key. Must match a property on the row item. |
| label | string | key | Column header label. Defaults to key. |
| width | number \| string | 1 | Stretch weight (number) or CSS value (e.g. '120px'). |
| sortable | boolean | true | Whether clicking the header sorts the table. |
| filterType | 'value' \| 'list' \| 'daterange' \| 'datetimerange' \| 'none' | 'value' | Filter UI for this column. 'list' is an alias for value-list filters; 'none' excludes it from buildColumnFilters. |
| headerSnippet | Snippet<[{key, label, sortKey, sortDir}]> | undefined | Custom Svelte 5 snippet rendered inside <th>. |
| cellSnippet | Snippet<[{item, value, index}]> | undefined | Custom Svelte 5 snippet rendered inside each <td>. Used for default row rendering when rowSnippet is not provided. |
Minimal example using columns
<script>
import { DataTable } from '@zhangt58/svelte-vtable';
const items = [
{ id: 1, name: 'Alice', department: 'Engineering', hireDate: '2024-01-15' },
{ id: 2, name: 'Bob', department: 'Sales', hireDate: '2024-03-10' },
];
/** @type {import('@zhangt58/svelte-vtable').ColumnDef[]} */
const columns = [
{ key: 'id', label: 'ID', width: 1, filterType: 'none' },
{ key: 'name', label: 'Name', width: 3, filterType: 'none' },
{ key: 'department', label: 'Department', width: 2, filterType: 'value' },
{ key: 'hireDate', label: 'Hire Date', width: 2, filterType: 'daterange' },
];
</script>
<!-- No visibleKeys, colWidths, rowSnippet, or separate filters required -->
<DataTable {items} {columns} inlineFilters style="height: 400px" />Controlled inline filters
For pagination, server requests, or shared filter controls, keep filters in the parent and pass the same state to DataTable:
<script>
import { DataTable, applyFilters } from '@zhangt58/svelte-vtable';
const rows = [
{ department: 'Engineering', hireDate: '2024-01-15' },
{ department: 'Sales', hireDate: '2024-03-10' },
];
const columns = [
{ key: 'department', label: 'Department', filterType: 'value' },
{ key: 'hireDate', label: 'Hire Date', filterType: 'daterange' },
];
let activeFilters = $state({});
const optionRows = $derived(rows); // Apply search or server scoping here when needed.
const filteredRows = $derived(applyFilters(optionRows, activeFilters));
const pagedRows = $derived(filteredRows.slice(0, 25));
function handleFilter({ allFilters }) {
activeFilters = { ...allFilters };
}
</script>
<DataTable
items={pagedRows}
{columns}
inlineFilters
filterItems={optionRows}
{activeFilters}
onfilter={handleFilter}
/>When filterItems and activeFilters are provided, derived filter options are faceted per column: each column is built from filterItems after applying the other active filters, but not that column's own filter. This keeps date range bounds and value choices available when the current column filter temporarily matches zero rows.
Date range quick presets such as Last 7d are relative to the current wall-clock date/time. Presets that would match no rows in the current option source are disabled.
Column Widths
Column widths can be specified as:
- Stretch weights (numbers): Distributed proportionally, e.g.,
{ name: 1, description: 3 } - Pixel values (strings): Fixed widths, e.g.,
{ id: '80px', name: '200px' }
Row Snippet Parameters
The rowSnippet receives an object with:
item- The current row dataindex- Row index in the current pageselect- Function to call to select this rowselected- Currently selected item (for comparison)
DataTableControls
Controls component for search and pagination.
Props
| Prop | Type | Default | Description |
| ----------------- | --------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| search | string | '' | Current search query |
| currentPage | number | 1 | Current page number |
| perPage | number | 25 | Items per page |
| totalItems | number | 0 | Total number of items |
| onpage | function | undefined | Callback for page changes: ({page}) => void |
| onsearch | function | undefined | Callback for search changes: ({search}) => void |
| onfilterstoggle | function | undefined | Callback when filters panel toggled: ({visible}) => void |
| onperpage | function | undefined | Callback when per-page changes: ({perPage}) => void |
| columnFilters | Array \| null | null | Optional explicit modal filter configurations. Overrides derived filter options. |
| columns | ColumnDef[] | [] | Column definitions used with filterItems to derive modal filter options. |
| filterItems | Array \| null | null | Base data source used to derive modal filter options. Pass unpaged/search-scoped rows; active peer filters are applied per column. |
| activeFilters | Object | {} | Current active filters used for the modal state and faceted option derivation. |
| onfilter | function | undefined | Callback when filters change (passed through to DataTableFilters): ({key, values, allFilters}) => void |
DataTableFilters
Multi-select filter component with flexible layout options. Implements OR logic within columns and AND logic across columns.
Props
| Prop | Type | Default | Description |
| --------------- | ---------------------------- | -------------- | ------------------------------------------------------------------- |
| columnFilters | Array | [] | Array of filter configurations (see below) |
| direction | 'horizontal' \| 'vertical' | 'horizontal' | Layout direction for filter grid |
| activeFilters | Object | {} | Current active filters { columnKey: [selectedValues] } |
| onfilter | function | undefined | Callback when filters change: ({key, values, allFilters}) => void |
| oncolumnsort | function | undefined | Callback when column sort changes: ({key, mode, dir}) => void |
| className | string | '' | Additional CSS classes |
| showCounts | boolean | true | Whether to show value counts |
columnFilters Structure
Each item in columnFilters should have:
{
key: 'columnKey', // Column identifier
label: 'Column Label', // Display label
uniqueValues: [...], // Array of unique values
counts: { value: count } // Optional: value frequency map
}Filter Logic
- OR within column: Selecting multiple values in one filter matches rows with ANY of those values
- AND across columns: All active column filters must match for a row to pass
Example usage with data filtering:
<script>
import { DataTableFilters } from '@zhangt58/svelte-vtable';
const data = [
{ name: 'Alice', dept: 'Engineering', status: 'Active' },
{ name: 'Bob', dept: 'Sales', status: 'Active' },
// ...
];
let activeFilters = $state({});
const columnFilters = [
{
key: 'dept',
label: 'Department',
uniqueValues: ['Engineering', 'Sales'],
counts: { Engineering: 5, Sales: 3 },
},
{
key: 'status',
label: 'Status',
uniqueValues: ['Active', 'Inactive'],
counts: { Active: 7, Inactive: 1 },
},
];
// Apply filters
const filteredData = $derived(() => {
return data.filter((item) => {
for (const [key, values] of Object.entries(activeFilters)) {
if (values?.length > 0 && !values.includes(item[key])) {
return false; // AND logic across columns
}
}
return true;
});
});
</script>
<DataTableFilters
{columnFilters}
{activeFilters}
onfilter={({ allFilters }) => (activeFilters = allFilters)}
direction="horizontal"
showCounts={true}
/>For complete examples, see DATATABLEFILTERS_README.md.
Styling (important)
This library ships with built-in CSS to provide sensible default visuals (light/dark mode, striping, hover, selection, sticky headers, and pagination control styling). There are two important details to understand so the styles work correctly for both local development and package consumers:
- Source vs published CSS
- The human-editable source is
src/lib/styles.css. It is written using Svelte-style:global(...)wrappers so the selectors are clear and scoped intentionally when compiled inside Svelte components. - Bundlers and consumer projects do not process Svelte
:global(...)tokens when they load plain.cssfiles. For that reason the package publishes a compiled plain-CSS file atsrc/lib/dist/styles.css. This compiled file has the:global(...)wrappers removed so the selectors are normal CSS selectors and will match in any bundler.
- How you should import the styles as a consumer
- Recommended (installed from npm):
import '@zhangt58/svelte-vtable/styles.css';This import resolves to the precompiled src/lib/dist/styles.css via the package exports entry in package.json.
- Working inside the repo or during local development (components import the file relatively):
Components inside this package import the compiled CSS via:
@import '../lib/dist/styles.css';Do not import the raw src/lib/styles.css from components or consumers — the :global(...) wrappers will remain and selectors won't behave as intended outside the Svelte compiler.
- How :global works here and why we compile it
:global(.selector)is a Svelte compiler token used when writing styles in Svelte components. It tells the Svelte compiler to treat the selector as global instead of scoping it to the component.- When shipping plain
.cssfiles to consumers, those tokens must be converted into regular selectors. The repository contains a tiny build script (scripts/build-styles.cjs) that strips:global(...)wrappers and emits a compiledsrc/lib/dist/styles.css. This is what gets published and what consumers should import.
- Build scripts and publishing
- The project has a
build:stylesscript that generatessrc/lib/dist/styles.cssfromsrc/lib/styles.css:
npm run build:stylespackage.jsonalready runsnpm run build:stylesas part ofprepublishOnlyto ensure the compiled CSS is present before publishing.
Optional recommendation: add prepare to package.json so that npm install in development or certain CI flows will also generate the compiled CSS automatically:
"scripts": {
"build:styles": "node ./scripts/build-styles.cjs",
"prepare": "npm run build:styles",
"prepublishOnly": "npm run build:styles && npm run check"
}This is optional — the current prepublishOnly is sufficient for publishing.
Theming
svelte-vtable exposes a set of CSS custom properties (variables) as its theming contract. All accent-coloured elements (active pagination button, active filter badge, filter button active state, dual-range slider) read from these variables, so you can re-theme the library without forking source or reconfiguring Tailwind.
Default values
:root {
--vtable-color-accent: #16a34a; /* green-600 — active elements */
--vtable-color-accent-light: #dcfce7; /* green-100 — active element background */
--vtable-color-accent-border: #22c55e; /* green-500 — active element border */
--vtable-color-accent-text: #15803d; /* green-700 — active element text */
--vtable-color-danger: #dc2626; /* red-600 — destructive actions */
--vtable-color-info: #3b82f6; /* blue-500 — dual-range slider */
--vtable-radius: 0.375rem; /* rounded-md */
--vtable-row-height: 2.25rem; /* virtual-scroll row height hint */
}Overriding the theme
Set any property on :root for a global override, or on a wrapper element for a scoped override:
/* app.css — global purple theme */
:root {
--vtable-color-accent: #7c3aed;
--vtable-color-accent-light: #ede9fe;
--vtable-color-accent-border: #8b5cf6;
--vtable-color-accent-text: #6d28d9;
}<!-- Scoped to a single section of a page -->
<div
style="--vtable-color-accent: #7c3aed; --vtable-color-accent-light: #ede9fe; --vtable-color-accent-border: #8b5cf6; --vtable-color-accent-text: #6d28d9;"
>
<DataTableControls ... />
<DataTable ... />
</div>What changes with --vtable-color-accent
| Element | Before (default green) | After override |
| ------------------------------------ | ----------------------------- | --------------------------------- |
| Active pagination button | green-100 bg + green-600 text | accent-light bg + accent text |
| Active filter count badge (controls) | green-600 background | accent background |
| Active filter button border | green-500 | accent-border |
| Active filter badge (count / range) | green-600 background | accent background |
| "Show All" button | green-600 border + text | accent border + text |
The live example at examples/ThemedExample.svelte lets you switch between green, purple, orange, and rose themes at runtime.
TypeScript
The library is written in JavaScript with JSDoc annotations. Type checking is available via svelte-check.
License
MIT © zhangt58
