flowbite-svelte is a comprehensive UI component library built on Tailwind CSS that provides production-ready Svelte components for building modern web applications. This article covers building advanced data tables with server-side processing, real-time updates, and complex state management. This is part 4 of a series on using flowbite-svelte with Svelte.
This guide walks through creating a production-ready data table system with server-side pagination, sorting, filtering, and real-time data synchronization using flowbite-svelte components and Svelte stores.
Prerequisites
Before starting, ensure you have:
- A SvelteKit project (SvelteKit 1.0+ or standalone Svelte 4+)
- Node.js 18+ and npm/pnpm/yarn
- Tailwind CSS configured in your project
- Understanding of Svelte stores, reactivity, and async operations
- Basic knowledge of REST APIs and server-side data processing
Installation
Install flowbite-svelte and required dependencies:
npm install flowbite-svelte flowbite
npm install -D tailwindcss postcss autoprefixer
npm install -D @flowbite-svelte-plugins/datatable
For icon support:
npm install -D flowbite-svelte-icons
Project Setup
Configure Tailwind CSS to include flowbite-svelte:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./src/**/*.{html,js,svelte,ts}',
'./node_modules/flowbite-svelte/**/*.{svelte,js,ts}'
],
theme: {
extend: {},
},
plugins: [require('flowbite/plugin')],
darkMode: 'class',
}
Add Tailwind directives to your main CSS file:
/* src/app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Import the CSS in your layout:
<!-- src/routes/+layout.svelte -->
<script>
import '../app.css';
</script>
<slot />
First Example / Basic Usage
Let's start with a simple server-side data table:
<!-- src/lib/ServerDataTable.svelte -->
<script lang="ts">
import {
TableHead,
TableHeadCell,
TableBody,
TableBodyRow,
TableBodyCell,
Button,
Spinner
} from 'flowbite-svelte';
interface TableData {
id: string;
name: string;
email: string;
role: string;
createdAt: string;
}
interface TableState {
data: TableData[];
loading: boolean;
page: number;
pageSize: number;
total: number;
sortBy: string;
sortOrder: 'asc' | 'desc';
}
export let apiUrl = '/api/users';
export let pageSize = 10;
let state: TableState = $state({
data: [],
loading: false,
page: 1,
pageSize,
total: 0,
sortBy: 'createdAt',
sortOrder: 'desc'
});
async function fetchData() {
state.loading = true;
try {
const params = new URLSearchParams({
page: state.page.toString(),
pageSize: state.pageSize.toString(),
sortBy: state.sortBy,
sortOrder: state.sortOrder
});
const response = await fetch(`${apiUrl}?${params}`);
const result = await response.json();
state.data = result.data;
state.total = result.total;
} catch (error) {
console.error('Failed to fetch data:', error);
} finally {
state.loading = false;
}
}
function handleSort(column: string) {
if (state.sortBy === column) {
state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
state.sortBy = column;
state.sortOrder = 'asc';
}
fetchData();
}
function handlePageChange(newPage: number) {
state.page = newPage;
fetchData();
}
$effect(() => {
fetchData();
});
</script>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
{#if state.loading}
<div class="flex justify-center items-center p-8">
<Spinner size="xl" />
</div>
{:else}
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<TableHead>
<TableHeadCell>
<button
class="flex items-center"
on:click={() => handleSort('name')}
>
Name
{#if state.sortBy === 'name'}
<span class="ml-1">
{state.sortOrder === 'asc' ? '↑' : '↓'}
</span>
{/if}
</button>
</TableHeadCell>
<TableHeadCell>
<button
class="flex items-center"
on:click={() => handleSort('email')}
>
Email
{#if state.sortBy === 'email'}
<span class="ml-1">
{state.sortOrder === 'asc' ? '↑' : '↓'}
</span>
{/if}
</button>
</TableHeadCell>
<TableHeadCell>
<button
class="flex items-center"
on:click={() => handleSort('role')}
>
Role
{#if state.sortBy === 'role'}
<span class="ml-1">
{state.sortOrder === 'asc' ? '↑' : '↓'}
</span>
{/if}
</button>
</TableHeadCell>
<TableHeadCell>
<button
class="flex items-center"
on:click={() => handleSort('createdAt')}
>
Created
{#if state.sortBy === 'createdAt'}
<span class="ml-1">
{state.sortOrder === 'asc' ? '↑' : '↓'}
</span>
{/if}
</button>
</TableHeadCell>
</TableHead>
<TableBody>
{#each state.data as item (item.id)}
<TableBodyRow>
<TableBodyCell class="font-medium text-gray-900 dark:text-white">
{item.name}
</TableBodyCell>
<TableBodyCell>{item.email}</TableBodyCell>
<TableBodyCell>{item.role}</TableBodyCell>
<TableBodyCell>
{new Date(item.createdAt).toLocaleDateString()}
</TableBodyCell>
</TableBodyRow>
{/each}
</TableBody>
</table>
{/if}
<div class="flex items-center justify-between p-4">
<div class="text-sm text-gray-700 dark:text-gray-400">
Showing {(state.page - 1) * state.pageSize + 1} to
{Math.min(state.page * state.pageSize, state.total)} of {state.total} entries
</div>
<div class="flex gap-2">
<Button
size="xs"
disabled={state.page === 1}
on:click={() => handlePageChange(state.page - 1)}
>
Previous
</Button>
<Button
size="xs"
disabled={state.page * state.pageSize >= state.total}
on:click={() => handlePageChange(state.page + 1)}
>
Next
</Button>
</div>
</div>
</div>
Using the component:
<!-- src/routes/+page.svelte -->
<script>
import ServerDataTable from '$lib/ServerDataTable.svelte';
</script>
<div class="p-6">
<h1 class="text-2xl font-bold mb-4">Users Table</h1>
<ServerDataTable apiUrl="/api/users" pageSize={10} />
</div>
This example demonstrates a basic server-side data table with sorting and pagination. The component fetches data from an API endpoint, handles loading states, and provides interactive sorting controls.
Understanding the Basics
flowbite-svelte provides several approaches for building data tables:
-
Basic Table Components -
TableHead,TableBody,TableBodyRow, etc. for custom implementations -
DataTable Plugin -
@flowbite-svelte-plugins/datatablefor feature-rich tables - Server-Side Processing - Custom implementation with API integration
For advanced use cases with server-side processing, we use the basic components because they provide:
- Full control over data fetching logic
- Custom state management
- Integration with Svelte stores
- Real-time update capabilities
Key concepts:
-
Reactive State - Using
$statefor reactive table state -
Effects - Using
$effectto trigger data fetching on state changes - Server-Side Pagination - Fetching only the current page from the server
- Sorting - Sending sort parameters to the server
Practical Example / Building Something Real
Let's create a complete advanced data table system with filtering, real-time updates, and state management:
// src/stores/dataTableStore.ts
import { writable, derived } from 'svelte/store';
export interface TableFilters {
search?: string;
role?: string;
dateFrom?: string;
dateTo?: string;
}
export interface TableState {
page: number;
pageSize: number;
sortBy: string;
sortOrder: 'asc' | 'desc';
filters: TableFilters;
}
function createDataTableStore(initialState: Partial<TableState> = {}) {
const { subscribe, update, set } = writable<TableState>({
page: 1,
pageSize: 10,
sortBy: 'createdAt',
sortOrder: 'desc',
filters: {},
...initialState
});
return {
subscribe,
setPage: (page: number) => update(s => ({ ...s, page })),
setPageSize: (size: number) => update(s => ({ ...s, pageSize: size, page: 1 })),
setSort: (sortBy: string, sortOrder: 'asc' | 'desc') =>
update(s => ({ ...s, sortBy, sortOrder, page: 1 })),
setFilters: (filters: TableFilters) =>
update(s => ({ ...s, filters, page: 1 })),
reset: () => set({
page: 1,
pageSize: 10,
sortBy: 'createdAt',
sortOrder: 'desc',
filters: {}
})
};
}
export const createTableStore = createDataTableStore;
Create the advanced table component:
<!-- src/lib/AdvancedDataTable.svelte -->
<script lang="ts">
import {
TableHead,
TableHeadCell,
TableBody,
TableBodyRow,
TableBodyCell,
Button,
Input,
Select,
Spinner,
Badge
} from 'flowbite-svelte';
import { createTableStore, type TableFilters } from '$stores/dataTableStore';
import { onMount } from 'svelte';
interface TableData {
id: string;
name: string;
email: string;
role: string;
status: 'active' | 'inactive';
createdAt: string;
}
interface ApiResponse {
data: TableData[];
total: number;
page: number;
pageSize: number;
}
export let apiUrl = '/api/users';
export let pageSize = 10;
export let enableRealtime = false;
const tableStore = createTableStore({ pageSize });
let tableState = $state({ data: [] as TableData[], total: 0, loading: false });
let filters = $state<TableFilters>({});
let searchDebounceTimer: ReturnType<typeof setTimeout>;
async function fetchData() {
tableState.loading = true;
try {
const state = $state.snapshot(tableStore);
const params = new URLSearchParams({
page: state.page.toString(),
pageSize: state.pageSize.toString(),
sortBy: state.sortBy,
sortOrder: state.sortOrder,
...Object.entries(state.filters).reduce((acc, [key, value]) => {
if (value) acc[key] = value.toString();
return acc;
}, {} as Record<string, string>)
});
const response = await fetch(`${apiUrl}?${params}`);
if (!response.ok) throw new Error('Failed to fetch data');
const result: ApiResponse = await response.json();
tableState.data = result.data;
tableState.total = result.total;
} catch (error) {
console.error('Failed to fetch data:', error);
} finally {
tableState.loading = false;
}
}
function handleSort(column: string) {
const state = $state.snapshot(tableStore);
const newOrder = state.sortBy === column && state.sortOrder === 'asc'
? 'desc'
: 'asc';
tableStore.setSort(column, newOrder);
}
function handleFilterChange(key: keyof TableFilters, value: string) {
filters = { ...filters, [key]: value || undefined };
if (key === 'search') {
clearTimeout(searchDebounceTimer);
searchDebounceTimer = setTimeout(() => {
tableStore.setFilters(filters);
}, 300);
} else {
tableStore.setFilters(filters);
}
}
function handlePageChange(newPage: number) {
tableStore.setPage(newPage);
}
function handlePageSizeChange(size: number) {
tableStore.setPageSize(size);
}
// Subscribe to store changes and refetch data
$effect(() => {
const unsubscribe = tableStore.subscribe(() => {
fetchData();
});
return unsubscribe;
});
// Real-time updates using polling
let pollInterval: ReturnType<typeof setInterval>;
onMount(() => {
fetchData();
if (enableRealtime) {
pollInterval = setInterval(() => {
fetchData();
}, 5000); // Poll every 5 seconds
}
return () => {
if (pollInterval) clearInterval(pollInterval);
clearTimeout(searchDebounceTimer);
};
});
$: currentState = $state.snapshot(tableStore);
$: totalPages = Math.ceil(tableState.total / currentState.pageSize);
$: startItem = (currentState.page - 1) * currentState.pageSize + 1;
$: endItem = Math.min(currentState.page * currentState.pageSize, tableState.total);
</script>
<div class="space-y-4">
<!-- Filters -->
<div class="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<Input
placeholder="Search..."
bind:value={filters.search}
on:input={(e) => handleFilterChange('search', e.target.value)}
/>
</div>
<div>
<Select
bind:value={filters.role}
on:change={(e) => handleFilterChange('role', e.target.value)}
>
<option value="">All Roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
<option value="moderator">Moderator</option>
</Select>
</div>
<div>
<Input
type="date"
placeholder="From Date"
bind:value={filters.dateFrom}
on:input={(e) => handleFilterChange('dateFrom', e.target.value)}
/>
</div>
<div>
<Input
type="date"
placeholder="To Date"
bind:value={filters.dateTo}
on:input={(e) => handleFilterChange('dateTo', e.target.value)}
/>
</div>
</div>
</div>
<!-- Table -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
{#if tableState.loading && tableState.data.length === 0}
<div class="flex justify-center items-center p-8">
<Spinner size="xl" />
</div>
{:else}
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<TableHead>
<TableHeadCell>
<button
class="flex items-center hover:text-gray-900 dark:hover:text-white"
on:click={() => handleSort('name')}
>
Name
{#if currentState.sortBy === 'name'}
<span class="ml-1 text-gray-900 dark:text-white">
{currentState.sortOrder === 'asc' ? '↑' : '↓'}
</span>
{/if}
</button>
</TableHeadCell>
<TableHeadCell>
<button
class="flex items-center hover:text-gray-900 dark:hover:text-white"
on:click={() => handleSort('email')}
>
Email
{#if currentState.sortBy === 'email'}
<span class="ml-1 text-gray-900 dark:text-white">
{currentState.sortOrder === 'asc' ? '↑' : '↓'}
</span>
{/if}
</button>
</TableHeadCell>
<TableHeadCell>
<button
class="flex items-center hover:text-gray-900 dark:hover:text-white"
on:click={() => handleSort('role')}
>
Role
{#if currentState.sortBy === 'role'}
<span class="ml-1 text-gray-900 dark:text-white">
{currentState.sortOrder === 'asc' ? '↑' : '↓'}
</span>
{/if}
</button>
</TableHeadCell>
<TableHeadCell>Status</TableHeadCell>
<TableHeadCell>
<button
class="flex items-center hover:text-gray-900 dark:hover:text-white"
on:click={() => handleSort('createdAt')}
>
Created
{#if currentState.sortBy === 'createdAt'}
<span class="ml-1 text-gray-900 dark:text-white">
{currentState.sortOrder === 'asc' ? '↑' : '↓'}
</span>
{/if}
</button>
</TableHeadCell>
</TableHead>
<TableBody>
{#each tableState.data as item (item.id)}
<TableBodyRow class="bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-600">
<TableBodyCell class="font-medium text-gray-900 dark:text-white">
{item.name}
</TableBodyCell>
<TableBodyCell>{item.email}</TableBodyCell>
<TableBodyCell>
<Badge color="info">{item.role}</Badge>
</TableBodyCell>
<TableBodyCell>
<Badge color={item.status === 'active' ? 'success' : 'warning'}>
{item.status}
</Badge>
</TableBodyCell>
<TableBodyCell>
{new Date(item.createdAt).toLocaleDateString()}
</TableBodyCell>
</TableBodyRow>
{/each}
</TableBody>
</table>
{/if}
</div>
<!-- Pagination -->
<div class="flex flex-col sm:flex-row items-center justify-between gap-4 p-4 bg-white dark:bg-gray-800 rounded-lg shadow">
<div class="flex items-center gap-4">
<span class="text-sm text-gray-700 dark:text-gray-400">
Show
</span>
<Select
value={currentState.pageSize.toString()}
on:change={(e) => handlePageSizeChange(Number(e.target.value))}
class="w-20"
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</Select>
<span class="text-sm text-gray-700 dark:text-gray-400">
entries
</span>
</div>
<div class="text-sm text-gray-700 dark:text-gray-400">
Showing <span class="font-semibold">{startItem}</span> to
<span class="font-semibold">{endItem}</span> of
<span class="font-semibold">{tableState.total}</span> entries
</div>
<div class="flex gap-2">
<Button
size="xs"
color="light"
disabled={currentState.page === 1}
on:click={() => handlePageChange(currentState.page - 1)}
>
Previous
</Button>
{#each Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const page = Math.max(1, Math.min(totalPages - 4, currentState.page - 2)) + i;
return page <= totalPages ? page : null;
}).filter(Boolean) as page}
<Button
size="xs"
color={page === currentState.page ? 'blue' : 'light'}
on:click={() => handlePageChange(page)}
>
{page}
</Button>
{/each}
<Button
size="xs"
color="light"
disabled={currentState.page >= totalPages}
on:click={() => handlePageChange(currentState.page + 1)}
>
Next
</Button>
</div>
</div>
</div>
Example API route for SvelteKit:
// src/routes/api/users/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url }) => {
const page = parseInt(url.searchParams.get('page') || '1');
const pageSize = parseInt(url.searchParams.get('pageSize') || '10');
const sortBy = url.searchParams.get('sortBy') || 'createdAt';
const sortOrder = url.searchParams.get('sortOrder') || 'desc';
const search = url.searchParams.get('search') || '';
const role = url.searchParams.get('role') || '';
// Simulate database query
// In production, replace with actual database query
let data = [
{ id: '1', name: 'John Doe', email: 'john@example.com', role: 'admin', status: 'active', createdAt: '2024-01-15' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'user', status: 'active', createdAt: '2024-01-20' },
// ... more data
];
// Apply filters
if (search) {
data = data.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase()) ||
item.email.toLowerCase().includes(search.toLowerCase())
);
}
if (role) {
data = data.filter(item => item.role === role);
}
// Apply sorting
data.sort((a, b) => {
const aVal = a[sortBy as keyof typeof a];
const bVal = b[sortBy as keyof typeof b];
const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
return sortOrder === 'asc' ? comparison : -comparison;
});
// Apply pagination
const total = data.length;
const start = (page - 1) * pageSize;
const end = start + pageSize;
const paginatedData = data.slice(start, end);
return json({
data: paginatedData,
total,
page,
pageSize
});
};
Using the advanced table:
<!-- src/routes/users/+page.svelte -->
<script>
import AdvancedDataTable from '$lib/AdvancedDataTable.svelte';
</script>
<div class="p-6">
<h1 class="text-2xl font-bold mb-4">Users Management</h1>
<AdvancedDataTable
apiUrl="/api/users"
pageSize={10}
enableRealtime={true}
/>
</div>
This example demonstrates a complete advanced data table system with:
- Server-side pagination, sorting, and filtering
- Debounced search input
- Real-time data updates via polling
- Centralized state management with Svelte stores
- Loading states and error handling
- Responsive design with Tailwind CSS
Common Issues / Troubleshooting
-
Table not updating after state changes
- Ensure you're subscribing to the store properly using
$effector reactive statements - Check that the store's
setmethods trigger reactivity correctly - Verify that
fetchData()is called when state changes
- Ensure you're subscribing to the store properly using
-
Performance issues with large datasets
- Implement proper debouncing for search inputs (300ms recommended)
- Use server-side pagination instead of loading all data
- Consider virtual scrolling for very large datasets
- Limit real-time polling frequency (5+ seconds)
-
Sorting not working correctly
- Verify that your API endpoint handles sort parameters correctly
- Check that sort state is properly reset when changing sort column
- Ensure sort order toggles correctly (asc ↔ desc)
-
Filters not applying
- Check that filter values are properly sent to the API
- Verify URLSearchParams construction includes all filter keys
- Ensure the API endpoint processes all filter parameters
Next Steps
- Add column visibility toggles: Allow users to show/hide columns
- Implement export functionality: Add CSV/Excel export for table data
- Add bulk actions: Implement row selection and bulk operations
- Integrate WebSocket: Replace polling with WebSocket for true real-time updates
- Add advanced filtering: Implement date range pickers and multi-select filters
- Optimize with caching: Add response caching to reduce server load
Summary
In this article, we created an advanced data table system using flowbite-svelte with server-side processing, real-time updates, and comprehensive state management. The system includes pagination, sorting, filtering, and real-time synchronization, providing a production-ready solution for managing large datasets in Svelte applications.
Top comments (0)