Save and restore table view configurations with localStorage persistence for Svelte applications
A lightweight, framework-agnostic Svelte package that lets users save, manage, and restore table configurations (filters, sorting, column order, visibility) as named "views". Perfect for data-heavy applications where users need to switch between different table configurations quickly.
- π― Complete Table State Management - Save filters, sort, columns, column order, widths, and more
- πΎ localStorage Persistence - Views persist across sessions automatically
- π Search & Filter - Find views quickly with live search
- β¨οΈ Keyboard Navigation - Arrow keys, Enter, Escape support
- βοΈ Inline Rename - Rename views with explicit save/cancel buttons
- π Usage Tracking - Track how often views are used
- π Recent Views - Quick access to last 7 days, top 5 views
- π Duplicate Views - Copy existing views with one click
βοΈ Update vs Save New - Smart split button when view is modified- β Column Validation - Gracefully handles missing columns
- π¨ Tailwind CSS Styled - Beautiful, accessible UI out of the box
- π Zero Dependencies - Only peer dependency is Svelte
- π¦ TypeScript Support - Full type definitions included
npm install svelte-table-views<script lang="ts">
import { ViewSelector, SaveViewModal, viewActions, activeViewId, activeViewModified } from 'svelte-table-views'
import type { TableConfig, SavedView } from 'svelte-table-views'
let showSaveModal = false
let capturedConfig: TableConfig | null = null
// Your table state
let filters = []
let sort = null
let columns = ['id', 'name', 'email']
let columnOrder = ['id', 'name', 'email']
</script><div class="flex items-center justify-between gap-4 mb-4">
<!-- View Selector Dropdown -->
<ViewSelector on:viewSelected={handleViewSelected} />
<!-- Save/Update Button -->
{#if $activeViewId && $activeViewModified}
<!-- Split button when view is modified -->
<div class="inline-flex">
<button on:click={handleUpdateView}>Update View</button>
<button on:click={openSaveModal}>Save New</button>
</div>
{:else}
<button on:click={openSaveModal}>Save View</button>
{/if}
</div>
<!-- Your Table Component -->
<YourTable {filters} {sort} {columns} {columnOrder} />
<!-- Save View Modal -->
{#if showSaveModal && capturedConfig}
<SaveViewModal
bind:open={showSaveModal}
config={capturedConfig}
on:save={handleViewSaved}
/>
{/if}function openSaveModal() {
capturedConfig = {
filters,
sort,
columns,
columnOrder,
columnWidths: {},
pageSize: 25
}
showSaveModal = true
}
async function handleViewSelected(event: CustomEvent<{ view: SavedView }>) {
const view = event.detail.view
// Apply saved config to your table
filters = view.config.filters
sort = view.config.sort
columns = view.config.columns
columnOrder = view.config.columnOrder
}
async function handleUpdateView() {
if ($activeViewId) {
await viewActions.update($activeViewId, {
config: {
filters,
sort,
columns,
columnOrder,
columnWidths: {},
pageSize: 25
}
})
}
}
function handleViewSaved(event: CustomEvent<{ id: string; name: string }>) {
console.log('View saved:', event.detail.name)
}Dropdown component for selecting, searching, renaming, and deleting saved views.
Props:
- None (controlled via stores)
Events:
viewSelected: CustomEvent<{ view: SavedView }>- Fired when user selects a viewdeleteView: CustomEvent<{ id: string }>- Fired when user deletes a view
Features:
- Search views by name (live filtering)
- Recent views section (last 7 days, top 5)
- All views section (alphabetically sorted)
- Inline rename with save/cancel buttons
- Duplicate view
- Delete with confirmation
- Keyboard navigation (Arrow keys, Enter, Escape)
Modal component for saving new table views.
Props:
open: boolean- Controls modal visibility (usebind:open)config: TableConfig- Table configuration to saveoriginalQuery?: string- Optional: original NL query that generated this config
Events:
save: CustomEvent<{ id: string; name: string }>- Fired when view is saved
Features:
- Name input (required, max 100 chars)
- Description input (optional, max 500 chars)
- Duplicate name detection
- Storage limit enforcement (50 views)
- Preview of what's being saved
- Keyboard shortcuts (Esc to cancel, Ctrl+Enter to save)
Writable store containing all saved views.
import { savedViews } from 'svelte-table-views'
$savedViews // SavedView[]Derived store containing recent views (last 7 days, top 5, sorted by lastUsed).
import { recentViews } from 'svelte-table-views'
$recentViews // SavedView[]Writable store tracking the currently active view ID.
import { activeViewId } from 'svelte-table-views'
$activeViewId // string | nullWritable store tracking whether the active view has been modified.
import { activeViewModified } from 'svelte-table-views'
$activeViewModified // booleanDerived store containing the full active view object.
import { activeView } from 'svelte-table-views'
$activeView // SavedView | nullSave a new view.
const newView = await viewActions.save({
name: 'High Priority Items',
description: 'Items with priority > 5',
config: {
filters: [{ columnId: 'priority', operator: 'greaterThan', value: 5 }],
sort: { columnId: 'createdAt', direction: 'desc' },
columns: ['id', 'name', 'priority'],
columnOrder: ['priority', 'name', 'id'],
columnWidths: {},
pageSize: 25
}
})Load an existing view. Updates usage statistics and sets as active.
const view = await viewActions.load('view-id-123')Update an existing view.
await viewActions.update('view-id-123', {
config: updatedConfig,
description: 'Updated description'
})Delete a view.
await viewActions.delete('view-id-123')Rename a view.
await viewActions.rename('view-id-123', 'New View Name')Mark the active view as modified (shows split button).
viewActions.markModified()Clear the active view.
viewActions.clearActive()Check if a view name already exists.
const exists = await viewActions.nameExists('My View')Get storage usage statistics.
const stats = await viewActions.getStorageStats()
console.log(`${stats.count}/${stats.limit} views (${stats.percentFull}% full)`)interface TableConfig {
filters: FilterCondition[]
sort: SortConfig | null
columns: string[]
columnOrder: string[]
columnWidths: Record<string, number>
pageSize: number
grouping?: string[]
}interface FilterCondition {
columnId: string
operator: string
value: any
}interface SortConfig {
columnId: string
direction: 'asc' | 'desc'
}interface SavedView {
// Identity
id: string
name: string
description?: string
// Configuration
config: TableConfig
// Optional: original NL query for reference
originalQuery?: string
// Metadata
createdAt: number
updatedAt: number
usageCount: number
lastUsed: number
}type SavedViewInput = Omit<SavedView, 'id' | 'createdAt' | 'updatedAt' | 'usageCount' | 'lastUsed'>The package automatically validates columns when loading a view:
function handleViewSelected(event: CustomEvent<{ view: SavedView }>) {
const view = event.detail.view
const availableColumns = ['id', 'name', 'email', 'created_at']
// Filter out missing columns
const validColumns = view.config.columns.filter(col =>
availableColumns.includes(col)
)
// Warn user if columns are missing
const missingColumns = view.config.columns.filter(col =>
!availableColumns.includes(col)
)
if (missingColumns.length > 0) {
alert(`Some columns no longer exist: ${missingColumns.join(', ')}`)
}
// Apply valid config
columns = validColumns
// ... rest of config
}The default localStorage key is 'svelte_table_views_saved_views'. To customize it, fork the package and modify src/lib/stores/saved-views.ts:
const STORAGE_KEY = 'my_app_saved_views'The default limit is 50 views. To change it, modify the getStorageStats function:
async getStorageStats() {
const views = get(savedViews)
const count = views.length
const limit = 100 // Change this
return { count, limit, percentFull: Math.round((count / limit) * 100) }
}The package uses Tailwind CSS classes. If you're not using Tailwind, you have two options:
npm install -D tailwindcss
npx tailwindcss initTarget the component classes in your global CSS:
/* Override ViewSelector styles */
.view-selector button {
/* Your styles */
}
/* Override SaveViewModal styles */
.save-view-modal {
/* Your styles */
}- Modern browsers with
crypto.randomUUID()support - localStorage support required
- No IE11 support
Contributions are welcome! Please read CONTRIBUTING.md before submitting PRs.
MIT Β© Jason (Shotley Builder)
- @shotleybuilder/svelte-table-kit - Headless TanStack Table wrapper for Svelte
- TanStack Table - Headless table library
See CHANGELOG.md for version history.
- π Report a Bug
- π‘ Request a Feature
- π Read the Docs