svelte

star 2

[Applies to: **/*.{svelte,js,ts}] Definitive guide for writing clean, performant, and maintainable Svelte 5 applications using the runes API and modern TypeScript practices.

Tryboy869 By Tryboy869 schedule Updated 2/28/2026

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');
});
Install via CLI
npx skills add https://github.com/Tryboy869/dojutsu-for-ai --skill svelte
Repository Details
star Stars 2
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator