toolchains/javascript/frameworks/svelte/SKILL.md
Svelte 5 - Reactive UI framework with compiler magic, Runes API, SvelteKit full-stack framework, SSR/SSG, minimal JavaScript
npx skillsauth add bobmatnyc/claude-mpm-skills svelteInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Svelte is a compiler-based reactive UI framework that shifts work from runtime to build time. Unlike React/Vue, Svelte compiles components to highly optimized vanilla JavaScript with minimal overhead. Svelte 5 introduces Runes API for explicit, fine-grained reactivity.
Key Features:
Installation:
# Create new SvelteKit project
npm create svelte@latest my-app
cd my-app
npm install
npm run dev
# Or Svelte only (no SvelteKit)
npm create vite@latest my-app -- --template svelte-ts
<script lang="ts">
// Reactive state - automatically tracks changes
let count = $state(0);
let user = $state({ name: 'Alice', age: 30 });
// Arrays and objects are deeply reactive
let todos = $state<Todo[]>([]);
function addTodo(text: string) {
todos.push({ id: Date.now(), text, done: false });
// No need for todos = [...todos] like React!
}
function increment() {
count++; // Triggers reactivity
}
</script>
<button onclick={increment}>
Clicked {count} times
</button>
<script lang="ts">
let firstName = $state('John');
let lastName = $state('Doe');
// Automatically updates when dependencies change
let fullName = $derived(`${firstName} ${lastName}`);
let greeting = $derived(`Hello, ${fullName}`);
// Complex derivations
let items = $state([1, 2, 3, 4, 5]);
let total = $derived(items.reduce((sum, n) => sum + n, 0));
let average = $derived(total / items.length);
</script>
<p>{greeting}</p>
<p>Average: {average.toFixed(2)}</p>
<script lang="ts">
let count = $state(0);
let logs = $state<string[]>([]);
// Runs when dependencies change
$effect(() => {
console.log(`Count is now: ${count}`);
logs.push(`Count changed to ${count}`);
// Optional cleanup function
return () => {
console.log('Cleaning up previous effect');
};
});
// Effect with external subscriptions
$effect(() => {
const interval = setInterval(() => {
count++;
}, 1000);
return () => clearInterval(interval);
});
</script>
<script lang="ts">
interface Props {
title: string;
count?: number;
onUpdate?: (value: number) => void;
}
// Type-safe props with defaults
let { title, count = 0, onUpdate } = $props<Props>();
// Props are read-only, but can derive from them
let displayTitle = $derived(title.toUpperCase());
</script>
<h1>{displayTitle}</h1>
<p>Count: {count}</p>
<button onclick={() => onUpdate?.(count + 1)}>
Increment
</button>
<!-- Child: SearchInput.svelte -->
<script lang="ts">
interface Props {
value: string;
placeholder?: string;
}
// Allows parent to bind to this prop
let { value = $bindable(''), placeholder = 'Search...' } = $props<Props>();
</script>
<input bind:value {placeholder} type="search" />
<!-- Parent.svelte -->
<script lang="ts">
import SearchInput from './SearchInput.svelte';
let query = $state('');
let results = $derived(query ? search(query) : []);
</script>
<SearchInput bind:value={query} />
<p>Found {results.length} results for "{query}"</p>
<!-- Counter.svelte -->
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2);
function increment() {
count++;
}
function decrement() {
count--;
}
</script>
<div class="counter">
<button onclick={decrement}>-</button>
<span>Count: {count} (Doubled: {doubled})</span>
<button onclick={increment}>+</button>
</div>
<style>
.counter {
display: flex;
gap: 1rem;
align-items: center;
}
button {
padding: 0.5rem 1rem;
font-size: 1.2rem;
}
</style>
<script lang="ts">
let loggedIn = $state(false);
let user = $state<User | null>(null);
let loading = $state(true);
</script>
{#if loading}
<p>Loading...</p>
{:else if loggedIn && user}
<p>Welcome, {user.name}!</p>
{:else}
<p>Please log in</p>
{/if}
<script lang="ts">
interface Todo {
id: number;
text: string;
done: boolean;
}
let todos = $state<Todo[]>([
{ id: 1, text: 'Learn Svelte', done: true },
{ id: 2, text: 'Build app', done: false }
]);
function toggle(id: number) {
const todo = todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
</script>
<ul>
{#each todos as todo (todo.id)}
<li class:done={todo.done}>
<input
type="checkbox"
checked={todo.done}
onchange={() => toggle(todo.id)}
/>
{todo.text}
</li>
{/each}
</ul>
<style>
.done {
text-decoration: line-through;
opacity: 0.6;
}
</style>
my-app/
├── src/
│ ├── routes/
│ │ ├── +page.svelte # Home page
│ │ ├── +page.ts # Universal load
│ │ ├── +page.server.ts # Server load
│ │ ├── +layout.svelte # Shared layout
│ │ ├── about/
│ │ │ └── +page.svelte # /about
│ │ └── blog/
│ │ ├── +page.svelte # /blog
│ │ └── [slug]/
│ │ └── +page.svelte # /blog/my-post
│ ├── lib/
│ │ ├── components/
│ │ ├── stores/
│ │ └── utils/
│ └── app.html
├── static/ # Static assets
└── svelte.config.js
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/posts/${params.slug}`);
const post = await response.json();
return {
post
};
};
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data } = $props<{ data: PageData }>();
</script>
<article>
<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>
</article>
// src/routes/admin/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user?.isAdmin) {
throw redirect(303, '/login');
}
const users = await db.users.findMany();
return {
users
};
};
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8)
});
export const actions = {
default: async ({ request, cookies }) => {
const formData = await request.formData();
const data = Object.fromEntries(formData);
const result = schema.safeParse(data);
if (!result.success) {
return fail(400, {
errors: result.error.flatten().fieldErrors
});
}
const user = await authenticateUser(result.data);
if (!user) {
return fail(401, { message: 'Invalid credentials' });
}
cookies.set('session', user.sessionToken, { path: '/' });
throw redirect(303, '/dashboard');
}
} satisfies Actions;
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import type { ActionData } from './$types';
let { form } = $props<{ form?: ActionData }>();
</script>
<form method="POST">
<input name="email" type="email" required />
{#if form?.errors?.email}
<span class="error">{form.errors.email[0]}</span>
{/if}
<input name="password" type="password" required />
{#if form?.errors?.password}
<span class="error">{form.errors.password[0]}</span>
{/if}
{#if form?.message}
<p class="error">{form.message}</p>
{/if}
<button type="submit">Log In</button>
</form>
// src/lib/stores/cart.svelte.ts
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
function createCart() {
let items = $state<CartItem[]>([]);
let total = $derived(
items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
let itemCount = $derived(
items.reduce((sum, item) => sum + item.quantity, 0)
);
return {
get items() { return items; },
get total() { return total; },
get itemCount() { return itemCount; },
addItem(item: Omit<CartItem, 'quantity'>) {
const existing = items.find(i => i.id === item.id);
if (existing) {
existing.quantity++;
} else {
items.push({ ...item, quantity: 1 });
}
},
removeItem(id: number) {
items = items.filter(i => i.id !== id);
},
clear() {
items = [];
}
};
}
export const cart = createCart();
<!-- Usage -->
<script lang="ts">
import { cart } from '$lib/stores/cart.svelte';
</script>
<div>
<p>Items: {cart.itemCount}</p>
<p>Total: ${cart.total.toFixed(2)}</p>
<button onclick={() => cart.addItem({ id: 1, name: 'Widget', price: 9.99 })}>
Add Widget
</button>
</div>
// src/lib/stores/user.ts
import { writable, derived } from 'svelte/store';
interface User {
id: number;
name: string;
email: string;
}
function createUserStore() {
const { subscribe, set, update } = writable<User | null>(null);
return {
subscribe,
login: (user: User) => set(user),
logout: () => set(null),
updateName: (name: string) => update(u => u ? { ...u, name } : null)
};
}
export const user = createUserStore();
// Derived store
export const isLoggedIn = derived(user, $user => $user !== null);
<!-- Usage with $ prefix -->
<script lang="ts">
import { user, isLoggedIn } from '$lib/stores/user';
</script>
{#if $isLoggedIn}
<p>Welcome, {$user?.name}!</p>
{:else}
<p>Please log in</p>
{/if}
<script lang="ts">
import { fade, slide, fly, scale } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
let visible = $state(true);
</script>
<button onclick={() => visible = !visible}>
Toggle
</button>
{#if visible}
<div transition:fade={{ duration: 300 }}>
Fades in and out
</div>
<div transition:slide={{ duration: 300 }}>
Slides in and out
</div>
<div transition:fly={{ y: 200, duration: 500, easing: quintOut }}>
Flies in from bottom
</div>
{/if}
// src/lib/transitions.ts
export function blur(node: HTMLElement, { duration = 300 }) {
return {
duration,
css: (t: number) => `
opacity: ${t};
filter: blur(${(1 - t) * 10}px);
`
};
}
<script lang="ts">
import { blur } from '$lib/transitions';
let show = $state(true);
</script>
{#if show}
<div transition:blur={{ duration: 500 }}>
Custom blur transition
</div>
{/if}
<script lang="ts">
import { flip } from 'svelte/animate';
import { quintOut } from 'svelte/easing';
let items = $state([1, 2, 3, 4, 5]);
function shuffle() {
items = items.sort(() => Math.random() - 0.5);
}
</script>
<button onclick={shuffle}>Shuffle</button>
<ul>
{#each items as item (item)}
<li animate:flip={{ duration: 500, easing: quintOut }}>
{item}
</li>
{/each}
</ul>
// src/lib/actions.ts
export function clickOutside(node: HTMLElement, callback: () => void) {
const handleClick = (event: MouseEvent) => {
if (!node.contains(event.target as Node)) {
callback();
}
};
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
};
}
<script lang="ts">
import { clickOutside } from '$lib/actions';
let showMenu = $state(false);
</script>
<button onclick={() => showMenu = !showMenu}>
Menu
</button>
{#if showMenu}
<div
class="menu"
use:clickOutside={() => showMenu = false}
>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
{/if}
<!-- Card.svelte -->
<script lang="ts">
let { title } = $props<{ title?: string }>();
</script>
<div class="card">
<header>
{#if title}
<h2>{title}</h2>
{:else}
<slot name="header">Default Header</slot>
{/if}
</header>
<main>
<slot>Default content</slot>
</main>
<footer>
<slot name="footer" />
</footer>
</div>
<!-- Usage -->
<Card title="My Card">
<p>This is the main content</p>
<div slot="footer">
<button>Action</button>
</div>
</Card>
<script lang="ts">
let Component = $state<typeof import('./A.svelte').default | typeof import('./B.svelte').default>(A);
let tag = $state<'div' | 'span'>('div');
</script>
<!-- Dynamic component -->
<svelte:component this={Component} />
<!-- Dynamic HTML element -->
<svelte:element this={tag}>
Content
</svelte:element>
<!-- Window events -->
<svelte:window
onresize={() => console.log('Window resized')}
onkeydown={(e) => console.log(e.key)}
/>
<!-- Document head -->
<svelte:head>
<title>My Page</title>
<meta name="description" content="Description" />
</svelte:head>
| React | Svelte 5 | Notes |
|-------|----------|-------|
| useState(0) | $state(0) | Direct state |
| useMemo(() => x * 2, [x]) | $derived(x * 2) | Auto-tracked |
| useEffect(() => {...}, [x]) | $effect(() => {...}) | Auto deps |
| props.name | let { name } = $props() | Destructure |
| {show && <div />} | {#if show}<div />{/if} | Explicit |
| Vue 3 | Svelte 5 | Notes |
|-------|----------|-------|
| ref(0) | $state(0) | Direct state |
| computed(() => x * 2) | $derived(x * 2) | Similar |
| watch(() => x, ...) | $effect(() => {...}) | Auto-tracked |
| defineProps<{ name: string }>() | let { name } = $props<Props>() | Type-safe |
| v-if="show" | {#if show} | Block syntax |
{#each items as item (item.id)}const Component = await import('./Heavy.svelte')// Counter.test.ts
import { render, fireEvent } from '@testing-library/svelte';
import { expect, test } from 'vitest';
import Counter from './Counter.svelte';
test('increments counter on click', async () => {
const { getByText } = render(Counter);
const button = getByText('+');
await fireEvent.click(button);
expect(getByText(/Count: 1/)).toBeInTheDocument();
});
// tests/login.test.ts
import { expect, test } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', '[email protected]');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('text=Welcome')).toBeVisible();
});
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel'; // or adapter-node, adapter-static
export default {
kit: {
adapter: adapter()
}
};
Available Adapters:
adapter-auto: Auto-detect platformadapter-vercel: Vercel deploymentadapter-node: Node.js serveradapter-static: Static site generationadapter-cloudflare: Cloudflare Pages/Workersadapter-netlify: Netlify deploymentdevelopment
Axum (Rust) web framework patterns for production APIs: routers/extractors, state, middleware, error handling, tracing, graceful shutdown, and testing
development
Optimize web performance using Core Web Vitals, modern patterns (View Transitions, Speculation Rules), and framework-specific techniques
development
Best practices for documenting APIs and code interfaces, eliminating redundant documentation guidance per agent.
development
Comprehensive API design patterns covering REST, GraphQL, gRPC, versioning, authentication, and modern API best practices