Build accessible, customizable UI components for SvelteKit 2 / Svelte 5 projects using shadcn-svelte, Tailwind CSS v4, Deno, Storybook, Lucide icons, LayerChart, and motion-sv. Use when creating component-based Svelte applications that need production-ready, styled UI elements. Also covers Skeleton UI and Melt UI for library selection, Storybook for component dev, LayerChart for data visualization, and Lucide for icons. Triggers: "add components", "UI components", "build UI", "install component", "create form", "create dialog", "svelte components", "shadcn-svelte", "skeleton ui", "melt ui", "storybook", "component story", "chart", "visualization", "layerchart", "bar chart", "line chart", "pie chart", "icon", "lucide", "add icon", "tailwind", "tailwind config", "theme", "dark mode", "animation", "motion", "motion-sv", "framer motion", "animate", "transition", "gesture", "whileHover", "exit animation"
Guide for working with shadcn-svelte in a Deno-based SvelteKit 2 / Svelte 5 project with Storybook, Lucide icons, LayerChart, motion-sv, and Tailwind CSS v4.
All examples use Svelte 5 runes ($state, $props, $derived, $effect),
Deno as the runtime, and Tailwind CSS v4 with @tailwindcss/vite.
This skill has detailed reference files. Read the relevant one before diving in:
| Reference | When to read |
|---|---|
references/tailwind-v4.md | Tailwind CSS v4.x setup, @theme, CSS-first config, @utility, v3→v4 migration |
references/lucide-icons.md | Icon usage with @lucide/svelte, direct imports, dynamic icons, accessibility |
references/layerchart.md | Data visualization with LayerChart v2 (next), shadcn-svelte chart integration, tooltips, simplified charts |
references/storybook.md | Svelte CSF v5 patterns, templates, play functions, SvelteKit mocking, MDX docs, configuration |
references/motion-sv.md | Animations with motion-sv (Framer Motion port), gestures, layout animations, AnimatePresence, page transitions |
references/layerchart.md)references/lucide-icons.md)references/motion-sv.md)| Library | Type | Best For | Learning Curve |
|---|---|---|---|
| shadcn-svelte | Copy-paste components | Full customization, TypeScript-first | Medium |
| Skeleton UI | Installable package | Rapid development, themes | Low |
| Melt UI | Headless primitives | Maximum accessibility control | High |
shadcn-svelte is copy-paste component infrastructure. Components live in
$lib/components/ui/ — you own and modify the code directly. Built on Bits
UI for accessibility, Tailwind CSS v4 for styling, and Svelte 5 runes
for reactivity.
deno run -A npm:sv create my-app
cd my-app
deno add -D npm:tailwindcss npm:@tailwindcss/vite
deno run -A npm:shadcn-svelte@latest init
// vite.config.ts
import { defineConfig } from "vite";
import { sveltekit } from "@sveltejs/kit/vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
});
src/app.css)@import "tailwindcss";
No tailwind.config.js needed. See references/tailwind-v4.md for @theme,
@utility, and advanced config.
src/routes/+layout.svelte)<script>
import '../app.css'
let { children } = $props();
</script>
{@render children()}
deno.json){
"tasks": {
"dev": "deno run -A npm:vite dev",
"build": "deno run -A npm:vite build",
"preview": "deno run -A npm:vite preview",
"storybook": "deno run -A npm:storybook dev -p 6006",
"build-storybook": "deno run -A npm:storybook build"
},
"imports": { "$lib/": "./src/lib/" },
"compilerOptions": { "verbatimModuleSyntax": true },
"nodeModulesDir": "auto"
}
"nodeModulesDir": "auto"is required for shadcn-svelte and Storybook under Deno.
deno run -A npm:shadcn-svelte@latest add button card dialog
deno run -A npm:shadcn-svelte@latest add --all # or add everything
deno run -A npm:shadcn-svelte@latest list # see available
Components install to: src/lib/components/ui/[component-name]/
| Svelte 4 | Svelte 5 |
|---|---|
export let prop | let { prop } = $props() |
let x = value (reactive) | let x = $state(value) |
$: derived = x + 1 | let derived = $derived(x + 1) |
$: { sideEffect() } | $effect(() => { sideEffect() }) |
<slot /> | {@render children()} |
<slot name="x" /> | {@render x()} |
let:attrs | {#snippet children({ props })} |
asChild let:builder | {#snippet child({ props })} |
on:click={handler} | onclick={handler} |
<script context="module"> | <script module> |
createEventDispatcher() | Callback props |
<!-- Svelte 4 (old) -->
<Dialog.Trigger asChild let:builder>
<Button builders={[builder]}>Open</Button>
</Dialog.Trigger>
<!-- Svelte 5 (current) -->
<Dialog.Trigger>
{#snippet child({ props })}
<Button {...props}>Open</Button>
{/snippet}
</Dialog.Trigger>
<script lang="ts">
import { Button } from "$lib/components/ui/button";
</script>
<Button variant="default">Click me</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Outlined</Button>
deno run -A npm:shadcn-svelte@latest add form input label
deno add npm:sveltekit-superforms npm:zod
<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";
let { data } = $props();
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>
{#snippet children({ props })}
<Form.Label>Email</Form.Label>
<Input {...props} type="email" bind:value={$formData.email} />
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Button type="submit">Submit</Button>
</form>
<script lang="ts">
import * as Dialog from "$lib/components/ui/dialog";
import { Button } from "$lib/components/ui/button";
let open = $state(false);
</script>
<Dialog.Root bind:open>
<Dialog.Trigger>
{#snippet child({ props })}
<Button {...props}>Open</Button>
{/snippet}
</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 onclick={() => (open = false)}>Close</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
deno run -A npm:shadcn-svelte@latest add table data-table button dropdown-menu checkbox input
deno add npm:@tanstack/table-core
Key patterns:
$state + get accessors in createSvelteTable(updater) => state = typeof updater === "function" ? updater(state) : updatercreateRawSnippet → renderSnippet for HTML;
renderComponent for componentsgetPaginationRowModel,
getSortedRowModel, getFilteredRowModel)oninput and onchangeSee the shadcn-svelte data table docs for complete examples.
For the full reference including SvelteKit mocking, play functions, shared
templates, MDX docs, and configuration options, see references/storybook.md.
deno run -A npm:storybook@latest init --type sveltekit
.storybook/preview.ts)import type { Preview } from "@storybook/svelte";
import "../src/app.css"; // Tailwind + theme variables
const preview: Preview = {
parameters: {
controls: { matchers: { color: /(background|color)$/i, date: /Date$/i } },
},
};
export default preview;
.stories.svelte)<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Button from './button.svelte';
const { Story } = defineMeta({
component: Button,
tags: ['autodocs'],
args: { children: 'Click me' as any },
argTypes: {
variant: { control: 'select', options: ['default', 'destructive', 'outline', 'ghost'] },
size: { control: 'select', options: ['default', 'sm', 'lg', 'icon'] },
},
render: template, // Default template for all stories
});
</script>
{#snippet template(args)}
<Button {...args}>{args.children}</Button>
{/snippet}
<!-- Uses default template from `render: template` above -->
<Story name="Default" args={{ variant: 'default' }} />
<Story name="Destructive" args={{ variant: 'destructive' }} />
<!-- Override with a per-story template -->
<Story name="With Icon">
{#snippet template(args)}
<Button {...args}>❤️ Save</Button>
{/snippet}
</Story>
<!-- Static story (no args reactivity) -->
<Story name="Static" asChild>
<Button>I'm static</Button>
</Story>
<script module lang="ts"> not <script context="module">defineMeta({ component, render: template }) — sets default template{#snippet template(args)} for dynamic stories (NOT
{#snippet children(args)})<Story> (no snippet needed)asChild for static stories that don't consume argstags: ['autodocs'] for auto-generated docs (replaces autodocs prop)onclick not on:click for Svelte 5 event handlerssrc/
├── lib/
│ ├── components/
│ │ ├── ui/ # shadcn-svelte components
│ │ │ ├── button/
│ │ │ │ ├── index.ts
│ │ │ │ ├── button.svelte
│ │ │ │ └── Button.stories.svelte
│ │ │ ├── chart/ # Chart wrapper (from shadcn add chart)
│ │ │ ├── dialog/
│ │ │ └── [...]
│ │ └── custom/
│ └── utils/
│ └── cn.ts
├── routes/
└── app.css
.storybook/
├── main.ts
└── preview.ts
deno.json
Deno package resolution: Set "nodeModulesDir": "auto" in deno.json.
shadcn CLI not detecting project:
deno run -A npm:shadcn-svelte@latest init --yes
Storybook can't find components: Verify "$lib/": "./src/lib/" in
deno.json imports.
Styling not applying: Check @tailwindcss/vite is in vite.config.ts
plugins and src/app.css has @import "tailwindcss". For Storybook, verify
preview.ts imports ../src/app.css.
references/tailwind-v4.mdreferences/lucide-icons.mdreferences/layerchart.mdreferences/motion-sv.md