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.
.cursor/rules/svelte.mdcWhat 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 letandon:clickfrom 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.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/svelte.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/svelte.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/svelte.mdManually: put the files in place
.clinerules/svelte.mdStep 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.