Toolmingo
Guides14 min read

Svelte 5 Cheat Sheet: Runes, Components & Patterns

A complete Svelte 5 cheat sheet — runes ($state, $derived, $effect), components, props, events, snippets, two-way binding, lifecycle, stores, SvelteKit routing, and common patterns with copy-ready examples.

Svelte is a compiler-based JavaScript framework with no virtual DOM — components compile to vanilla JS. Svelte 5 introduced runes, a new reactivity primitive. This cheat sheet covers everything from basic syntax to SvelteKit routing with copy-ready examples.

Quick reference

The 25 patterns that cover 95% of everyday Svelte development.

Pattern Example
Reactive state let count = $state(0)
Derived value let doubled = $derived(count * 2)
Side effect $effect(() => { console.log(count) })
Props declaration let { name, age = 0 } = $props()
Bind variable <input bind:value={text} />
Conditional {#if show}<p>hi</p>{/if}
Each loop {#each items as item (item.id)}
Await block {#await promise then data}
Event handler <button onclick={() => count++}>
Snippet {#snippet row(item)}{/snippet}
Render snippet {@render row(item)}
Component import import Card from './Card.svelte'
Writable store const n = writable(0)
Store subscribe $n (auto-subscribe with $)
Class binding class:active={isActive}
Style binding style:color={color}
Two-way prop bind:value={val} on component
Lifecycle onMount(() => { ... })
Transition transition:fade={{ duration: 300 }}
Animation animate:flip={{ duration: 200 }}
Action use:tooltip={{ text: 'hi' }}
SvelteKit page +page.svelte
Load data export async function load({ fetch })
Form action export const actions = { default }
Route param $page.params.id

Components

Basic component

<!-- Greeting.svelte -->
<script lang="ts">
  let { name, greeting = 'Hello' }: { name: string; greeting?: string } = $props()
</script>

<p>{greeting}, {name}!</p>

<style>
  p { font-weight: bold; }
</style>
<!-- App.svelte -->
<script lang="ts">
  import Greeting from './Greeting.svelte'
</script>

<Greeting name="Alice" greeting="Hi" />

Props with $props()

<script lang="ts">
  // Svelte 5: destructure $props()
  let { title, count = 0, items = [] }: {
    title: string
    count?: number
    items?: string[]
  } = $props()

  // Rest props (pass through to element)
  let { class: className, ...rest } = $props()
</script>

<div class={className} {...rest}>
  <h2>{title}</h2>
  <p>Count: {count}</p>
</div>

Snippets (Svelte 5 slots replacement)

<!-- Card.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte'

  let { header, children }: {
    header?: Snippet
    children: Snippet
  } = $props()
</script>

<div class="card">
  {#if header}
    <div class="card-header">{@render header()}</div>
  {/if}
  <div class="card-body">{@render children()}</div>
</div>
<!-- Using Card -->
<Card>
  {#snippet header()}
    <h2>My Card</h2>
  {/snippet}
  <p>Card content here</p>
</Card>

Snippets with parameters

<!-- Table.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte'

  let { rows, rowSnippet }: {
    rows: { id: number; name: string }[]
    rowSnippet: Snippet<[{ id: number; name: string }]>
  } = $props()
</script>

<table>
  {#each rows as row (row.id)}
    <tr>{@render rowSnippet(row)}</tr>
  {/each}
</table>
<Table {rows}>
  {#snippet rowSnippet(row)}
    <td>{row.id}</td>
    <td>{row.name}</td>
  {/snippet}
</Table>

Runes (Svelte 5 reactivity)

$state

<script lang="ts">
  // Primitive — reactive
  let count = $state(0)

  // Object — deep reactive (proxy)
  let user = $state({ name: 'Alice', age: 30 })

  // Array — deep reactive
  let items = $state<string[]>([])

  function addItem(item: string) {
    items.push(item)        // mutations tracked automatically
    user.name = 'Bob'       // nested mutations tracked
  }

  // $state.raw — not proxied, replaced wholesale
  let big = $state.raw(new Float64Array(1_000_000))
</script>

<button onclick={() => count++}>{count}</button>
<p>{user.name} is {user.age}</p>

$derived

<script lang="ts">
  let count = $state(0)

  // Simple expression
  let doubled = $derived(count * 2)

  // Complex computation — use $derived.by()
  let stats = $derived.by(() => {
    const values = [1, 2, 3, count]
    return {
      sum: values.reduce((a, b) => a + b, 0),
      avg: values.reduce((a, b) => a + b, 0) / values.length
    }
  })
</script>

<p>{count} × 2 = {doubled}</p>
<p>Sum: {stats.sum}, Avg: {stats.avg}</p>

$effect

<script lang="ts">
  let count = $state(0)
  let element = $state<HTMLElement>()

  // Runs after DOM update when dependencies change
  $effect(() => {
    console.log('count changed:', count)
    // Return cleanup function (optional)
    return () => console.log('cleanup')
  })

  // $effect.pre — runs before DOM update
  $effect.pre(() => {
    console.log('before DOM update')
  })

  // Don't track dependency inside $effect.root
  const cleanup = $effect.root(() => {
    $effect(() => { /* runs until cleanup() called */ })
  })
</script>

$props and $bindable

<!-- Counter.svelte -->
<script lang="ts">
  // $bindable makes a prop two-way bindable
  let { count = $bindable(0) } = $props()
</script>

<button onclick={() => count++}>{count}</button>
<!-- Parent -->
<script lang="ts">
  let n = $state(0)
</script>

<Counter bind:count={n} />
<p>Parent sees: {n}</p>

Template syntax

Conditionals

{#if user.isAdmin}
  <AdminPanel />
{:else if user.isModerator}
  <ModPanel />
{:else}
  <UserView />
{/if}

Each loops

<!-- Basic loop with key -->
{#each items as item (item.id)}
  <li>{item.name}</li>
{/each}

<!-- With index -->
{#each items as item, i (item.id)}
  <li>{i + 1}. {item.name}</li>
{/each}

<!-- Empty fallback -->
{#each items as item (item.id)}
  <li>{item.name}</li>
{:else}
  <p>No items found.</p>
{/each}

<!-- Destructuring -->
{#each entries as [key, value] (key)}
  <p>{key}: {value}</p>
{/each}

Await blocks

<script lang="ts">
  let promise = $state(fetchUser(1))
</script>

{#await promise}
  <p>Loading…</p>
{:then user}
  <p>Hello {user.name}</p>
{:catch error}
  <p>Error: {error.message}</p>
{/await}

<!-- Shorthand: skip loading state -->
{#await promise then user}
  <p>Hello {user.name}</p>
{/await}

Key block (force re-render)

<!-- Re-mounts component when id changes -->
{#key userId}
  <UserProfile {userId} />
{/key}

Bindings

Form bindings

<script lang="ts">
  let name = $state('')
  let age = $state(0)
  let agreed = $state(false)
  let role = $state('viewer')
  let tags = $state<string[]>([])
</script>

<!-- Text input -->
<input bind:value={name} type="text" />

<!-- Number input (auto-converts to number) -->
<input bind:value={age} type="number" />

<!-- Checkbox -->
<input bind:checked={agreed} type="checkbox" />

<!-- Radio group -->
<input bind:group={role} type="radio" value="admin" />
<input bind:group={role} type="radio" value="viewer" />

<!-- Multi-select checkbox group -->
<input bind:group={tags} type="checkbox" value="svelte" />
<input bind:group={tags} type="checkbox" value="react" />

<!-- Textarea -->
<textarea bind:value={name}></textarea>

<!-- Select -->
<select bind:value={role}>
  <option value="admin">Admin</option>
  <option value="viewer">Viewer</option>
</select>

Element bindings

<script lang="ts">
  let el = $state<HTMLElement>()
  let width = $state(0)
  let height = $state(0)
  let scrollY = $state(0)
</script>

<!-- DOM reference -->
<div bind:this={el}></div>

<!-- Dimensions (read-only) -->
<div bind:clientWidth={width} bind:clientHeight={height}></div>

<!-- Window scroll -->
<svelte:window bind:scrollY />

Events

DOM events (Svelte 5)

<script lang="ts">
  function handleClick(e: MouseEvent) {
    console.log(e.target)
  }

  function handleInput(e: Event) {
    const target = e.target as HTMLInputElement
    console.log(target.value)
  }
</script>

<!-- Inline handler -->
<button onclick={() => console.log('clicked')}>Click</button>

<!-- Named handler -->
<button onclick={handleClick}>Click</button>

<!-- With event modifiers via JS -->
<button onclick={(e) => { e.preventDefault(); submit() }}>Submit</button>

<!-- Keyboard events -->
<input
  onkeydown={(e) => { if (e.key === 'Enter') submit() }}
  oninput={handleInput}
/>

Component events (Svelte 5)

<!-- Button.svelte — use callback props instead of createEventDispatcher -->
<script lang="ts">
  let { onclick }: { onclick?: () => void } = $props()
</script>

<button {onclick}>Click me</button>
<!-- Parent -->
<Button onclick={() => console.log('clicked')} />

Lifecycle

<script lang="ts">
  import { onMount, onDestroy, beforeUpdate, afterUpdate, tick } from 'svelte'

  // Runs once after component mounts (browser only)
  onMount(() => {
    const timer = setInterval(() => { /* ... */ }, 1000)
    // Return cleanup (called on destroy)
    return () => clearInterval(timer)
  })

  // Runs before component is destroyed
  onDestroy(() => {
    console.log('cleanup')
  })

  // Runs before DOM updates
  beforeUpdate(() => { /* ... */ })

  // Runs after DOM updates
  afterUpdate(() => { /* ... */ })

  // Wait for DOM to update
  async function updateAndRead() {
    someState = 'new value'
    await tick()        // DOM is now updated
    console.log(el.textContent)
  }
</script>

Stores

<!-- stores.ts -->
<script lang="ts">
  import { writable, readable, derived, get } from 'svelte/store'

  // Writable store
  export const count = writable(0)

  // Readable store (external source)
  export const time = readable(new Date(), (set) => {
    const interval = setInterval(() => set(new Date()), 1000)
    return () => clearInterval(interval)
  })

  // Derived store
  export const doubled = derived(count, ($count) => $count * 2)

  // Derived from multiple stores
  export const combined = derived(
    [count, time],
    ([$count, $time]) => `${$count} at ${$time.toLocaleTimeString()}`
  )

  // Read store value outside component
  const current = get(count)
</script>
<!-- Component.svelte -->
<script lang="ts">
  import { count, doubled } from './stores'

  // $ prefix auto-subscribes and auto-unsubscribes
</script>

<p>Count: {$count}</p>
<p>Doubled: {$doubled}</p>
<button onclick={() => count.update(n => n + 1)}>+1</button>
<button onclick={() => count.set(0)}>Reset</button>

Transitions and animations

<script lang="ts">
  import { fade, fly, slide, scale, blur, draw, crossfade } from 'svelte/transition'
  import { flip } from 'svelte/animate'
  import { cubicOut } from 'svelte/easing'

  let show = $state(true)
  let items = $state(['a', 'b', 'c'])
</script>

<!-- Fade in/out -->
{#if show}
  <div transition:fade={{ duration: 300 }}>Fades</div>
{/if}

<!-- Fly from below -->
{#if show}
  <div transition:fly={{ y: 20, duration: 400, easing: cubicOut }}>Flies</div>
{/if}

<!-- In and out separately -->
{#if show}
  <div in:fly={{ y: -20 }} out:fade>Different in/out</div>
{/if}

<!-- FLIP animation for lists -->
{#each items as item (item)}
  <div animate:flip={{ duration: 200 }}>{item}</div>
{/each}

Actions

<!-- actions.ts -->
<script lang="ts">
  import type { Action } from 'svelte/action'

  // Tooltip action
  export const tooltip: Action<HTMLElement, string> = (node, text) => {
    // Setup
    node.setAttribute('title', text)

    return {
      update(newText) {
        node.setAttribute('title', newText)
      },
      destroy() {
        node.removeAttribute('title')
      }
    }
  }

  // Click outside action
  export const clickOutside: Action<HTMLElement, () => void> = (node, callback) => {
    function handle(e: MouseEvent) {
      if (!node.contains(e.target as Node)) callback()
    }
    document.addEventListener('click', handle)
    return {
      destroy() {
        document.removeEventListener('click', handle)
      }
    }
  }
</script>
<script lang="ts">
  import { tooltip, clickOutside } from './actions'

  let open = $state(false)
</script>

<button use:tooltip={'Click to open'}>Hover me</button>

{#if open}
  <div use:clickOutside={() => (open = false)}>
    Dropdown content
  </div>
{/if}

Special elements

<!-- Window and document event listeners -->
<svelte:window
  bind:scrollY={scrollY}
  onkeydown={(e) => { if (e.key === 'Escape') close() }}
/>

<svelte:document
  ontouchstart={handleTouch}
/>

<!-- Body attributes -->
<svelte:body class:modal-open={isOpen} />

<!-- Head tags -->
<svelte:head>
  <title>Page Title</title>
  <meta name="description" content="Page description" />
</svelte:head>

<!-- Dynamic component -->
<script lang="ts">
  import A from './A.svelte'
  import B from './B.svelte'
  let Current = $state(A)
</script>

<svelte:component this={Current} />

<!-- Self-referential (recursive) component -->
<svelte:self depth={depth + 1} />

SvelteKit

File structure

src/
├── routes/
│   ├── +layout.svelte          # Root layout (wraps all pages)
│   ├── +layout.ts              # Root layout load function
│   ├── +page.svelte            # Home page (/)
│   ├── +page.ts                # Home page data loading
│   ├── +error.svelte           # Error page
│   ├── about/
│   │   └── +page.svelte        # /about
│   ├── blog/
│   │   ├── +page.svelte        # /blog (list)
│   │   └── [slug]/
│   │       ├── +page.svelte    # /blog/:slug
│   │       └── +page.ts        # Load single post
│   └── api/
│       └── users/
│           └── +server.ts      # REST endpoint
├── lib/
│   ├── components/
│   └── server/                 # Server-only code
└── app.html

Load functions

// +page.ts — runs on server and client
import type { PageLoad } from './$types'

export const load: PageLoad = async ({ fetch, params, url }) => {
  const res = await fetch(`/api/posts/${params.slug}`)
  if (!res.ok) throw error(404, 'Post not found')
  const post = await res.json()
  return { post }
}
// +page.server.ts — server only (access DB directly)
import type { PageServerLoad } from './$types'
import { db } from '$lib/server/db'

export const load: PageServerLoad = async ({ params, locals }) => {
  const user = locals.user  // set by hooks.server.ts
  const post = await db.post.findUnique({ where: { slug: params.slug } })
  if (!post) throw error(404)
  return { post, user }
}
<!-- +page.svelte -->
<script lang="ts">
  import type { PageData } from './$types'

  let { data }: { data: PageData } = $props()
</script>

<h1>{data.post.title}</h1>

API routes

// src/routes/api/users/+server.ts
import type { RequestHandler } from './$types'
import { json, error } from '@sveltejs/kit'

export const GET: RequestHandler = async ({ url }) => {
  const page = Number(url.searchParams.get('page') ?? 1)
  const users = await db.user.findMany({ skip: (page - 1) * 10, take: 10 })
  return json(users)
}

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json()
  // validate body...
  const user = await db.user.create({ data: body })
  return json(user, { status: 201 })
}

Form actions

// +page.server.ts
import type { Actions } from './$types'
import { fail, redirect } from '@sveltejs/kit'

export const actions: Actions = {
  login: async ({ request, cookies }) => {
    const data = await request.formData()
    const email = data.get('email') as string
    const password = data.get('password') as string

    if (!email || !password) {
      return fail(400, { email, missing: true })
    }

    const user = await authenticate(email, password)
    if (!user) return fail(401, { email, invalid: true })

    cookies.set('session', user.sessionId, { path: '/' })
    redirect(303, '/dashboard')
  }
}
<!-- +page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms'
  import type { ActionData } from './$types'

  let { form }: { form: ActionData } = $props()
</script>

<form method="POST" action="?/login" use:enhance>
  <input name="email" type="email" value={form?.email ?? ''} />
  <input name="password" type="password" />
  {#if form?.missing}<p>Please fill all fields</p>{/if}
  {#if form?.invalid}<p>Invalid credentials</p>{/if}
  <button type="submit">Log in</button>
</form>

Navigation and routing

<script lang="ts">
  import { goto, invalidate, preloadData } from '$app/navigation'
  import { page } from '$app/stores'

  // Current route info
  // $page.url, $page.params, $page.route.id, $page.status
</script>

<!-- Link with prefetch -->
<a href="/about" data-sveltekit-preload-data>About</a>

<!-- Programmatic navigation -->
<button onclick={() => goto('/dashboard')}>Go to dashboard</button>
<button onclick={() => goto('/blog', { replaceState: true })}>Replace</button>

<!-- Active link -->
<a href="/about" class:active={$page.url.pathname === '/about'}>About</a>

Common patterns

Data fetching with loading state

<script lang="ts">
  type User = { id: number; name: string }

  let users = $state<User[]>([])
  let loading = $state(false)
  let error = $state<string | null>(null)

  async function fetchUsers() {
    loading = true
    error = null
    try {
      const res = await fetch('/api/users')
      if (!res.ok) throw new Error('Failed to fetch')
      users = await res.json()
    } catch (e) {
      error = (e as Error).message
    } finally {
      loading = false
    }
  }

  // onMount alternative using $effect
  $effect(() => {
    fetchUsers()
  })
</script>

{#if loading}
  <p>Loading…</p>
{:else if error}
  <p>Error: {error}</p>
{:else}
  {#each users as user (user.id)}
    <p>{user.name}</p>
  {/each}
{/if}

Context API (pass data without prop drilling)

<!-- Parent.svelte -->
<script lang="ts">
  import { setContext } from 'svelte'

  const theme = $state({ primary: '#ff3e00', dark: false })
  setContext('theme', theme)
</script>

<Child />
<!-- DeepChild.svelte -->
<script lang="ts">
  import { getContext } from 'svelte'

  const theme = getContext<{ primary: string; dark: boolean }>('theme')
</script>

<p style:color={theme.primary}>Themed text</p>

Reusable store pattern (context store)

// counterStore.ts — one store per component tree
import { writable } from 'svelte/store'
import { getContext, setContext } from 'svelte'

const KEY = Symbol('counter')

export function createCounter(initial = 0) {
  const count = writable(initial)
  return {
    subscribe: count.subscribe,
    increment: () => count.update(n => n + 1),
    reset: () => count.set(0)
  }
}

export function setCounterContext(counter: ReturnType<typeof createCounter>) {
  setContext(KEY, counter)
}

export function getCounterContext() {
  return getContext<ReturnType<typeof createCounter>>(KEY)
}

Common mistakes

Mistake Problem Fix
let count = 0 in Svelte 5 Not reactive without $state let count = $state(0)
Mutating a $state object reference Loses reactivity Mutate properties: obj.name = 'x', not obj = { name: 'x' }
Using createEventDispatcher in Svelte 5 Deprecated Use callback props: let { onclick } = $props()
<slot> in Svelte 5 Deprecated Use {#snippet} and {@render}
Forgetting (key) in {#each} Inefficient DOM updates {#each items as item (item.id)}
$effect with no cleanup for subscriptions Memory leak Return cleanup: return () => unsub()
Accessing DOM before mount undefined error Use onMount or bind:this
Using stores when runes suffice Extra complexity Prefer $state/$derived inside components

Svelte 4 → Svelte 5 migration

Svelte 4 Svelte 5
export let name let { name } = $props()
$: doubled = count * 2 let doubled = $derived(count * 2)
$: { console.log(count) } $effect(() => { console.log(count) })
<slot> {#snippet children()}{/snippet} + {@render children()}
createEventDispatcher Callback props
on:click onclick
bind:this on component bind:this still works

FAQ

Q: Svelte vs React — what are the key differences?
Svelte compiles to vanilla JS at build time — no runtime framework, no virtual DOM. Components are smaller, faster to hydrate, and require less boilerplate. React has a larger ecosystem and more job market demand. Svelte is excellent for performance-critical apps and smaller teams.

Q: When should I use stores vs runes ($state)?
Use $state/$derived inside components — they're simpler and co-located. Use stores (writable, readable, derived) for state shared across multiple components that don't share a parent, or for state that lives outside the component tree.

Q: How do I share state between routes in SvelteKit?
Use a Svelte store (writable) in src/lib/stores.ts. Import it in both routes. For server data, put it in locals via hooks.server.ts and access via load functions.

Q: Are Svelte 5 runes browser-only?
No — runes work in SSR (SvelteKit) and on the client. $state, $derived, and $effect are all compile-time transforms, not runtime APIs.

Q: How do I handle authentication in SvelteKit?
Use hooks.server.ts to validate the session cookie and populate locals.user. Then in protected +page.server.ts files, check locals.user and throw redirect(303, '/login') if not authenticated.

Q: Does Svelte have TypeScript support?
Yes — use <script lang="ts"> in .svelte files. SvelteKit projects use tsconfig.json. The $props() rune works naturally with TypeScript generics for full type safety.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools