Skip to main content
rule

svelte · sanjeed5 — Cursor rule: Svelte 5 runes and TypeScript

A Cursor rule that pins down how Svelte 5 components get written: runes instead of legacy reactivity, typed scripts, keyed lists. Useful when the project already runs Svelte 5.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/svelte.mdc
Open on GitHub

What svelte does

The rule covers Svelte 5 with TypeScript and applies to **/*.{svelte,js,ts}. It asks for lang="ts" on every <script> block and keeps logic, markup and styles inside one .svelte file rather than pulling simple calculations into separate modules.

Reactivity is split by rune: $state for state, $derived for computed values, $effect for side effects, $props() for component properties. The older export let, $: and on:click forms are marked as wrong. Stores stay in the picture for state that many components share, not for local counters.

On performance the rule wants a unique key in {#each} blocks and a dynamic import for anything not needed on first paint. If your codebase is still on Svelte 4, these examples fight the code you already have, so upgrade first and add the rule after. The pitfalls section in the source stops partway through $effect dependencies.

What it helps with

  • move components off legacy reactivity onto Svelte 5 runes
  • drop export let and on:click from new code
  • get type checking inside <script> blocks
  • fix list re-renders with a key in {#each}
  • keep derived values apart from side effects

Example prompts for the agent

  • rewrite this component using Svelte 5 runes
  • check this file against the svelte rule and list the violations
  • type the props with $props()

How to install svelte

Manually: put the files in place

.cursor/rules/svelte.mdc

Frontmatter: description, globs, alwaysApply

Step by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules

Questions about svelte

How do I install the svelte rule in Cursor?

Create .cursor/rules/svelte.mdc in your project root and paste the rule content into it. Cursor picks it up for files matching **/*.{svelte,js,ts}.

What does the svelte rule do?

It sets the coding style for Svelte 5 with TypeScript: the $state, $derived, $effect and $props() runes, typing in <script lang="ts">, keys in {#each}. The agent follows it while writing or editing components.

Does it work with Svelte 4?

No. The examples are built on Svelte 5 runes and treat export let and $: as wrong, so on a Svelte 4 project the rule argues with the code you already have.

Source text

svelte.mdc · CC0-1.0
---
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');
});
```

© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc

Similar IDE rules

All IDE rules for Frontend and UI →

svelte by other authors comes first.

From the same set69

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor