
Shadcn Svelte
- 10 installs
- 22 repo stars
- Updated May 28, 2026
- acedergren/agentic-tools
shadcn-svelte is a Claude Code skill that provides expert debugging and build guidance for shadcn-svelte components, TanStack Table in Svelte 5, and Tailwind v4.1.
About
This skill gives expert guidance for building UI with shadcn-svelte, TanStack Table in Svelte 5, and Tailwind v4.1. It documents reactivity bugs like missing get accessors, Bits UI builder destructuring, and the silent Tailwind v4.1 migration failure. A developer uses it when components render but interactions silently break. It also includes a library-selection decision tree and data-table complexity trade-offs.
- Fixes non-obvious Svelte 5 runes reactivity bugs in shadcn-svelte and TanStack Table
- Documents the Tailwind v4.1 silent migration failure (@tailwind vs @import)
- Library selection tree: shadcn-svelte vs Skeleton vs Melt UI vs Bits UI
Shadcn Svelte by the numbers
- 10 all-time installs (skills.sh)
- Ranked #1,691 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
shadcn-svelte capabilities & compatibility
- Capabilities
- ui components · data table · reactivity debugging · tailwind migration
- Use cases
- frontend · ui design · debugging
- IDEs
- vscode · cursor ide
- Pricing
- Free
What shadcn-svelte says it does
**Key trade-off**: shadcn is NOT a library — you fork components into `$lib/components/ui/` and own security patches, bug fixes, and upgrades permanently.
**The most common TanStack + Svelte 5 bug.** Table renders correctly, pagination UI works, but sorting/filtering clicks do nothing.
npx skills add https://github.com/acedergren/agentic-tools --skill shadcn-svelteAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 10 |
|---|---|
| repo stars | ★ 22 |
| Last updated | May 28, 2026 |
| Repository | acedergren/agentic-tools ↗ |
What it does
Build and debug shadcn-svelte, TanStack Table, and Tailwind v4.1 UI in Svelte 5 apps.
Who is it for?
Fixing Svelte 5 runes reactivity bugs in shadcn-svelte, TanStack Table, and superforms, plus Tailwind v4.1 migration.
Skip if: Basic shadcn-svelte installation, which the skill assumes you already know.
When should I use this skill?
When working with shadcn-svelte components, TanStack Table in Svelte 5, or Tailwind v4.1.
What you get
Components stay reactive with get accessors, Bits UI builders, and correct Tailwind v4.1 imports.
By the numbers
- Data-table complexity cliff estimated from ~50 lines to ~200 lines
- 4 documented critical anti-patterns
Files
shadcn-svelte Expert Guidance
Assumption: You know how to run npx shadcn-svelte@latest add. This skill covers what the docs won't tell you.
NEVER
- Never destructure Bits UI builders at module level (
const { trigger } = Dialog) — builders are reactive objects, destructuring captures stale references. UseasChild let:builderpattern. - Never pass
data: myDatadirectly tocreateSvelteTable— Svelte 5 runes require getter accessors or data never updates. - Never use
@tailwind base/components/utilitiesin Tailwind v4.1 — directives silently do nothing; use@import "tailwindcss". - Never expect
npm updateto patch shadcn components — they're forked into your codebase; you own maintenance. - Never start with TanStack Table for a simple display table — the complexity cliff is steep (each feature adds 100–200 lines).
Library Selection
Need Svelte UI components?
│
├─ Own the code, heavy customization → shadcn-svelte
│ ├─ Complex data tables → TanStack integration built-in
│ └─ Unique design system → copy-paste, modify freely
│
├─ Ship fast, customize later → Skeleton UI
│ └─ Pre-built themes, npm package (auto-updates)
│
├─ Accessibility-first, bring own styles → Melt UI
│ └─ Headless primitives only
│
└─ Nothing fits → Build from Bits UI primitivesKey trade-off: shadcn is NOT a library — you fork components into $lib/components/ui/ and own security patches, bug fixes, and upgrades permanently.
Data table complexity cliff:
Simple table: ~50 lines
+ sorting: +100 lines
+ filtering: +150 lines
+ selection: +200 lines
+ visibility: +100 linesStart with <table>, upgrade to TanStack only when you need 2+ features.
Critical Anti-Patterns
#1: Early Builder Destructuring (Bits UI)
<!-- WRONG - breaks all click handlers silently -->
<script>
const { trigger } = Dialog; // stale reference
</script>
<!-- CORRECT -->
<Dialog.Root>
<Dialog.Trigger asChild let:builder>
<Button builders={[builder]}>Open</Button>
</Dialog.Trigger>
</Dialog.Root>Symptom: Component renders, click handlers silently fail. Error says undefined, no mention of builders.
#2: TanStack Table — Missing get Accessors
The most common TanStack + Svelte 5 bug. Table renders correctly, pagination UI works, but sorting/filtering clicks do nothing.
// WRONG - data never updates after init
const table = createSvelteTable({ data: myData, state: { sorting } });
// CORRECT - reactive getters
const table = createSvelteTable({
get data() { return myData; },
state: {
get sorting() { return sorting; }
},
onSortingChange: (updater) => {
sorting = typeof updater === "function" ? updater(sorting) : updater;
}
});Every onXChange handler must handle both function and value: typeof updater === "function" ? updater(old) : updater.
#3: Tailwind v4.1 — Silent Migration Failure
/* WRONG - silently does nothing in v4.1 */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* CORRECT */
@import "tailwindcss";// vite.config.ts - plugin order matters
import tailwindcss from '@tailwindcss/vite'
plugins: [tailwindcss(), sveltekit()] // tailwindcss BEFORE sveltekitSymptom: Styles work in dev (cached), production build has zero Tailwind classes. No error.
#4: superforms — Destructuring Before Bind
// WRONG - formData becomes stale reference, validation breaks
const { form: formData } = superForm(data.form);
formData.email = value;
// CORRECT - use bind: to connect to store
const { form: formData } = superForm(data.form);
// In template: <Input bind:value={$formData.email} />Expert Patterns
CSS Variable Theme (Tailwind v4.1)
@import "tailwindcss";
@layer theme {
:root {
--color-primary: 0 0% 9%; /* space-separated HSL enables /alpha */
--color-destructive: 0 84% 60%;
}
.dark { --color-primary: 0 0% 98%; }
}
/* Alpha via / syntax */
.overlay { @apply bg-[hsl(var(--color-primary)/0.5)]; }Space-separated HSL format (not hsl(H,S,L)) is required for the /alpha Tailwind syntax to work.
Form Field Pattern
<Form.Field {form} name="email">
<Form.Control let:attrs>
<Form.Label>Email</Form.Label>
<Input {...attrs} type="email" bind:value={$formData.email} />
</Form.Control>
<Form.FieldErrors />
</Form.Field>let:attrs spreads aria attributes automatically. <Form.FieldErrors /> auto-wires to validation state.
Debugging Checklists
Table not updating
1. get data() accessor used (not data: myData)? 2. All state wrapped in get (sorting, pagination, filters)? 3. Every onXChange has typeof updater === "function" guard?
Builder undefined
1. asChild let:builder on Trigger? 2. builders={[builder]} array passed to child? 3. No module-level destructuring?
Tailwind classes missing
1. @import "tailwindcss" (not @tailwind directives)? 2. @tailwindcss/vite plugin in vite.config? 3. Plugin before sveltekit() in array? 4. Deleted .svelte-kit/ and node_modules/.vite/ cache?
When to Load References
Load `references/datatable-tanstack-svelte5.md` when:
- Implementing 3+ table features (sorting + filtering + selection)
- TanStack errors mentioning
columnDeforgetCoreRowModel - Row selection with checkboxes across paginated data
Load `references/form-patterns.md` when:
- Multi-step wizard forms with validation
- Cross-field dependencies or async validation
- Zod + superforms backend integration
Do NOT load references for library choice, anti-pattern debugging, or Tailwind migration — handle with this file.
{
"name": "shadcn-svelte-skill",
"version": "1.0.0",
"description": "Build accessible, customizable UI components for Svelte/SvelteKit projects using shadcn-svelte CLI, Tailwind CSS v4.1, and TypeScript",
"author": {
"name": "fakebizprez",
"email": "anthony@linehaul.ai"
},
"keywords": ["svelte", "sveltekit", "shadcn-svelte", "ui-components", "tailwind", "typescript"]
}
shadcn-svelte Component Assistant
Load and use the shadcn-svelte-skill for all component guidance.
User Request
Topic/Task: $ARGUMENTS
Your Role
You are a shadcn-svelte component development expert. Based on the user's request, provide guided assistance using the shadcn-svelte-skill skill.
---
Topic Categories
If no argument provided or "help" requested
Provide a welcoming overview and topic menu:
1. Brief overview: Explain shadcn-svelte capabilities (copy-paste components, Tailwind v4.1, TypeScript) 2. Available topics: List the following options with brief descriptions
add- Install and add components to your projectform- Form creation with sveltekit-superforms and Zod validationtable- DataTable with TanStack Table v8 (sorting, filtering, pagination)dialog- Modal, dialog, drawer, and sheet patternstheme- CSS variables, dark mode, and customizationdebug- Troubleshoot common issues{component-name}- Guidance for specific components (button, card, etc.)
3. Ask: "What would you like help with today?"
---
If "$ARGUMENTS" contains "add" or "install"
Guide the user through component installation:
1. Overview: Explain shadcn-svelte's copy-paste model 2. Installation commands:
# Initialize (first time)
pnpm dlx shadcn-svelte@latest init
# Add individual components
pnpm dlx shadcn-svelte@latest add button
pnpm dlx shadcn-svelte@latest add card alert dialog
# Add all components
pnpm dlx shadcn-svelte@latest add --all
# List available components
pnpm dlx shadcn-svelte@latest list3. Component location: src/lib/components/ui/[component-name]/
4. Import patterns:
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";5. Next steps: Suggest form setup, theming, or specific components
---
If "$ARGUMENTS" contains "form" or "forms"
Walk through form creation workflow:
1. Overview: Explain shadcn-svelte forms with sveltekit-superforms
2. Required components:
pnpm dlx shadcn-svelte@latest add form input label button
pnpm add sveltekit-superforms zod3. Form structure with complete example:
<script lang="ts">
import { superForm } from "sveltekit-superforms";
import { zodClient } from "sveltekit-superforms/adapters";
import { z } from "zod";
import * as Form from "$lib/components/ui/form";
import { Input } from "$lib/components/ui/input";
import { Button } from "$lib/components/ui/button";
const schema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
const form = superForm(data.form, {
validators: zodClient(schema),
});
const { form: formData, enhance } = form;
</script>
<form method="POST" use:enhance>
<Form.Field {form} name="email">
<Form.Control let:attrs>
<Form.Label>Email</Form.Label>
<Input {...attrs} type="email" bind:value={$formData.email} />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Button type="submit">Submit</Button>
</form>4. Key concepts:
- Zod schema for validation
- superForm for form state
- Form.Field/Form.Control structure
- Progressive enhancement with
use:enhance
5. For complex forms: Reference workflows.md for multi-step builds
6. Next steps: Table setup, dialog patterns, or theming
---
If "$ARGUMENTS" contains "table" or "datatable"
Guide through DataTable setup with TanStack Table v8:
1. Overview: Explain TanStack Table v8 for production data tables
2. Installation:
pnpm dlx shadcn-svelte@latest add table data-table button dropdown-menu checkbox input
pnpm add @tanstack/table-core3. File structure:
routes/your-route/
columns.ts # Column definitions
data-table.svelte # Main table component
data-table-actions.svelte
+page.svelte4. Key patterns:
- Use
$statefor pagination, sorting, filtering createSvelteTablewithgetaccessorsrenderComponentfor interactive cellsrenderSnippetfor formatted cells
5. For complete examples: Reference datatable-tanstack-svelte5.md and shadcn-datatable.md
6. Next steps: Form integration, dialog patterns
---
If "$ARGUMENTS" contains "dialog" or "modal" or "drawer" or "sheet"
Explain modal/dialog/drawer patterns:
1. Overview: Dialog vs Drawer vs Sheet use cases
2. Installation:
pnpm dlx shadcn-svelte@latest add dialog drawer button3. Dialog example:
<script lang="ts">
import * as Dialog from "$lib/components/ui/dialog";
import { Button } from "$lib/components/ui/button";
let open = false;
</script>
<Dialog.Root bind:open>
<Dialog.Trigger asChild let:builder>
<Button builders={[builder]}>Open Dialog</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Title</Dialog.Title>
<Dialog.Description>Description</Dialog.Description>
</Dialog.Header>
<p>Content here</p>
<Dialog.Footer>
<Button on:click={() => (open = false)}>Close</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>4. Drawer example (mobile-friendly):
<script lang="ts">
import * as Drawer from "$lib/components/ui/drawer";
import { Button } from "$lib/components/ui/button";
</script>
<Drawer.Root>
<Drawer.Trigger asChild let:builder>
<Button builders={[builder]} variant="outline">Open Drawer</Button>
</Drawer.Trigger>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>Navigation</Drawer.Title>
</Drawer.Header>
<nav class="flex flex-col gap-2 p-4">
<a href="/">Home</a>
</nav>
</Drawer.Content>
</Drawer.Root>5. When to use what:
- Dialog: Focused actions, confirmations, forms
- Drawer: Navigation, side panels (mobile-friendly)
- Sheet: Content panels, settings
6. Next steps: Form in dialog, table with actions
---
If "$ARGUMENTS" contains "theme" or "css" or "dark" or "styling"
Guide through theming and customization:
1. Overview: CSS variables and Tailwind v4.1 theming
2. Theme location: src/app.css
3. CSS variables structure:
@import "tailwindcss";
@layer theme {
:root {
--color-background: 0 0% 100%;
--color-foreground: 0 0% 3.6%;
--color-primary: 0 0% 9%;
--color-primary-foreground: 0 0% 100%;
--color-secondary: 0 0% 96.1%;
--color-muted: 0 0% 96.1%;
--color-border: 0 0% 89.8%;
}
.dark {
--color-background: 0 0% 3.6%;
--color-foreground: 0 0% 98%;
/* ... dark mode values */
}
}4. Dark mode setup:
pnpm i mode-watcher <script>
import { modeWatcher } from "mode-watcher";
</script>
<div use:modeWatcher><!-- app --></div>5. Component customization:
- Modify directly in
src/lib/components/ui/ - Use
cn()utility for class merging - Override with Tailwind classes
6. Next steps: Custom component creation, accessibility
---
If "$ARGUMENTS" contains "debug" or "error" or "troubleshoot"
Provide troubleshooting guidance:
1. Ask: "What error are you seeing?" or "What's not working?"
2. Common issues:
Component Not Found:
pnpm dlx shadcn-svelte@latest list # Check installed
pnpm dlx shadcn-svelte@latest add button --overwrite # ReinstallStyling Issues:
- Verify Tailwind v4.1 in
vite.config.ts - Check CSS variables in
src/app.css - Ensure
@import "tailwindcss"at top
TypeScript Errors:
- Check path aliases in
svelte.config.js - Verify
$libpoints to./src/lib
Import Errors:
- Use
$lib/components/ui/not relative paths - Check component exists in directory
3. Provide specific fix based on error description
4. Next steps: Suggest preventive measures
---
If "$ARGUMENTS" contains a specific component name
Provide guidance for that specific component:
1. Check if component exists in shadcn-svelte 2. Installation command: pnpm dlx shadcn-svelte@latest add {component} 3. Basic usage example with common props 4. Variants and customization options 5. Common patterns for that component 6. Reference skill for detailed documentation
---
Guidelines
1. Always reference shadcn-svelte-skill for accurate technical details 2. Provide complete code examples that work out of the box 3. Use Tailwind v4.1 patterns (not v3 syntax) 4. Include imports in all code examples 5. For complex features: Reference workflows.md 6. For DataTables: Reference datatable-tanstack-svelte5.md
---
Response Format
Structure your response as:
1. Brief overview (2-3 sentences) 2. Installation (if needed) 3. Code example (complete, runnable) 4. Key concepts (bullet points) 5. Next steps (suggest related topics)
shadcn-svelte-skill - Expert Component Guidance
Version: 3.0.0 Grade: F → C (26/120 → ~75/120) Token Reduction: 1175 lines → 293 lines (75% reduction)
What This Skill Does
Expert guidance for shadcn-svelte architecture, TanStack Table reactivity, and Tailwind CSS v4.1 patterns. NOT an installation guide - focuses on what official docs don't tell you.
TDD Improvements Applied
1. Description Quality (RED → GREEN)
Problem: Description buried in implementation details Test Failed: Agent loaded for basic installation (should use official docs)
Fix:
- Clear negative scope: "NOT for basic installation"
- 5 specific expert use cases
- Added decision keywords: "choosing between", "debugging", "troubleshooting"
Result: ✅ Agent loads only for expert decisions, not installation
2. Knowledge Delta (RED → GREEN)
Problem: 95% installation instructions Claude already knows Test Failed: < 30% expert knowledge ratio
Removed (950 lines):
- Complete installation walkthrough
- Step-by-step component setup
- Code examples from official docs
- CLI command reference
Added (293 lines of expert insights):
- Library selection decision tree (when shadcn vs Skeleton vs Melt UI)
- 4 critical anti-patterns that WILL break your code
- Hidden costs of shadcn (no npm update, maintenance burden)
- Non-obvious patterns (CSS variable alpha syntax, TanStack state updaters)
Result: ✅ 90% expert knowledge (was 5%)
3. Anti-Patterns Added
Problem: No warnings about common failures Test Failed: Skill didn't prevent breaking changes
Added 4 Critical Anti-Patterns:
1. Early Destructuring of Builders
<!-- WRONG -->
const { trigger } = Dialog; // ❌ Breaks reactivity
<!-- CORRECT -->
<Dialog.Trigger asChild let:builder>
<Button builders={[builder]}> <!-- ✅ Works -->2. TanStack Table - Missing `get` Accessors
// WRONG
createSvelteTable({ data: myData }) // ❌ Stale
// CORRECT
createSvelteTable({ get data() { return myData } }) // ✅ ReactiveThis is THE most common Svelte 5 + TanStack bug
3. Tailwind v4.1 Migration
/* WRONG */
@tailwind base;
/* CORRECT */
@import "tailwindcss";4. Form Validation Destructuring
// WRONG
formData.email = value; // ❌ No validation
// CORRECT
<Input bind:value={$formData.email} /> // ✅ ReactiveResult: ✅ Prevents hours of debugging common issues
4. Progressive Disclosure
Problem: 1175 lines in single file Test Failed: Extreme token waste
Refactored:
- Core: 293 lines (decisions + anti-patterns + expert patterns)
- References: Installation guide, full TanStack examples, form patterns
- Loading triggers: "LOAD references/ when..."
Result: ✅ 75% reduction, loads details only when needed
Key Features
Library Selection Decision Tree
Need UI components?
├─ Maximum customization → shadcn-svelte
├─ Rapid prototyping → Skeleton UI
├─ Accessibility-first → Melt UI
└─ Unique requirements → Bits UI primitivesHidden Costs
1. No npm update - Manual component updates 2. CSS variable pattern - HSL triplets for alpha channel 3. Complexity cliff - Data tables grow from 50 → 500+ lines fast
Expert Patterns
- TanStack state updater:
typeof updater === "function" ? updater(state) : updater - CSS variable alpha:
bg-[hsl(var(--color-primary)/0.5)] - Form field pattern:
let:attrs+bind:value+ auto-wired errors
Quick Debugging
- Table not updating? Check
getaccessors - Builder undefined? Check
asChild let:builder - Tailwind not applying? Check
@import "tailwindcss"
When to Use This Skill
✅ Use when:
- Choosing between shadcn/Skeleton/Melt UI
- TanStack Table reactivity issues
- Tailwind v4.1 migration problems
- Form validation not working
- Need expert patterns missing from docs
❌ Don't use for:
- Basic installation (see shadcn-svelte.com)
- Component gallery browsing
- Simple "how to install X"
Installation
cp -r shadcn-svelte-skill ~/.agents/skills/ # Claude Code
cp -r shadcn-svelte-skill ~/.cursor/skills/ # CursorResources
- Official Docs: https://www.shadcn-svelte.com/docs (for installation)
- This Skill: Non-obvious decisions, breaking patterns, expert insights
DataTable: Tailwind v4.1 + TanStack Table v8 + Svelte 5
This is a reference document. Load this when building production DataTable components. See SKILL.md for basic setup.
Overview
DataTables require careful styling for:
- Row states (hover, selected, sorted)
- Responsive layouts
- Dark mode support
- Performance with large datasets
With Tailwind v4.1 + @tailwindcss/vite:
- Zero-runtime CSS via Vite plugin
- CSS variables for theming (no PostCSS needed)
- Automatic content scanning
- Dynamic utilities for row states
Tailwind v4.1 Setup for DataTables
CSS Variables in app.css
@import "tailwindcss";
@layer theme {
:root {
/* Table colors */
--color-table-bg: 0 0% 100%;
--color-table-row-hover: 0 0% 96.1%;
--color-table-row-selected: 210 40% 96%;
--color-table-border: 0 0% 89.8%;
--color-table-text: 0 0% 3.6%;
--color-table-header-bg: 0 0% 94.1%;
}
.dark {
--color-table-bg: 0 0% 14.9%;
--color-table-row-hover: 0 0% 22%;
--color-table-row-selected: 210 100% 35%;
--color-table-border: 0 0% 22%;
--color-table-text: 0 0% 98%;
--color-table-header-bg: 0 0% 22%;
}
}
@layer utilities {
.table-cell {
@apply px-4 py-3 text-sm;
}
.table-row-hover {
@apply hover:bg-[hsl(var(--color-table-row-hover))];
}
.table-row-selected {
@apply bg-[hsl(var(--color-table-row-selected))] hover:bg-[hsl(var(--color-table-row-selected))] border-l-4 border-l-primary;
}
.table-header {
@apply bg-[hsl(var(--color-table-header-bg))] font-semibold text-xs uppercase tracking-wide;
}
}tailwind.config.js (v4.1 minimal)
export default {
theme: {
extend: {
colors: {
'table-bg': 'hsl(var(--color-table-bg))',
'table-row-hover': 'hsl(var(--color-table-row-hover))',
'table-row-selected': 'hsl(var(--color-table-row-selected))',
'table-border': 'hsl(var(--color-table-border))',
'table-text': 'hsl(var(--color-table-text))',
'table-header': 'hsl(var(--color-table-header-bg))',
},
},
},
}Installation
pnpm dlx shadcn-svelte@latest add table data-table button dropdown-menu checkbox
pnpm i @tanstack/svelte-tableArchitecture: State Management with Tailwind v4.1
Use this pattern for all data tables to avoid state synchronization issues.
<script lang="ts">
import { createTable, Render, Subscribe, createRender } from "svelte-headless-table";
import {
addPagination,
addSortBy,
addFilters,
addColumnVisibility,
addRowSelection,
} from "svelte-headless-table/plugins";
import { readable } from "svelte/store";
interface Row {
id: string;
name: string;
email: string;
amount: number;
status: "active" | "inactive";
}
// 1. Data as reactive state (Svelte 5 rune)
let data = $state<Row[]>([
{ id: "1", name: "Alice", email: "alice@example.com", amount: 100, status: "active" },
{ id: "2", name: "Bob", email: "bob@example.com", amount: 200, status: "inactive" },
]);
// 2. Table instance (derived from reactive data)
const dataStore = readable(data);
// 3. Create table with plugins
const table = createTable(dataStore, {
page: addPagination({ initialPageSize: 10 }),
sort: addSortBy(),
filters: addFilters(),
select: addRowSelection(),
colVis: addColumnVisibility(),
});
// 4. Define columns (static structure recommended)
const columns = table.createColumns([
table.column({
accessor: "name",
header: "Name",
cell: (item) => item.name,
plugins: {
sort: {
disable: false,
},
filter: {
exclude: false,
},
},
}),
table.column({
accessor: "email",
header: "Email",
cell: (item) => item.email,
}),
table.column({
accessor: "amount",
header: "Amount",
cell: (item) => `$${item.amount}`,
plugins: {
sort: {
disable: false,
},
},
}),
table.column({
accessor: "status",
header: "Status",
cell: (item) => item.status,
plugins: {
filter: {
exclude: false,
},
},
}),
]);
// 5. Derive table state
const { headerRows, pageRows, tableAttrs } = table.createVM(columns);
</script>Row Selection with Tailwind v4.1
Use v4.1 utilities with CSS variables for selected state styling:
<script lang="ts">
import * as Checkbox from "$lib/components/ui/checkbox";
import { cn } from "$lib/utils";
let selectedRows = $state<Set<string>>(new Set());
function toggleRowSelection(id: string) {
if (selectedRows.has(id)) {
selectedRows.delete(id);
} else {
selectedRows.add(id);
}
selectedRows = selectedRows;
}
$derived isSelected = (id: string) => selectedRows.has(id);
</script>
<Table.Body>
{#each $pageRows as row (row.id)}
{@const rowSelected = isSelected(row.original.id)}
<Table.Row
class={cn(
"table-row-hover transition-colors",
rowSelected && "table-row-selected"
)}
>
<Table.Cell class="w-12">
<Checkbox.Root
checked={rowSelected}
onCheckedChange={() => toggleRowSelection(row.original.id)}
/>
</Table.Cell>
{#each row.cells as cell (cell.id)}
<Table.Cell class="table-cell">
<Render this={cell.render()} />
</Table.Cell>
{/each}
</Table.Row>
{/each}
</Table.Body>Key v4.1 patterns:
cn()for conditional class merging (no runtime overhead)table-row-selectedcustom utility with CSS variablestransition-colorsfor smooth state changes- No
@applyoverrides needed—compose utilities directly
Responsive Table with Tailwind v4.1
Use v4.1 utilities for responsive overflow handling:
<script lang="ts">
import { cn } from "$lib/utils";
</script>
<div class="w-full overflow-x-auto border border-[hsl(var(--color-table-border))] rounded-lg">
<table class={cn(
"w-full border-collapse",
"text-[hsl(var(--color-table-text))]",
)}>
<thead class="table-header">
{#each $headerRows as headerRow (headerRow.id)}
<tr>
{#each headerRow.cells as cell (cell.id)}
<th class="table-cell text-left">
<Render this={cell.render()} />
</th>
{/each}
</tr>
{/each}
</thead>
<tbody>
{#each $pageRows as row (row.id)}
<tr class="table-row-hover border-b border-[hsl(var(--color-table-border))]">
{#each row.cells as cell (cell.id)}
<td class="table-cell">
<Render this={cell.render()} />
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
<style>
/* Responsive: hide columns on mobile */
@media (max-width: 640px) {
:global(th:nth-child(n+3)),
:global(td:nth-child(n+3)) {
@apply hidden;
}
}
</style>Sorting with Tailwind v4.1
<script lang="ts">
import { ChevronDown, ChevronUp, ChevronsUpDown } from "@lucide/svelte";
import { cn } from "$lib/utils";
function handleSort(columnId: string) {
table.toggleSort(columnId);
}
</script>
<Table.Header>
{#each $headerRows as headerRow (headerRow.id)}
<Table.Row>
{#each headerRow.cells as cell (cell.id)}
<Table.Head>
<button
on:click={() => handleSort(cell.id)}
class={cn(
"flex items-center gap-2",
"hover:bg-[hsl(var(--color-table-row-hover))]",
"px-2 py-1 rounded transition-colors"
)}
>
<Render this={cell.render()} />
{#if cell.colDef.plugins?.sort?.disable === false}
<ChevronsUpDown class="w-4 h-4 opacity-50" />
{/if}
</button>
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>Filtering & Search
<script lang="ts">
import { Input } from "$lib/components/ui/input";
import { debounce } from "lodash-es";
let searchTerm = $state("");
let columnFilters = $state([]);
const debouncedFilter = debounce((value: string) => {
columnFilters = value
? [{ id: "name", value }]
: [];
}, 300);
function handleSearch(value: string) {
searchTerm = value;
debouncedFilter(value);
}
</script>
<div class="mb-4">
<Input
type="text"
placeholder="Filter by name..."
value={searchTerm}
on:input={(e) => handleSearch(e.currentTarget.value)}
class="max-w-sm"
/>
</div>Tailwind v4.1 + Svelte 5 Patterns
Use `cn()` for conditional styling (zero-runtime overhead):
<script lang="ts">
import { cn } from "$lib/utils";
interface Props {
selected?: boolean;
disabled?: boolean;
}
let { selected = false, disabled = false }: Props = $props();
$derived rowClasses = cn(
"table-row-hover px-4 py-3",
selected && "table-row-selected",
disabled && "opacity-50 cursor-not-allowed",
);
</script>
<tr class={rowClasses}>
<slot />
</tr>Combine CSS variables with utilities:
<!-- Use color tokens via CSS variables + Tailwind utilities -->
<div class="bg-[hsl(var(--color-table-row-selected))] border-l-4 border-l-primary">
Styled with CSS variables + Tailwind utilities
</div>Performance: Use `@layer utilities` for reusable patterns:
@layer utilities {
.table-cell {
@apply px-4 py-3 text-sm border-b border-[hsl(var(--color-table-border))];
}
.table-row-hover {
@apply hover:bg-[hsl(var(--color-table-row-hover))] transition-colors;
}
.table-row-selected {
@apply bg-[hsl(var(--color-table-row-selected))] border-l-4 border-l-primary;
}
}Common Pitfalls (v4.1)
| Issue | Cause | Fix |
|---|---|---|
| Colors don't update in dark mode | CSS variables not defined in .dark | Add all variables to both :root and .dark in app.css |
| Dynamic classes don't compile | Tailwind can't parse dynamic strings | Use template literals or cn() utility, not string concatenation |
| Styles flashing on load | CSS not imported in layout | Ensure import '../app.css' in root +layout.svelte |
| Table layout breaks on mobile | No responsive utilities applied | Use @media queries or responsive classes (sm:hidden) |
Performance Optimization
Virtual scrolling for 1000+ rows:
<script lang="ts">
import { VirtualScroller } from "@sveltejs/svelte-virtual";
</script>
<VirtualScroller items={$pageRows} let:item>
<tr class="table-row-hover">
<!-- render row -->
</tr>
</VirtualScroller>Debounce filters (reduce re-renders):
<script lang="ts">
import { debounce } from "lodash-es";
const debouncedFilter = debounce((value: string) => {
columnFilters = [{ id: "name", value }];
}, 300);
</script>Testing DataTables
import { render, screen } from "@testing-library/svelte";
import DataTable from "./DataTable.svelte";
it("applies selected row class", async () => {
render(DataTable, { props: { data: mockData } });
const row = screen.getByText("Alice").closest("tr");
expect(row).toHaveClass("table-row-selected");
});
it("respects dark mode CSS variables", () => {
document.documentElement.classList.add("dark");
const table = screen.getByRole("table");
const computed = window.getComputedStyle(table);
expect(computed.backgroundColor).toBe("rgb(38, 38, 38)"); // dark mode value
});Complete Example Component
<!-- src/lib/components/DataTable.svelte -->
<script lang="ts">
import { createTable, Render } from "svelte-headless-table";
import { addPagination, addSortBy } from "svelte-headless-table/plugins";
import { cn } from "$lib/utils";
import * as Table from "$lib/components/ui/table";
import { Checkbox } from "$lib/components/ui/checkbox";
interface Row {
id: string;
name: string;
email: string;
}
let { data }: { data: Row[] } = $props();
let selectedRows = $state<Set<string>>(new Set());
const table = createTable(data, {
page: addPagination({ initialPageSize: 10 }),
sort: addSortBy(),
});
const columns = table.createColumns([
table.column({
accessor: "name",
header: "Name",
cell: (item) => item.name,
}),
table.column({
accessor: "email",
header: "Email",
cell: (item) => item.email,
}),
]);
const { headerRows, pageRows } = table.createVM(columns);
</script>
<div class="border border-[hsl(var(--color-table-border))] rounded-lg overflow-hidden">
<table class="w-full">
<thead class="table-header">
{#each $headerRows as headerRow (headerRow.id)}
<tr>
{#each headerRow.cells as cell (cell.id)}
<th class="table-cell text-left">
<Render this={cell.render()} />
</th>
{/each}
</tr>
{/each}
</thead>
<tbody>
{#each $pageRows as row (row.id)}
{@const selected = selectedRows.has(row.original.id)}
<tr
class={cn(
"table-row-hover border-b border-[hsl(var(--color-table-border))]",
selected && "table-row-selected",
)}
>
<td class="table-cell w-12">
<Checkbox.Root
checked={selected}
onCheckedChange={() => {
if (selected) {
selectedRows.delete(row.original.id);
} else {
selectedRows.add(row.original.id);
}
selectedRows = selectedRows;
}}
/>
</td>
{#each row.cells as cell (cell.id)}
<td class="table-cell">
<Render this={cell.render()} />
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>Resources
- Tailwind v4.1: https://tailwindcss.com/docs/v4
- Vite Plugin: https://tailwindcss.com/docs/installation/using-vite
- TanStack Table: https://tanstack.com/table/v8/docs/guide/tables
- shadcn-svelte: https://www.shadcn-svelte.com/docs
- Svelte 5 Runes: https://svelte.dev/docs/svelte/$state
shadcn-svelte Data Tables
Build feature-rich data tables using TanStack Table v8 with Svelte 5 and shadcn-svelte components.
Installation
# Add table and data-table helpers
pnpm dlx shadcn-svelte@latest add table data-table
# Install TanStack Table core
pnpm add @tanstack/table-core
# Add supporting components as needed
pnpm dlx shadcn-svelte@latest add button dropdown-menu input checkboxProject Structure
routes/
your-route/
columns.ts # Column definitions
data-table.svelte # Main table component
data-table-actions.svelte # Row action menus
data-table-checkbox.svelte # Selection checkboxes
data-table-[feature]-button.svelte # Sortable headers
+page.svelte # Page that uses the tableCore Imports
// Always needed
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import {
createSvelteTable,
FlexRender,
renderComponent,
renderSnippet,
} from "$lib/components/ui/data-table/index.js";
// Feature-specific state types
import type {
PaginationState,
SortingState,
ColumnFiltersState,
VisibilityState,
RowSelectionState,
} from "@tanstack/table-core";
// Feature-specific row models
import {
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
} from "@tanstack/table-core";Column Definitions Pattern
Define columns in columns.ts:
import type { ColumnDef } from "@tanstack/table-core";
import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/index.js";
import { createRawSnippet } from "svelte";
export type YourDataType = {
id: string;
// ... other fields
};
export const columns: ColumnDef<YourDataType>[] = [
// Simple text column
{
accessorKey: "status",
header: "Status",
},
// Formatted cell with snippet
{
accessorKey: "amount",
header: () => {
const headerSnippet = createRawSnippet(() => ({
render: () => `<div class="text-end">Amount</div>`,
}));
return renderSnippet(headerSnippet);
},
cell: ({ row }) => {
const cellSnippet = createRawSnippet<[{ value: number }]>((getValue) => {
const { value } = getValue();
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(value);
return {
render: () => `<div class="text-end font-medium">${formatted}</div>`,
};
});
return renderSnippet(cellSnippet, { value: row.original.amount });
},
},
// Component-based cell (for complex UI)
{
id: "actions",
cell: ({ row }) => renderComponent(DataTableActions, { id: row.original.id }),
},
];Table Component Pattern (data-table.svelte)
Basic Structure
<script lang="ts" generics="TData, TValue">
import { type ColumnDef, getCoreRowModel } from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
};
let { data, columns }: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() { return data; },
columns,
getCoreRowModel: getCoreRowModel(),
});
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No results.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>Feature Implementation
Pagination
Add state and handlers:
<script lang="ts" generics="TData, TValue">
import { type PaginationState, getPaginationRowModel } from "@tanstack/table-core";
import { Button } from "$lib/components/ui/button/index.js";
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
const table = createSvelteTable({
// ... existing config
state: {
get pagination() { return pagination; },
},
onPaginationChange: (updater) => {
pagination = typeof updater === "function" ? updater(pagination) : updater;
},
getPaginationRowModel: getPaginationRowModel(),
});
</script>
<!-- Add controls -->
<div class="flex items-center justify-end space-x-2 pt-4">
<Button
variant="outline"
size="sm"
onclick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onclick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>Sorting
Create sortable header component (data-table-[field]-button.svelte):
<script lang="ts">
import type { ComponentProps } from "svelte";
import ArrowUpDownIcon from "@lucide/svelte/icons/arrow-up-down";
import { Button } from "$lib/components/ui/button/index.js";
let { variant = "ghost", ...restProps }: ComponentProps<typeof Button> = $props();
</script>
<Button {variant} {...restProps}>
Email
<ArrowUpDownIcon class="ms-2" />
</Button>Update column definition:
{
accessorKey: "email",
header: ({ column }) => renderComponent(DataTableEmailButton, {
onclick: column.getToggleSortingHandler(),
}),
}Add to table:
<script lang="ts" generics="TData, TValue">
import { type SortingState, getSortedRowModel } from "@tanstack/table-core";
let sorting = $state<SortingState>([]);
const table = createSvelteTable({
// ... existing config
state: {
get sorting() { return sorting; },
},
onSortingChange: (updater) => {
sorting = typeof updater === "function" ? updater(sorting) : updater;
},
getSortedRowModel: getSortedRowModel(),
});
</script>Filtering
<script lang="ts" generics="TData, TValue">
import { type ColumnFiltersState, getFilteredRowModel } from "@tanstack/table-core";
import { Input } from "$lib/components/ui/input/index.js";
let columnFilters = $state<ColumnFiltersState>([]);
const table = createSvelteTable({
// ... existing config
state: {
get columnFilters() { return columnFilters; },
},
onColumnFiltersChange: (updater) => {
columnFilters = typeof updater === "function" ? updater(columnFilters) : updater;
},
getFilteredRowModel: getFilteredRowModel(),
});
</script>
<!-- Add filter input -->
<div class="flex items-center py-4">
<Input
placeholder="Filter emails..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
oninput={(e) => table.getColumn("email")?.setFilterValue(e.currentTarget.value)}
onchange={(e) => table.getColumn("email")?.setFilterValue(e.currentTarget.value)}
class="max-w-sm"
/>
</div>Visibility
<script lang="ts" generics="TData, TValue">
import { type VisibilityState } from "@tanstack/table-core";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
let columnVisibility = $state<VisibilityState>({});
const table = createSvelteTable({
// ... existing config
state: {
get columnVisibility() { return columnVisibility; },
},
onColumnVisibilityChange: (updater) => {
columnVisibility = typeof updater === "function" ? updater(columnVisibility) : updater;
},
});
</script>
<!-- Add visibility dropdown -->
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline" class="ms-auto">Columns</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
{#each table.getAllColumns().filter((col) => col.getCanHide()) as column (column.id)}
<DropdownMenu.CheckboxItem
class="capitalize"
bind:checked={() => column.getIsVisible(), (v) => column.toggleVisibility(!!v)}
>
{column.id}
</DropdownMenu.CheckboxItem>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>Row Selection
Create checkbox component (data-table-checkbox.svelte):
<script lang="ts">
import type { ComponentProps } from "svelte";
import { Checkbox } from "$lib/components/ui/checkbox/index.js";
let {
checked = false,
onCheckedChange = (v) => (checked = v),
...restProps
}: ComponentProps<typeof Checkbox> = $props();
</script>
<Checkbox bind:checked={() => checked, onCheckedChange} {...restProps} />Add select column:
{
id: "select",
header: ({ table }) => renderComponent(DataTableCheckbox, {
checked: table.getIsAllPageRowsSelected(),
indeterminate: table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
"aria-label": "Select all",
}),
cell: ({ row }) => renderComponent(DataTableCheckbox, {
checked: row.getIsSelected(),
onCheckedChange: (value) => row.toggleSelected(!!value),
"aria-label": "Select row",
}),
enableSorting: false,
enableHiding: false,
}Add to table:
<script lang="ts" generics="TData, TValue">
import { type RowSelectionState } from "@tanstack/table-core";
let rowSelection = $state<RowSelectionState>({});
const table = createSvelteTable({
// ... existing config
state: {
get rowSelection() { return rowSelection; },
},
onRowSelectionChange: (updater) => {
rowSelection = typeof updater === "function" ? updater(rowSelection) : updater;
},
});
</script>
<!-- Show selected count -->
<div class="text-muted-foreground flex-1 text-sm">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>Row Actions Pattern
Create actions component (data-table-actions.svelte):
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
let { id }: { id: string } = $props();
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Open menu</span>
<EllipsisIcon />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>Actions</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => navigator.clipboard.writeText(id)}>
Copy ID
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item>View details</DropdownMenu.Item>
<DropdownMenu.Item>Edit</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>Complete Example with All Features
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
type PaginationState,
type SortingState,
type ColumnFiltersState,
type VisibilityState,
type RowSelectionState,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
} from "@tanstack/table-core";
import {
createSvelteTable,
FlexRender,
} from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import { Input } from "$lib/components/ui/input/index.js";
type DataTableProps<TData, TValue> = {
data: TData[];
columns: ColumnDef<TData, TValue>[];
};
let { data, columns }: DataTableProps<TData, TValue> = $props();
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
let sorting = $state<SortingState>([]);
let columnFilters = $state<ColumnFiltersState>([]);
let columnVisibility = $state<VisibilityState>({});
let rowSelection = $state<RowSelectionState>({});
const table = createSvelteTable({
get data() { return data; },
columns,
state: {
get pagination() { return pagination; },
get sorting() { return sorting; },
get columnFilters() { return columnFilters; },
get columnVisibility() { return columnVisibility; },
get rowSelection() { return rowSelection; },
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onPaginationChange: (updater) => {
pagination = typeof updater === "function" ? updater(pagination) : updater;
},
onSortingChange: (updater) => {
sorting = typeof updater === "function" ? updater(sorting) : updater;
},
onColumnFiltersChange: (updater) => {
columnFilters = typeof updater === "function" ? updater(columnFilters) : updater;
},
onColumnVisibilityChange: (updater) => {
columnVisibility = typeof updater === "function" ? updater(columnVisibility) : updater;
},
onRowSelectionChange: (updater) => {
rowSelection = typeof updater === "function" ? updater(rowSelection) : updater;
},
});
</script>
<div class="w-full">
<div class="flex items-center py-4">
<Input
placeholder="Filter..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
oninput={(e) => table.getColumn("email")?.setFilterValue(e.currentTarget.value)}
onchange={(e) => table.getColumn("email")?.setFilterValue(e.currentTarget.value)}
class="max-w-sm"
/>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline" class="ms-auto">Columns</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
{#each table.getAllColumns().filter((col) => col.getCanHide()) as column (column.id)}
<DropdownMenu.CheckboxItem
class="capitalize"
bind:checked={() => column.getIsVisible(), (v) => column.toggleVisibility(!!v)}
>
{column.id}
</DropdownMenu.CheckboxItem>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head class="[&:has([role=checkbox])]:ps-3">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell class="[&:has([role=checkbox])]:ps-3">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No results.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 pt-4">
<div class="text-muted-foreground flex-1 text-sm">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onclick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>Key Patterns
Svelte 5 State Management
Always use Svelte 5 runes:
$statefor reactive state$propsfor component propsgetaccessors increateSvelteTableconfig
Cell Rendering Strategies
1. Simple snippets (createRawSnippet): For basic HTML formatting 2. Components (renderComponent): For interactive UI, dropdowns, buttons 3. Direct values: For plain text or numbers
State Updater Pattern
All state handlers follow this pattern:
onStateChange: (updater) => {
state = typeof updater === "function" ? updater(state) : updater;
}Common Pitfalls
- Forgetting
getaccessors in state config - Not binding both
oninputandonchangefor filters - Missing row models (pagination, sorting, filtering)
- Using wrong import path for data-table helpers
Usage in Pages
<!-- routes/your-route/+page.svelte -->
<script lang="ts">
import DataTable from "./data-table.svelte";
import { columns } from "./columns.js";
let { data } = $props();
</script>
<DataTable data={data.items} {columns} />// routes/your-route/+page.server.ts
export async function load() {
const items = await fetchYourData();
return { items };
}Complex Build Workflow
For building complete features requiring multiple shadcn components.
Phase 1: Requirements Analysis
Input: User request for a complete feature/section
Steps:
1. Call shadcn___get_project_registries - Note all available registries
2. Break down the request into components needed:
Example: "login form" needs:
- Form (validation)
- Input (email, password)
- Button (submit)
- Label (field labels)
- Card (container)
- Alert (error messages)3. For each identified component:
- Call
shadcn___search_items_in_registries - Verify it exists in registry
- Note exact name
4. Output component hierarchy:
## Feature: [Name]
## Components Required:
- form (validation and submission)
- input (email and password fields)
- button (submit action)
- card (form container)
- alert (error display)
## Component Hierarchy:
Card
└── Form
├── Label + Input (email)
├── Label + Input (password)
├── Button (submit)
└── Alert (errors)Phase 2: Component Research
Input: Component list from Phase 1
Steps:
1. For each component:
a. Get implementation details:
shadcn___view_items_in_registries(items: ["@shadcn/component"])- Note file dependencies
- Note key props
b. Get examples:
shadcn___get_item_examples_from_registries(registries, query: "component-demo")- For forms: get validation examples
- For data: get loading state examples
2. Get installation command for ALL components at once:
shadcn___get_add_command_for_items(items: ["@shadcn-svelte/form", "@shadcn-svelte/input", ...])3. Output research summary with:
- Installation commands
- Key imports for each component
- Relevant example code snippets
- Important props to use
Phase 3: Implementation
Input: Requirements + Research from previous phases
Steps:
1. Build implementation following:
- Use EXACT imports from research
- Follow hierarchy from requirements
- Adapt examples to match use case
- Add proper TypeScript types
- Include state management (useState, form hooks)
- Add error handling
2. Run audit:
shadcn___get_audit_checklist- Verify best practices followed
3. OUTPUT complete implementation:
<!-- All necessary imports -->
<script lang="ts">
import { Form, FormControl, FormField } from "$lib/components/ui/form"
import { Input } from "$lib/components/ui/input"
import { Button } from "$lib/components/ui/button"
// Full implementation
</script>4. Include setup instructions:
- Installation commands needed
- Where to add the component
- Any additional setup (providers, configs)
Example: Login Form
Phase 1 Output:
Components: card, form, input, button, label, alert
Hierarchy: Card > Form > (Label+Input)*2 + Button + AlertPhase 2 Output:
npx shadcn-svelte@latest add card form input button label alertPhase 3 Output:
<script lang="ts">
import { enhance } from '$app/forms';
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "$lib/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "$lib/components/ui/card"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "$lib/components/ui/form"
import { Input } from "$lib/components/ui/input"
const formSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
export let form: HTMLFormElement;
// SvelteKit form action with progressive enhancement
const handleSubmit = enhance(() => {
return async ({ result, data }) => {
if (result.type === 'success') {
// Handle successful login
console.log('Login successful:', data);
} else {
// Handle errors
console.error('Login failed:', result.error);
}
};
});
</script>
<Card>
<CardHeader>
<CardTitle>Login</CardTitle>
</CardHeader>
<CardContent>
<form method="POST" action="/login" use:handleSubmit class="space-y-4">
<div class="space-y-2">
<label for="email">Email</label>
<Input
id="email"
name="email"
type="email"
placeholder="email@example.com"
required
/>
</div>
<div class="space-y-2">
<label for="password">Password</label>
<Input
id="password"
name="password"
type="password"
placeholder="Enter password"
required
/>
</div>
<Button type="submit" class="w-full">Login</Button>
</form>
</CardContent>
</Card>Related skills
FAQ
Why doesn't my TanStack Table update in Svelte 5?
Svelte 5 runes require get accessors; passing data directly to createSvelteTable means data never updates after init.
Why are Tailwind classes missing in the production build?
Tailwind v4.1 ignores @tailwind directives; you must use @import "tailwindcss" and place @tailwindcss/vite before sveltekit.