Raw view: Svelte

No invisible, zero-width, or bidi-control characters detected.

---
description: Definitive guide for writing clean, performant, and maintainable Svelte 5 applications using the runes API and modern TypeScript practices.
globs: **/*.{svelte,js,ts}
---
# 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**
```svelte
<!-- 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**
```svelte
<!-- 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"`**
```svelte
<script>
  let name: string = 'world'; // No type checking
</script>
```

**✅ GOOD: Explicitly enable TypeScript**
```svelte
<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)**
```svelte
<script lang="ts">
  let count = 0; // Not reactive outside top-level
  function increment() { count++; }
</script>
```

**✅ GOOD: Explicit `$state` for all reactive variables**
```svelte
<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**
```svelte
<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**
```svelte
<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**
```svelte
<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**
```svelte
<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)**
```svelte
<script lang="ts">
  export let name: string;
  export let age: number = 30;
</script>
```

**✅ GOOD: `$props()` for properties**
```svelte
<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`**
```svelte
<button on:click={() => alert('Clicked!')}>Click me</button>
```

**✅ GOOD: `onclick` (standard HTML attribute)**
```svelte
<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**
```svelte
<!-- MyComponent.svelte -->
<slot />
```

**✅ GOOD: Typed snippets for predictable content**
```svelte
<!-- 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**
```svelte
// store.ts
import { writable } from 'svelte/store';
export const localCounter = writable(0); // Unnecessary global
```

**✅ GOOD: Use `$state` for component-local reactivity**
```svelte
<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.

```ts
// 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);
```
```svelte
<!-- 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`**
```svelte
{#each items as item}
  <ItemComponent {item} />
{/each}
```

**✅ GOOD: Use a unique `key`**
```svelte
{#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.

```svelte
<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**
```svelte
<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**
```svelte
<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**
```svelte
<script lang="ts">
  interface Props { items: any[]; } // Loses type safety
  let { items }: Props = $props();
</script>
```

**✅ GOOD: Explicit generics for type safety**
```svelte
<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 `div`s or `span`s.

**❌ BAD: Non-semantic button**
```svelte
<div onclick={handleClick} role="button" tabindex="0">Click me</div>
```

**✅ GOOD: Semantic HTML element**
```svelte
<button onclick={handleClick}>Click me</button>
```

### 6.2. ARIA Attributes

Use ARIA attributes when semantic HTML isn't sufficient, but only when necessary.

```svelte
<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.

```ts
// 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`.

```ts
// 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');
});
```