Full-featured DataTable plugin for Svelar / SvelteKit 2 with Svelte 5.
- Sorting — single-click or multi-sort (Shift+click)
- Searching — global search with debounce
- Pagination — configurable per-page options, page navigation
- Selection — single, multi, range (Shift+click), select all
- Column management — visibility toggle, reorder, resize
- Editing — four modes: modal, bubble (popover), inline (double-click), excel (spreadsheet)
- Export — CSV, clipboard, print (Excel/PDF with optional deps)
- Virtual scroll — render 10,000+ rows with only visible DOM nodes
- Row grouping — group rows by any column value
- Per-column filters — programmatic column-level filtering
- Custom cells — Svelte 5 snippets for full cell rendering control
- Row customization —
rowClass,rowIdfunction, expandable detail rows - Server-side — jQuery DataTables wire protocol compatible, GET or POST
- State persistence — save sort/filter/page/column state to localStorage
- Tailwind CSS —
classNamesprop for pure Tailwind customization of every element - CSS variables — 12+ CSS custom properties for quick theming
- Footer aggregation — per-column footer with custom functions
- Responsive — horizontal scroll for small screens
# Install and publish route stubs
npx svelar plugin:install @beeblock/svelar-datatable
# Or manual install
npm install @beeblock/svelar-datatable
npx svelar plugin:publish @beeblock/svelar-datatablePeer dependencies: @beeblock/svelar >= 0.6.7, svelte ^5.0.0. exceljs is an optional peer dependency used only for Excel export.
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
import type { ColumnDef } from '@beeblock/svelar-datatable';
const columns: ColumnDef[] = [
{ key: 'id', header: 'ID', type: 'number', sortable: true },
{ key: 'name', header: 'Name', sortable: true, searchable: true },
{ key: 'email', header: 'Email', sortable: true, searchable: true },
];
const data = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
</script>
<DataTable {data} {columns} sortable searchable paginate perPage={10} /><!-- Frontend -->
<DataTable
serverUrl="/api/datatable/users"
columns={columns}
sortable
searchable
paginate
perPage={25}
/>// src/routes/api/datatable/users/+server.ts
import { DataTableController } from '@beeblock/svelar-datatable/server';
import { User } from '$lib/modules/auth/domain/models/User.js';
const dt = new DataTableController(User);
export const GET = dt.handle('query');
export const POST = dt.handle('query');The server controller handles pagination, sorting, searching, and filtering — compatible with the jQuery DataTables wire protocol.
Server search is case-insensitive on PostgreSQL (ILIKE) and uses the database driver's normal LIKE behavior on SQLite/MySQL.
Use serverParams on the component for custom server filters. GET requests send these as filters[name] query params; POST requests include them in customFilters.
<script lang="ts">
let priority = $state('');
function serverParams() {
return priority ? { priority } : {};
}
</script>
<DataTable
serverUrl="/api/datatable/cards"
columns={columns}
serverParams={serverParams}
searchable
sortable
/>Register matching filters on the server:
import { DataTableController } from '@beeblock/svelar-datatable/server';
import { Card } from '$lib/modules/boards/domain/models/Card.js';
const dt = new DataTableController(Card, {
searchable: ['title', 'description', 'priority'],
orderable: ['title', 'priority', 'updated_at'],
filters: {
priority: (query, value) => query.where('priority', value),
},
});
export const GET = dt.handle('query');When a model uses Svelar's Searchable mixin, global server search can use Meilisearch:
const dt = new DataTableController(Card, {
searchDriver: 'auto',
filters: {
priority: (query, value) => query.where('priority', value),
},
meilisearchFilter: (filters) => {
if (!filters.priority) return undefined;
return `priority = "${String(filters.priority).replaceAll('"', '\\"')}"`;
},
});searchDriver: 'auto' is the default. It tries Meilisearch for global search when the model exposes search(), then falls back to database search if Meilisearch is not configured. Use searchDriver: 'database' to force SQL search, or searchDriver: 'meilisearch' to fail instead of falling back.
Meilisearch mode returns the indexed document fields from model.search(). Keep the datatable columns in the model's searchable/displayed attributes, and mark filtered/sorted attributes in Meilisearch index settings.
The plugin is meant to stay a reusable table black box, but it exposes extension points instead of forcing app-specific forks.
customCellsnippet for custom badges, links, menus, previews, or composed app UI.buttonsfor export buttons and custom actions that receive selected rows and all rows.classNamesfor Tailwind/shadcn styling without editing plugin CSS.bind:storeReffor page-level controls such as external filters, refresh buttons, selected-row panels, or programmatic column visibility.editorFieldandeditorModefor inline, modal, bubble, or Excel-style editing.
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
import type { ButtonDef, ColumnDef } from '@beeblock/svelar-datatable';
const columns: ColumnDef[] = [
{ key: 'title', header: 'Title' },
{ key: 'priority', header: 'Priority', type: 'custom' },
{ key: 'actions', header: 'Actions', type: 'custom', sortable: false, searchable: false },
];
const buttons: ButtonDef[] = [
{
key: 'archive',
label: 'Archive selected',
disabled: (rows) => rows.length === 0,
action: (rows) => archiveCards(rows),
},
];
</script>
{#snippet customCell({ row, column, value })}
{#if column.key === 'priority'}
<span class="rounded border px-2 py-0.5 text-xs">{value}</span>
{:else if column.key === 'actions'}
<button type="button" onclick={() => openCard(row)}>Open</button>
{:else}
{value}
{/if}
{/snippet}
<DataTable {columns} data={cards} {buttons} {customCell} selectable="multi" />filtersonDataTableControllerorDataTableService.addFilter()for app-specific server filters.baseQueryfor tenant/team/user scoping.scopesfor named reusable query constraints.computedColumnsfor derived SQL columns.searchDriver,meilisearchFilter, andmeilisearchSortfor Searchable/Meilisearch-backed server search.
const dt = new DataTableController(Card, {
baseQuery: (query) => query.where('board_id', board.id),
searchable: ['title', 'description', 'priority'],
orderable: ['title', 'priority', 'position', 'updated_at'],
filters: {
priority: (query, value) => query.where('priority', value),
completed: (query, value) => query.where('completed', value === 'true'),
},
searchDriver: 'auto',
meilisearchFilter: (filters) => {
const clauses = [];
if (filters.priority) clauses.push(`priority = "${filters.priority}"`);
if (filters.completed) clauses.push(`completed = ${filters.completed === 'true'}`);
return clauses.length ? clauses.join(' AND ') : undefined;
},
});The package exposes the required Svelar plugin entry at @beeblock/svelar-datatable/plugin.
import DatatablePlugin from '@beeblock/svelar-datatable/plugin';The plugin publishes a route stub to src/routes/api/datatable/+server.ts and registers default config under the datatable config key.
Four editor modes, configured via editorMode:
<!-- Modal editor -->
<DataTable editorMode="modal" editorFields={fields} onEdit={save} onCreate={create} />
<!-- Bubble (popover anchored to row) -->
<DataTable editorMode="bubble" editorFields={fields} onEdit={save} />
<!-- Inline (double-click a cell) -->
<DataTable editorMode="inline" onCellEdit={saveCellEdit} />
<!-- Excel (spreadsheet navigation) -->
<DataTable editorMode="excel" onCellEdit={saveCellEdit} />Server-side Excel editing should still go through normal Svelar backend layers. Wire onCellEdit to a PATCH route that validates with a FormRequest, creates a DTO, runs an action/service, and returns a resource response. Throw from onCellEdit when the API rejects the update so the table can keep the old value.
<script lang="ts">
import { apiFetchJson } from '@beeblock/svelar/http';
import type { ButtonDef, ExportFormat } from '@beeblock/svelar-datatable';
async function saveCell(row, columnKey, newValue) {
const response = await apiFetchJson(`/api/datatable/cards/${row.public_id}`, {
method: 'PATCH',
body: JSON.stringify({ column: columnKey, value: newValue }),
});
if (!response.ok) {
throw new Error(response.error?.message ?? 'Failed to update row');
}
}
const buttons: (ButtonDef | ExportFormat)[] = [
'csv',
{
key: 'mark-urgent',
label: 'Mark urgent',
disabled: (rows) => rows.length === 0,
action: (rows) => Promise.all(rows.map((row) => saveCell(row, 'priority', 'urgent'))),
},
];
</script>
<DataTable
serverUrl="/api/datatable/cards"
{columns}
selectable="multi"
editorMode="excel"
onCellEdit={saveCell}
{buttons}
/>Style every element with Tailwind utility classes via the classNames prop:
<DataTable
{data}
{columns}
striped={false}
hover={false}
classNames={{
container: '!bg-slate-900 !border-slate-800',
thead: '!bg-slate-950',
th: '!bg-slate-950 !text-slate-400 !tracking-widest',
tr: 'hover:!bg-slate-800',
td: '!text-slate-300 !border-b-slate-800',
pagination: '!border-t-slate-800 !text-slate-400 !bg-slate-900',
pageButton: '!bg-slate-800 !text-slate-400 !border-slate-700',
searchInput: '!bg-slate-800 !text-slate-200 !border-slate-700',
}}
/>All 39 keys from DataTableClassNames are wired: container, toolbar, toolbarLeft, toolbarRight, searchInput, thead, th, tbody, tr, trSelected, trEven, td, tfoot, tf, pagination, paginationInfo, paginationControls, pageButton, pageButtonActive, perPageSelect, btn, btnCreate, btnEdit, btnDelete, editorModal, editorBackdrop, editorField, editorInput, editorLabel, loading, empty, error.
// UI component (Svelte source — not compiled)
import { DataTable } from '@beeblock/svelar-datatable/ui';
// Types
import type {
ColumnDef, EditorFieldDef, ButtonDef, DataTableClassNames,
DataTableConfig, DataTableState, ExportFormat,
} from '@beeblock/svelar-datatable';
// Stores
import { DataTableStore, ServerDataTableStore } from '@beeblock/svelar-datatable';
// Server controller (API routes)
import { DataTableController, DataTableService } from '@beeblock/svelar-datatable/server';Full documentation with all props, callbacks, store API, and examples: svelar.dev/docs/datatable
npm run lint
npm run test
npm run buildMIT