DataTable Plugin
A full-featured DataTable plugin for Svelar/SvelteKit with sorting, searching, pagination, inline/modal/bubble/excel editing, export buttons, virtual scrolling, row grouping, and server-side processing out of the box.
Package: @beeblock/svelar-datatable
Install:
npx svelar plugin:install @beeblock/svelar-datatable
Imports:
// UI component
import { DataTable } from '@beeblock/svelar-datatable/ui';
// Types
import type { ColumnDef, EditorFieldDef, ButtonDef, DataTableClassNames } from '@beeblock/svelar-datatable';
// Stores (advanced usage)
import { DataTableStore, ServerDataTableStore } from '@beeblock/svelar-datatable';
// Server-side controller (API routes)
import { DataTableController } from '@beeblock/svelar-datatable/server';
Quick Start
Client-Side (Minimal)
Pass columns and data directly. All sorting, filtering, and pagination happen in the browser.
<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', searchable: true },
{ key: 'email', header: 'Email', searchable: true },
];
const data = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' },
];
</script>
<DataTable {columns} {data} />
Server-Side (Minimal)
Pass serverUrl to fetch data from your API. The component handles pagination, sorting, and search sync with the server automatically.
<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' },
{ key: 'name', header: 'Name', searchable: true },
{ key: 'email', header: 'Email', searchable: true },
];
</script>
<DataTable {columns} serverUrl="/api/users" />
Column Definition (ColumnDef)
Each column is defined with a ColumnDef object. All properties except key and header are optional.
| Property | Type | Default | Description |
|---|---|---|---|
key |
string |
required | The object property to display |
header |
string |
required | Column header label |
type |
ColumnType |
'string' |
Data type for sorting/casting |
sortable |
boolean |
true |
Whether this column can be sorted |
searchable |
boolean |
true |
Whether global search includes this column |
filterable |
boolean |
false |
Whether per-column filtering is enabled |
visible |
boolean |
true |
Whether the column is initially visible |
width |
string |
— | CSS width (e.g. '200px', '20%') |
minWidth |
string |
— | CSS min-width |
maxWidth |
string |
— | CSS max-width |
className |
string |
— | CSS class applied to each cell in this column |
headerClassName |
string |
— | CSS class applied to the header cell |
orderable |
boolean |
true |
Whether the column can be reordered via drag |
defaultSort |
'asc' | 'desc' |
— | Apply a default sort when the table initializes |
footer |
string | ((rows) => string | number) |
— | Static text or computed footer value |
editable |
boolean |
true |
Whether the cell is editable (inline/excel modes) |
editorField |
EditorFieldDef |
— | Field definition for modal/bubble editor forms |
Column Types
'string'— default; sorted alphabetically vialocaleCompare'number'— sorted numerically; casts values toNumber'date'— sorted by date timestamp'boolean'— sorted as0/1; excel mode accepts'true'/'1''html'— renders raw HTML in the cell (use with caution)'custom'— use with thecustomCellsnippet for full control
Footer Functions
A column footer can be a string or a function that receives all filtered rows:
const columns: ColumnDef[] = [
{ key: 'name', header: 'Product' },
{
key: 'price',
header: 'Price',
type: 'number',
footer: (rows) => {
const total = rows.reduce((sum, r) => sum + (r.price ?? 0), 0);
return `Total: $${total.toFixed(2)}`;
},
},
{
key: 'quantity',
header: 'Qty',
type: 'number',
footer: (rows) => rows.reduce((sum, r) => sum + (r.quantity ?? 0), 0),
},
];
Data Sources
Client-Side
Pass a data array directly. All sorting, filtering, searching, and pagination are handled entirely in the browser.
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
let users = $state([
{ id: 1, name: 'Alice', role: 'Admin' },
{ id: 2, name: 'Bob', role: 'User' },
]);
const columns = [
{ key: 'id', header: 'ID', type: 'number' as const },
{ key: 'name', header: 'Name' },
{ key: 'role', header: 'Role' },
];
</script>
<DataTable {columns} data={users} />
If the data prop changes reactively, the table re-renders with the new data.
Server-Side
Pass serverUrl to fetch data from a remote endpoint. The component sends requests using the jQuery DataTables wire protocol, making it compatible with many existing server-side implementations.
<DataTable
{columns}
serverUrl="/api/users"
serverMethod="POST"
perPage={25}
/>
Props:
serverUrl— the endpoint URLserverMethod—'GET'(default) sends parameters as query strings;'POST'sends a JSON bodycsrfCookieName— cookie name to read the CSRF token from (default:'XSRF-TOKEN')csrfHeaderName— header name to send the CSRF token as (default:'X-CSRF-Token')
The CSRF token is automatically read from the XSRF-TOKEN cookie and sent in the X-CSRF-Token header. This matches the Svelar CSRF convention out of the box. Override with csrfCookieName and csrfHeaderName if your backend uses different names.
Server-Side Route Handler
Use DataTableController from @beeblock/svelar-datatable/server to create an API route:
// src/routes/api/users/+server.ts
import { DataTableController } from '@beeblock/svelar-datatable/server';
import { User } from '$lib/models/User';
const ctrl = new DataTableController(User);
export const GET = ctrl.handle('query');
export const POST = ctrl.handle('query');
DataTableServiceOptions
Pass options to the DataTableController constructor or construct a DataTableService directly for advanced use:
import { DataTableController } from '@beeblock/svelar-datatable/server';
import { User } from '$lib/models/User';
const ctrl = new DataTableController(User, {
// Only these columns are searchable
searchable: ['name', 'email'],
// Only these columns can be sorted
orderable: ['name', 'email', 'created_at'],
// Base query applied to every request (e.g. scoping to active users)
baseQuery: (query) => query.where('active', true),
// Named scopes you can activate
scopes: {
admins: (query) => query.where('role', 'admin'),
recent: (query) => query.where('created_at', '>', '2025-01-01'),
},
// Computed columns added as SQL expressions
computedColumns: {
full_name: "first_name || ' ' || last_name",
},
});
export const GET = ctrl.handle('query');
export const POST = ctrl.handle('query');
Advanced: DataTableService API
For full control, use DataTableService directly:
import { DataTableService } from '@beeblock/svelar-datatable/server';
import { parseDataTableRequest } from '@beeblock/svelar-datatable/server';
import { User } from '$lib/models/User';
export async function GET(event) {
const request = parseDataTableRequest(event.url.searchParams);
const service = new DataTableService(User);
service
.searchable(['name', 'email'])
.orderable(['name', 'created_at'])
.setBaseQuery((q) => q.where('active', true))
.addScope('admins', (q) => q.where('role', 'admin'))
.applyScope('admins')
.addComputedColumn('full_name', "first_name || ' ' || last_name");
const response = await service.process(request);
return new Response(JSON.stringify(response), {
headers: { 'Content-Type': 'application/json' },
});
}
Features
Sorting
Enabled by default. Click a column header to sort ascending, click again for descending, click a third time to remove the sort. Hold Shift and click another header for multi-column sorting.
<DataTable
{columns}
{data}
sortable={true}
onSort={(sort) => {
console.log('Current sort state:', sort);
}}
/>
Disable sorting on a specific column:
{ key: 'actions', header: 'Actions', sortable: false }
Searching
A global search input appears in the toolbar when searchable is enabled (default: true). It filters across all columns where searchable is not false.
<DataTable
{columns}
{data}
searchable={true}
searchDebounceMs={300}
/>
Exclude a column from global search:
{ key: 'avatar', header: 'Avatar', searchable: false }
Pagination
Enabled by default. Control the number of rows per page and the available options.
<DataTable
{columns}
{data}
paginate={true}
perPage={25}
perPageOptions={[10, 25, 50, 100]}
onPageChange={(page, perPage) => {
console.log(`Page ${page}, showing ${perPage} rows`);
}}
/>
Pagination works identically for both client-side and server-side modes — the server automatically receives start and length parameters.
Selection
Enable row selection with the selectable prop. Supports single selection, multi-selection, and Shift+click range selection.
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
import type { DataTableStore } from '@beeblock/svelar-datatable';
let store: DataTableStore | undefined = $state();
function handleSelect(selectedRows: any[]) {
console.log('Selected:', selectedRows);
}
function getSelection() {
if (store) {
const rows = store.getSelectedRows();
console.log('Currently selected:', rows);
}
}
</script>
<DataTable
{columns}
{data}
selectable="multi"
onSelect={handleSelect}
bind:storeRef={store}
/>
<button onclick={getSelection}>Get Selected</button>
Selection modes:
"none"— no selection (default)"single"— only one row at a time"multi"— multiple rows; Shift+click for range selection
Row Grouping
Group rows by a column value. Grouped rows display a styled header row for each group.
<DataTable {columns} {data} groupBy="department" />
Virtual Scroll
For large datasets (5000+ rows), enable virtual scrolling to only render visible rows in the DOM. Best used with paginate={false}.
<DataTable
{columns}
{data}
virtualScroll={true}
virtualRowHeight={48}
paginate={false}
/>
Row Expand
Show expandable detail rows beneath each row. Provide an expandContent snippet for custom content.
<DataTable {columns} {data} expandable={true}>
{#snippet expandContent({ row })}
<div style="padding: 1rem;">
<h4>Details for {row.name}</h4>
<p>Email: {row.email}</p>
<p>Created: {row.created_at}</p>
</div>
{/snippet}
</DataTable>
Custom Cell Rendering
Use the customCell snippet to render any column with custom markup. The snippet receives { row, column, value }.
<DataTable {columns} {data}>
{#snippet customCell({ row, column, value })}
{#if column.key === 'status'}
<span
class="badge"
style:background={value === 'active' ? '#22c55e' : '#ef4444'}
style:color="white"
style:padding="0.125rem 0.5rem"
style:border-radius="9999px"
style:font-size="0.75rem"
>
{value}
</span>
{:else if column.key === 'price'}
<span>${Number(value).toFixed(2)}</span>
{:else}
{value}
{/if}
{/snippet}
</DataTable>
For columns that use the customCell snippet, set type: 'custom' to avoid default rendering:
{ key: 'status', header: 'Status', type: 'custom' }
Row Appearance
Control row styling with several props:
<DataTable
{columns}
{data}
striped={true}
hover={true}
compact={false}
bordered={false}
rowId="id"
rowClass={(row, index) => {
if (row.status === 'urgent') return 'row-urgent';
return '';
}}
/>
Props:
striped— alternating row background (default:true)hover— highlight rows on hover (default:true)compact— reduced cell padding (default:false)bordered— borders around every cell (default:false)rowId— string key (e.g.'id') or function(row) => string | numberto uniquely identify each rowrowClass— static CSS class string or function(row, index) => string
Unstyled Mode
Strip all built-in CSS and apply your own classes via the classNames prop. Ideal for Tailwind CSS projects.
<DataTable
{columns}
{data}
unstyled={true}
classNames={{
container: 'bg-white rounded-lg border border-gray-200',
toolbar: 'flex items-center justify-between p-4',
searchInput: 'border rounded px-3 py-1.5 text-sm',
table: 'w-full',
thead: 'bg-gray-50',
th: 'px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase',
tbody: '',
tr: 'border-b border-gray-100',
trSelected: 'bg-blue-50',
td: 'px-4 py-3 text-sm text-gray-700',
pagination: 'flex items-center justify-between px-4 py-3',
pageButton: 'px-3 py-1 rounded text-sm',
pageButtonActive: 'bg-blue-500 text-white',
btn: 'px-3 py-1.5 rounded text-sm font-medium',
btnCreate: 'bg-blue-500 text-white hover:bg-blue-600',
btnEdit: 'bg-yellow-500 text-white hover:bg-yellow-600',
btnDelete: 'bg-red-500 text-white hover:bg-red-600',
editorModal: 'fixed inset-0 z-50 flex items-center justify-center',
editorBackdrop: 'fixed inset-0 bg-black/50',
editorField: 'mb-4',
editorInput: 'w-full border rounded px-3 py-2 text-sm',
editorLabel: 'block text-sm font-medium text-gray-700 mb-1',
loading: 'text-center py-8 text-gray-400',
empty: 'text-center py-8 text-gray-400',
error: 'bg-red-50 text-red-600 px-4 py-2 text-sm',
}}
/>
Column Reordering
Reorder columns programmatically via the store:
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
import type { DataTableStore } from '@beeblock/svelar-datatable';
let store: DataTableStore | undefined = $state();
function reorder() {
store?.reorderColumns(['email', 'name', 'id']);
}
</script>
<DataTable {columns} {data} bind:storeRef={store} />
<button onclick={reorder}>Reorder Columns</button>
Column Visibility
Toggle column visibility via the store or the built-in column toggle dropdown in the toolbar.
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
import type { DataTableStore } from '@beeblock/svelar-datatable';
let store: DataTableStore | undefined = $state();
// Toggle a single column
function toggleEmail() {
store?.toggleColumnVisibility('email');
}
// Set all at once
function showOnlyNameAndId() {
store?.setColumnVisibility({ id: true, name: true, email: false });
}
</script>
<DataTable {columns} {data} bind:storeRef={store} />
State Persistence
Save and restore sort, filters, search, page, column visibility, and column order to localStorage:
<DataTable
{columns}
{data}
stateSaveKey="users-table-state"
/>
The state is automatically saved on every change and restored when the component mounts with the same key.
Export Buttons
Add export and action buttons to the toolbar.
<DataTable
{columns}
{data}
buttons={['csv', 'clipboard', 'print']}
/>
Built-in export formats: 'csv', 'clipboard', 'print', 'excel', 'pdf'.
For custom buttons, use ButtonDef objects:
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
import type { ButtonDef } from '@beeblock/svelar-datatable';
const buttons: (ButtonDef | string)[] = [
'csv',
'clipboard',
{
key: 'archive',
label: 'Archive Selected',
variant: 'destructive',
disabled: (selectedRows) => selectedRows.length === 0,
action: async (selectedRows) => {
await fetch('/api/users/archive', {
method: 'POST',
body: JSON.stringify({ ids: selectedRows.map((r) => r.id) }),
});
},
},
];
</script>
<DataTable {columns} {data} selectable="multi" {buttons} />
ButtonDef interface:
| Property | Type | Description |
|---|---|---|
key |
string |
Unique identifier |
label |
string |
Button text |
icon |
Component |
Optional Svelte icon component |
action |
string | ((selectedRows, allData) => void | Promise<void>) |
Click handler or built-in action name |
variant |
'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' |
Visual style |
disabled |
boolean | ((selectedRows) => boolean) |
Static or dynamic disabled state |
collection |
ButtonDef[] |
Nested dropdown buttons |
Editor Modes
Modal Editor (editorMode="modal")
Opens a full-form modal dialog for creating, editing, and deleting records.
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
import type { ColumnDef, EditorFieldDef } from '@beeblock/svelar-datatable';
const columns: ColumnDef[] = [
{ key: 'id', header: 'ID', type: 'number', editable: false },
{ key: 'name', header: 'Name' },
{ key: 'email', header: 'Email' },
{ key: 'role', header: 'Role' },
];
const editorFields: EditorFieldDef[] = [
{ name: 'name', type: 'text', label: 'Name', required: true, placeholder: 'Full name' },
{ name: 'email', type: 'text', label: 'Email', required: true, placeholder: 'user@example.com' },
{
name: 'role',
type: 'select',
label: 'Role',
options: [
{ label: 'Admin', value: 'admin' },
{ label: 'Editor', value: 'editor' },
{ label: 'Viewer', value: 'viewer' },
],
},
];
let data = $state([
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' },
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'viewer' },
]);
async function handleCreate(formData: Record<string, any>) {
const res = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(formData),
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) {
const body = await res.json();
// Throw errors to show validation messages in the editor
throw { errors: body.errors };
}
const user = await res.json();
data = [...data, user];
}
async function handleEdit(row: any, formData: Record<string, any>) {
const res = await fetch(`/api/users/${row.id}`, {
method: 'PUT',
body: JSON.stringify(formData),
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) {
const body = await res.json();
throw { errors: body.errors };
}
const updated = await res.json();
data = data.map((u) => (u.id === row.id ? updated : u));
}
async function handleDelete(rows: any[]) {
const ids = rows.map((r) => r.id);
await fetch('/api/users', {
method: 'DELETE',
body: JSON.stringify({ ids }),
});
data = data.filter((u) => !ids.includes(u.id));
}
</script>
<DataTable
{columns}
{data}
editorMode="modal"
{editorFields}
onCreate={handleCreate}
onEdit={handleEdit}
onDelete={handleDelete}
selectable="multi"
/>
Bubble Editor (editorMode="bubble")
A floating popover form that anchors to the selected row. Provides the same form fields as the modal editor but in a compact popover. Click the backdrop to close.
<DataTable
{columns}
{data}
editorMode="bubble"
{editorFields}
onCreate={handleCreate}
onEdit={handleEdit}
onDelete={handleDelete}
/>
Uses the same editorFields, onCreate, onEdit, and onDelete callbacks as the modal editor.
Inline Editor (editorMode="inline")
Double-click any editable cell to edit its value in place. Press Enter to save, Escape to cancel. Blurring the input also saves.
The New and Edit buttons in the toolbar fall back to a modal form using the editorFields configuration.
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
const columns = [
{ key: 'id', header: 'ID', type: 'number' as const, editable: false },
{ key: 'name', header: 'Name', editable: true },
{ key: 'price', header: 'Price', type: 'number' as const, editable: true },
];
let data = $state([
{ id: 1, name: 'Widget', price: 9.99 },
{ id: 2, name: 'Gadget', price: 19.99 },
]);
async function handleCellEdit(row: any, columnKey: string, newValue: any, oldValue: any) {
await fetch(`/api/products/${row.id}`, {
method: 'PATCH',
body: JSON.stringify({ [columnKey]: newValue }),
headers: { 'Content-Type': 'application/json' },
});
}
</script>
<DataTable
{columns}
{data}
editorMode="inline"
onCellEdit={handleCellEdit}
/>
Excel Mode (editorMode="excel")
Spreadsheet-style navigation and editing. Click a cell to focus it, press Enter or start typing to edit, use Arrow keys to navigate, Tab to move right, and Escape to cancel.
When navigating past the last row on a page, the table automatically moves to the next page (and vice versa for the first row).
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
const columns = [
{ key: 'id', header: 'ID', type: 'number' as const, editable: false },
{ key: 'name', header: 'Name', editable: true },
{ key: 'quantity', header: 'Qty', type: 'number' as const, editable: true },
{ key: 'price', header: 'Price', type: 'number' as const, editable: true },
];
let data = $state([
{ id: 1, name: 'Widget', quantity: 10, price: 9.99 },
{ id: 2, name: 'Gadget', quantity: 5, price: 19.99 },
{ id: 3, name: 'Doohickey', quantity: 8, price: 14.99 },
]);
async function handleCellEdit(row: any, columnKey: string, newValue: any, oldValue: any) {
// For server-side mode, if this throws, the cell value is automatically reverted
const res = await fetch(`/api/products/${row.id}`, {
method: 'PATCH',
body: JSON.stringify({ [columnKey]: newValue }),
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) throw new Error('Failed to save');
}
</script>
<DataTable
{columns}
{data}
editorMode="excel"
onCellEdit={handleCellEdit}
perPage={50}
/>
Excel mode keyboard shortcuts:
| Key | Action |
|---|---|
| Click | Focus cell |
| Enter | Start editing / confirm edit |
| Escape | Cancel editing |
| Arrow keys | Navigate between cells |
| Tab | Move to next editable cell |
| Type any character | Start editing with that character |
EditorFieldDef
Each field in the editor form (modal or bubble) is defined with an EditorFieldDef:
| Property | Type | Description |
|---|---|---|
name |
string |
Form field name (matches column key) |
type |
FieldType |
Input type |
label |
string |
Display label |
placeholder |
string |
Input placeholder text |
options |
{ label: string; value: any }[] |
Options for select, multi-select, radio fields |
multiple |
boolean |
Allow multiple selections (multi-select) |
required |
boolean |
Mark field as required |
disabled |
boolean |
Disable the field |
className |
string |
CSS class for the field wrapper |
dependsOn |
string |
Name of another field this depends on |
dependsOnValue |
any |
Only show when the depended field has this value |
showWhen |
(formData) => boolean |
Dynamic visibility function |
defaultValue |
any |
Default value for new records |
Available field types:
'text', 'textarea', 'number', 'select', 'multi-select', 'checkbox', 'radio', 'date', 'datetime', 'upload', 'hidden', 'readonly'
Conditional Fields
const editorFields: EditorFieldDef[] = [
{
name: 'type',
type: 'select',
label: 'Account Type',
options: [
{ label: 'Individual', value: 'individual' },
{ label: 'Company', value: 'company' },
],
},
{
name: 'company_name',
type: 'text',
label: 'Company Name',
// Only show when type is 'company'
dependsOn: 'type',
dependsOnValue: 'company',
},
{
name: 'tax_id',
type: 'text',
label: 'Tax ID',
// Or use showWhen for more complex logic
showWhen: (formData) => formData.type === 'company' && formData.country === 'US',
},
];
Callbacks Reference
| Callback | Signature | Used by |
|---|---|---|
onSort |
(sort: SortState[]) => void |
All modes |
onFilter |
(filters: FilterState[]) => void |
All modes |
onPageChange |
(page: number, perPage: number) => void |
All modes |
onSelect |
(selectedRows: T[]) => void |
All modes with selection |
onRowClick |
(row: T, event: MouseEvent) => void |
All modes |
onEdit |
(row: T, data: Record<string, any>) => void | Promise<void> |
Modal, bubble editors |
onCreate |
(data: Record<string, any>) => void | Promise<void> |
Modal, bubble editors |
onDelete |
(rows: T[]) => void | Promise<void> |
Modal, bubble editors |
onCellEdit |
(row: T, columnKey: string, newValue: any, oldValue: any) => void | Promise<void> |
Inline, excel editors |
For onEdit and onCreate, throw an object with errors to display validation messages:
async function handleEdit(row, formData) {
const res = await fetch(`/api/users/${row.id}`, {
method: 'PUT',
body: JSON.stringify(formData),
});
if (!res.ok) {
const body = await res.json();
throw { errors: body.errors };
// e.g. { errors: { email: 'Email is already taken' } }
}
}
Store API (DataTableStore)
Access the store via bind:storeRef:
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
import type { DataTableStore } from '@beeblock/svelar-datatable';
let store: DataTableStore | undefined = $state();
</script>
<DataTable {columns} {data} bind:storeRef={store} />
Data
| Method | Description |
|---|---|
setData(rows: T[]) |
Replace all rows (client-side only) |
getState(): DataTableState |
Get the full current state snapshot |
subscribe(listener: () => void): () => void |
Subscribe to state changes; returns unsubscribe function |
setLoading(loading: boolean) |
Set loading state |
setError(error: string | null) |
Set or clear error message |
resetState() |
Clear sort, filters, search, selection; reset page to 1 |
Sort and Filter
| Method | Description |
|---|---|
setSort(sort: SortState[]) |
Set sort state directly |
toggleSort(column: string, multiSort?: boolean) |
Toggle sort on a column; pass true for multi-sort |
setGlobalSearch(search: string) |
Set global search term |
setFilters(filters: FilterState[]) |
Replace all filters |
setColumnFilter(column, value, operator?) |
Set or clear a single column filter |
Pagination
| Method | Description |
|---|---|
setPage(page: number) |
Navigate to a page (clamped to valid range) |
setPerPage(perPage: number) |
Change rows per page; resets to page 1 |
Selection
| Method | Description |
|---|---|
toggleSelect(rowId) |
Toggle a row's selection |
selectSingle(rowId) |
Select only this row |
selectAll() |
Select all filtered rows |
deselectAll() |
Clear all selections |
selectRange(fromId, toId) |
Select a range of rows between two IDs |
getSelectedRows(): T[] |
Get currently selected row objects |
Editor
| Method | Description |
|---|---|
openEditor(rowId, column, mode) |
Open editor for a row (or null for create) |
closeEditor() |
Close the editor |
setFormField(name, value) |
Update a form field value |
setValidationErrors(errors) |
Set validation error messages |
Excel Mode
| Method | Description |
|---|---|
focusCell(rowIndex, columnKey) |
Focus a specific cell |
startCellEdit() |
Enter edit mode on the focused cell |
commitCellEdit() |
Save the current cell edit; returns change object or null |
cancelCellEdit() |
Cancel the current cell edit |
clearExcelFocus() |
Remove all excel focus/edit state |
navigateCell(direction) |
Move focus; returns 'page-next', 'page-prev', or null |
Column
| Method | Description |
|---|---|
toggleColumnVisibility(key) |
Toggle a column's visibility |
setColumnVisibility(record) |
Set visibility for multiple columns at once |
reorderColumns(columnOrder: string[]) |
Set column order |
getVisibleColumns(): ColumnDef[] |
Get currently visible columns in order |
ServerDataTableStore
Extends DataTableStore for server-side data fetching. Created automatically when you pass serverUrl to the DataTable component. You generally do not need to instantiate it directly.
Key differences from DataTableStore:
setSort,setGlobalSearch,setFilters,setPage,setPerPageall trigger a server fetch instead of local recomputation- Sort/search/filter changes are debounced (300ms) to avoid excessive requests
- Page changes are immediate (no debounce)
- Concurrent requests are automatically aborted (only the latest response is applied)
Additional methods:
| Method | Description |
|---|---|
initialFetch() |
Perform the first data fetch (called automatically on mount) |
destroy() |
Abort pending requests and clean up timers |
CSS Customization
CSS Variables
Override the built-in styles with CSS custom properties:
:root {
--sdt-bg: #ffffff;
--sdt-border: #e5e7eb;
--sdt-primary: #3b82f6;
--sdt-text: #111827;
--sdt-text-muted: #6b7280;
--sdt-hover: #f9fafb;
--sdt-cell-padding: 0.75rem 1rem;
--sdt-font: inherit;
--sdt-row-stripe: rgba(0, 0, 0, 0.02);
--sdt-row-hover: #f9fafb;
}
classNames Prop
For full control (especially with Tailwind CSS), use the classNames prop. See the DataTableClassNames interface:
interface DataTableClassNames {
container?: string;
toolbar?: string;
toolbarLeft?: string;
toolbarRight?: string;
searchInput?: string;
table?: string;
thead?: string;
th?: string;
thSortable?: string;
tbody?: string;
tr?: string;
trSelected?: string;
trEven?: string;
td?: string;
tfoot?: string;
tf?: string;
pagination?: string;
paginationInfo?: string;
paginationControls?: string;
pageButton?: string;
pageButtonActive?: string;
perPageSelect?: string;
btn?: string;
btnCreate?: string;
btnEdit?: string;
btnDelete?: string;
editorModal?: string;
editorBackdrop?: string;
editorField?: string;
editorInput?: string;
editorLabel?: string;
loading?: string;
empty?: string;
error?: string;
}
Tailwind Dark Theme Example
Use the classNames prop with Tailwind utility classes to completely restyle the table. Pure Tailwind — no custom CSS, no CSS variables, no :global() hacks:
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
import type { ColumnDef } from '@beeblock/svelar-datatable';
const columns: ColumnDef[] = [
{ key: 'id', header: '#', type: 'number', sortable: true, width: '50px' },
{ key: 'name', header: 'Full Name', sortable: true, searchable: true },
{ key: 'email', header: 'Email', sortable: true, searchable: true },
{ key: 'role', header: 'Role', sortable: true },
{ key: 'status', header: 'Status', sortable: true },
{
key: 'salary', header: 'Salary', type: 'number', sortable: true,
footer: (rows) => `Total: $${rows.reduce((sum, r) => sum + r.salary, 0).toLocaleString()}`
},
];
const data = [
{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin', status: 'Active', salary: 95000 },
{ id: 2, name: 'Bob Smith', email: 'bob@example.com', role: 'Editor', status: 'Active', salary: 72000 },
{ id: 3, name: 'Charlie Brown', email: 'charlie@example.com', role: 'Viewer', status: 'Inactive', salary: 55000 },
];
</script>
{#snippet customCell({ row, column, value }: { row: any; column: any; value: any })}
{#if column.key === 'status'}
<span class="inline-flex items-center gap-1 text-xs font-semibold uppercase
{value === 'Active' ? 'text-emerald-400' : 'text-red-400'}">
<span class="w-1.5 h-1.5 rounded-full
{value === 'Active' ? 'bg-emerald-400' : 'bg-red-400'}"></span>
{value}
</span>
{:else if column.key === 'role'}
<span class="px-2 py-0.5 rounded text-xs font-medium
{value === 'Admin'
? 'bg-emerald-500/20 text-emerald-300 ring-1 ring-emerald-500/30'
: value === 'Editor'
? 'bg-sky-500/20 text-sky-300 ring-1 ring-sky-500/30'
: 'bg-gray-500/20 text-gray-400 ring-1 ring-gray-500/30'}">
{value}
</span>
{:else if column.key === 'salary'}
<span class="font-mono text-emerald-300">${value?.toLocaleString()}</span>
{:else}
{value}
{/if}
{/snippet}
<div class="rounded-xl overflow-hidden shadow-2xl">
<DataTable
{data}
{columns}
sortable
searchable
paginate
selectable="multi"
perPage={10}
striped={false}
hover={false}
{customCell}
buttons={['csv', 'clipboard']}
classNames={{
container: '!bg-slate-900 !border-slate-800 font-sans',
toolbar: '!bg-slate-900',
searchInput: '!bg-slate-800 !text-slate-200 !border-slate-700 focus:!border-emerald-500 focus:!shadow-[0_0_0_2px_rgba(16,185,129,0.2)] placeholder:!text-slate-500',
thead: '!bg-slate-950',
th: '!bg-slate-950 !text-slate-400 !uppercase !text-[0.6875rem] !tracking-widest !font-semibold !border-b-slate-800',
tbody: '!bg-slate-900',
tr: 'hover:!bg-slate-800 !transition-colors',
trSelected: '!bg-emerald-500/10',
trEven: '!bg-white/[0.02]',
td: '!text-slate-300 !border-b-slate-800',
tfoot: '!bg-slate-950',
tf: '!text-emerald-400 !font-semibold !border-t-slate-800',
pagination: '!border-t-slate-800 !text-slate-400 !bg-slate-900',
pageButton: '!bg-slate-800 !text-slate-400 !border-slate-700 hover:!bg-slate-700 hover:!text-slate-200',
perPageSelect: '!bg-slate-800 !text-slate-200 !border-slate-700',
btnCreate: '!bg-emerald-500 !text-white !border-transparent hover:!bg-emerald-600',
btnEdit: '!bg-slate-800 !text-slate-200 !border-slate-700 hover:!bg-slate-700',
btnDelete: '!bg-red-500 !text-white !border-transparent hover:!bg-red-600',
editorModal: '!bg-slate-900 !border-slate-800',
editorBackdrop: '!bg-black/60',
}}
/>
</div>
Key points:
striped={false}andhover={false}— disable the built-in hover/stripe CSS so Tailwind classes control everything!(important modifier) — needed to override built-invar(--sdt-*)styles- No
<style>block — 100% Tailwind utility classes via theclassNamesprop - Every element of the table is customizable: toolbar, search, headers, rows, cells, footer, pagination, buttons, editor modal
Complete Example
A full-featured example combining client-side data, modal editing, custom cells, export buttons, selection, state persistence, and more:
<script lang="ts">
import { DataTable } from '@beeblock/svelar-datatable/ui';
import type { ColumnDef, EditorFieldDef, ButtonDef, DataTableStore } from '@beeblock/svelar-datatable';
let store: DataTableStore | undefined = $state();
const columns: ColumnDef[] = [
{ key: 'id', header: 'ID', type: 'number', editable: false, width: '60px' },
{ key: 'name', header: 'Name', searchable: true },
{ key: 'email', header: 'Email', searchable: true },
{
key: 'role',
header: 'Role',
type: 'custom',
filterable: true,
},
{
key: 'status',
header: 'Status',
type: 'custom',
},
{
key: 'salary',
header: 'Salary',
type: 'number',
footer: (rows) => {
const total = rows.reduce((sum, r) => sum + (r.salary ?? 0), 0);
return `Total: $${total.toLocaleString()}`;
},
},
{ key: 'created_at', header: 'Joined', type: 'date', sortable: true },
];
const editorFields: EditorFieldDef[] = [
{ name: 'name', type: 'text', label: 'Full Name', required: true },
{ name: 'email', type: 'text', label: 'Email', required: true },
{
name: 'role',
type: 'select',
label: 'Role',
options: [
{ label: 'Admin', value: 'admin' },
{ label: 'Editor', value: 'editor' },
{ label: 'Viewer', value: 'viewer' },
],
},
{
name: 'status',
type: 'select',
label: 'Status',
options: [
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
],
defaultValue: 'active',
},
{ name: 'salary', type: 'number', label: 'Salary' },
{ name: 'notes', type: 'textarea', label: 'Notes', placeholder: 'Optional notes...' },
];
const buttons: (ButtonDef | string)[] = [
'csv',
'clipboard',
'print',
{
key: 'deactivate',
label: 'Deactivate Selected',
variant: 'destructive',
disabled: (selected) => selected.length === 0,
action: async (selected) => {
const ids = selected.map((r) => r.id);
data = data.map((u) => ids.includes(u.id) ? { ...u, status: 'inactive' } : u);
store?.deselectAll();
},
},
];
let data = $state([
{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', role: 'admin', status: 'active', salary: 95000, created_at: '2024-01-15' },
{ id: 2, name: 'Bob Smith', email: 'bob@example.com', role: 'editor', status: 'active', salary: 72000, created_at: '2024-03-20' },
{ id: 3, name: 'Charlie Brown', email: 'charlie@example.com', role: 'viewer', status: 'inactive', salary: 55000, created_at: '2024-06-01' },
{ id: 4, name: 'Diana Prince', email: 'diana@example.com', role: 'admin', status: 'active', salary: 110000, created_at: '2023-11-10' },
{ id: 5, name: 'Eve Wilson', email: 'eve@example.com', role: 'editor', status: 'active', salary: 68000, created_at: '2025-01-05' },
]);
let nextId = $state(6);
async function handleCreate(formData: Record<string, any>) {
const newUser = { ...formData, id: nextId++ };
data = [...data, newUser];
}
async function handleEdit(row: any, formData: Record<string, any>) {
data = data.map((u) => (u.id === row.id ? { ...u, ...formData } : u));
}
async function handleDelete(rows: any[]) {
const ids = rows.map((r) => r.id);
data = data.filter((u) => !ids.includes(u.id));
}
</script>
<DataTable
{columns}
{data}
bind:storeRef={store}
sortable={true}
searchable={true}
paginate={true}
perPage={10}
perPageOptions={[5, 10, 25, 50]}
selectable="multi"
editorMode="modal"
{editorFields}
onCreate={handleCreate}
onEdit={handleEdit}
onDelete={handleDelete}
{buttons}
stateSaveKey="demo-users-table"
striped={true}
hover={true}
expandable={true}
>
{#snippet customCell({ row, column, value })}
{#if column.key === 'role'}
<span style="
background: {value === 'admin' ? '#dbeafe' : value === 'editor' ? '#fef3c7' : '#e5e7eb'};
color: {value === 'admin' ? '#1d4ed8' : value === 'editor' ? '#92400e' : '#374151'};
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
">
{value}
</span>
{:else if column.key === 'status'}
<span style="
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.8125rem;
color: {value === 'active' ? '#16a34a' : '#dc2626'};
">
<span style="
width: 0.5rem;
height: 0.5rem;
border-radius: 9999px;
background: {value === 'active' ? '#22c55e' : '#ef4444'};
"></span>
{value}
</span>
{:else}
{value}
{/if}
{/snippet}
{#snippet expandContent({ row })}
<div style="padding: 1rem 2rem; background: #f9fafb;">
<p><strong>Notes:</strong> {row.notes ?? 'No additional notes.'}</p>
<p><strong>Email:</strong> {row.email}</p>
<p><strong>Joined:</strong> {row.created_at}</p>
</div>
{/snippet}
</DataTable>