name: svelte description: "[Applies to: **/*.{svelte,js,ts}] Definitive guide for writing clean, performant, and maintainable Svelte 5 applications using the runes API and modern TypeScript practices." source: "cursor_mdc"
Svelte Best Practices (Svelte 5 + TypeScript)
This guide outlines the definitive best practices for developing Svelte 5 applications with TypeScript. We prioritize explicit reactivity, strong typing, and compiler-driven performance.
1. Code Organization and Structure
Always structure your Svelte components for clarity and maintainability.
1.1. HTML-First Component Design
Keep your component logic, markup, and styles co-located within the .svelte file. This is Svelte's core philosophy.
❌ BAD: Over-abstracting into separate JS files for simple logic
<!-- MyComponent.svelte -->
<script lang="ts">
import { calculateValue } from './utils'; // Unnecessary abstraction
let count = $state(0);
let computed = $derived(calculateValue(count));
</script>
✅ GOOD: Keep related logic within the component
<!-- MyComponent.svelte -->
<script lang="ts">
let count = $state(0);
let computed = $derived(count * 2 + 1); // Simple logic stays here
</script>
1.2. TypeScript Everywhere
Use lang="ts" in all <script> tags. Configure svelte-check in CI and use the VS Code extension for real-time feedback.
❌ BAD: Missing lang="ts"
<script>
let name: string = 'world'; // No type checking
</script>
✅ GOOD: Explicitly enable TypeScript
<script lang="ts">
let name: string = 'world'; // Full type safety
</script>
2. Component Architecture (Svelte 5 Runes)
Svelte 5's runes API makes reactivity explicit and powerful. Embrace it fully.
2.1. Explicit Reactive State with $state
Declare all reactive state using $state. This makes your component's reactivity clear and refactor-friendly.
❌ BAD: Implicit reactivity (Svelte 4 style)
<script lang="ts">
let count = 0; // Not reactive outside top-level
function increment() { count++; }
</script>
✅ GOOD: Explicit $state for all reactive variables
<script lang="ts">
let count = $state(0); // Clearly reactive
function increment() { count++; }
</script>
2.2. Derived State with $derived
Use $derived for any value that is a pure computation of other reactive state.
❌ BAD: $effect or $: for derived values
<script lang="ts">
let count = $state(0);
$effect(() => { // Incorrectly using effect for derivation
console.log('Count changed:', count);
// This is a side effect, not a derivation
});
$: doubled = count * 2; // Svelte 4 style, less explicit
</script>
✅ GOOD: $derived for pure computations
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2); // Clearly a derived value
</script>
2.3. Side Effects with $effect
Reserve $effect for actual side effects (e.g., DOM manipulation, API calls, logging).
❌ BAD: Mixing derivations and effects
<script lang="ts">
let count = $state(0);
$: if (count > 5) { // Svelte 4 style, combines derivation and effect
alert('Count is too high!');
}
</script>
✅ GOOD: Clear separation of concerns
<script lang="ts">
let count = $state(0);
$effect(() => { // Clearly a side effect
if (count > 5) {
alert('Count is too high!');
}
});
</script>
2.4. Component Properties with $props
Declare all component properties using the $props() rune and destructuring. This provides powerful type safety and flexibility.
❌ BAD: export let (Svelte 4 style)
<script lang="ts">
export let name: string;
export let age: number = 30;
</script>
✅ GOOD: $props() for properties
<script lang="ts">
interface Props {
name: string;
age?: number; // Optional prop
}
let { name, age = 30 }: Props = $props();
</script>
2.5. Event Handling
Remove the on: prefix for event handlers. They are now standard properties.
❌ BAD: on:click
<button on:click={() => alert('Clicked!')}>Click me</button>
✅ GOOD: onclick (standard HTML attribute)
<button onclick={() => alert('Clicked!')}>Click me</button>
2.6. Slots and Snippets
Use {@render} for rendering slots and snippets, providing type safety and better control over content.
❌ BAD: Untyped slots
<!-- MyComponent.svelte -->
<slot />
✅ GOOD: Typed snippets for predictable content
<!-- MyComponent.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
let { header }: { header: Snippet } = $props();
</script>
<div>
{@render header()}
<slot />
</div>
3. State Management
Prioritize local component state with $state. Use Svelte stores for truly global or complex cross-component state.
3.1. Local State First
Most state should live within the component using $state.
❌ BAD: Over-reliance on global stores for simple component state
// store.ts
import { writable } from 'svelte/store';
export const localCounter = writable(0); // Unnecessary global
✅ GOOD: Use $state for component-local reactivity
<script lang="ts">
let count = $state(0); // Local to this component
</script>
3.2. Svelte Stores for Global State
For state shared across many components or complex application-wide state, use Svelte's built-in stores.
// src/lib/stores/auth.ts
import { writable } from 'svelte/store';
export const isAuthenticated = writable(false);
export const user = writable<{ id: string; name: string } | null>(null);
<!-- AuthStatus.svelte -->
<script lang="ts">
import { isAuthenticated, user } from '$lib/stores/auth';
</script>
{#if $isAuthenticated}
<p>Welcome, {$user?.name}!</p>
{:else}
<p>Please log in.</p>
{/if}
4. Performance Considerations
SvelteKit handles many optimizations automatically. Supplement these with mindful coding.
4.1. {#each} with Keys
Always provide a unique key to {#each} blocks for efficient DOM updates.
❌ BAD: Missing key
{#each items as item}
<ItemComponent {item} />
{/each}
✅ GOOD: Use a unique key
{#each items as item (item.id)}
<ItemComponent {item} />
{/each}
4.2. Dynamic Imports for Lazy Loading
Lazy-load components or modules that aren't immediately needed to reduce initial bundle size.
<script lang="ts">
let showModal = $state(false);
async function openModal() {
const { default: Modal } = await import('./Modal.svelte');
// Render Modal component
showModal = true;
}
</script>
{#if showModal}
<Modal />
{/if}
<button onclick={openModal}>Open Modal</button>
5. Common Pitfalls and Gotchas
Avoid these common mistakes to ensure robust and predictable Svelte applications.
5.1. Misunderstanding $effect Dependencies
$effect runs when its dependencies change. Be explicit about what it depends on.
❌ BAD: Implicit dependencies or missing cleanup
<script lang="ts">
let count = $state(0);
$effect(() => {
// This effect might not re-run if `count` isn't directly used
// or if `doSomething` has internal state changes not tracked by Svelte.
doSomething(count);
});
</script>
✅ GOOD: Explicit dependencies and cleanup
<script lang="ts">
let count = $state(0);
$effect(() => {
const timer = setInterval(() => {
console.log('Count is', count); // `count` is a dependency
}, 1000);
return () => clearInterval(timer); // Cleanup function
});
</script>
5.2. Forgetting generics Attribute
When creating generic components, remember to add the generics attribute to the <script> tag.
❌ BAD: Untyped generic props
<script lang="ts">
interface Props { items: any[]; } // Loses type safety
let { items }: Props = $props();
</script>
✅ GOOD: Explicit generics for type safety
<script lang="ts" generics="Item extends { id: string }">
interface Props { items: Item[]; }
let { items }: Props = $props();
</script>
6. Accessibility
Build accessible Svelte applications by default.
6.1. Semantic HTML
Always prefer semantic HTML elements over generic divs or spans.
❌ BAD: Non-semantic button
<div onclick={handleClick} role="button" tabindex="0">Click me</div>
✅ GOOD: Semantic HTML element
<button onclick={handleClick}>Click me</button>
6.2. ARIA Attributes
Use ARIA attributes when semantic HTML isn't sufficient, but only when necessary.
<label for="username">Username</label>
<input id="username" type="text" aria-describedby="username-hint" />
<p id="username-hint">Your username must be unique.</p>
7. Testing Approaches
Implement a robust testing strategy for Svelte applications.
7.1. Unit Testing with Vitest and Svelte Testing Library
Use vitest for your test runner and @testing-library/svelte for component testing.
// src/lib/components/Counter.test.ts
import { render, screen } from '@testing-library/svelte';
import { expect, test } from 'vitest';
import Counter from './Counter.svelte';
test('Counter increments value on click', async () => {
render(Counter);
const button = screen.getByRole('button', { name: /Count:/i });
expect(button).toHaveTextContent('Count: 0');
await button.click();
expect(button).toHaveTextContent('Count: 1');
});
7.2. End-to-End (E2E) Testing
For critical user flows, use Playwright or Cypress.
// tests/example.spec.ts (Playwright)
import { test, expect } from '@playwright/test';
test('homepage has title and intro text', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/SvelteKit/);
await expect(page.locator('h1')).toHaveText('Welcome to SvelteKit');
});