Guide

Svelte fundamentals explained

A regional newsroom publishes dozens of guides per week. Editors need a fast dashboard to draft headlines, preview SEO snippets, and publish without shipping a heavy JavaScript runtime to every reader. Svelte takes a different path from React and Vue: instead of diffing a virtual DOM at runtime, the Svelte compiler analyzes your component code and emits precise DOM update instructions. The result is often smaller bundles, snappier first paint, and syntax that feels like enhanced HTML. SvelteKit adds file-based routing, server-side rendering, and adapters for static hosting or Node. This guide covers Svelte’s compile-time reactivity model, components and bindings, stores, SvelteKit data loading, a Harbor Archive editorial dashboard worked example, a framework decision table, common pitfalls, and a production checklist alongside our TypeScript fundamentals guide and SSR vs CSR rendering guide.

What Svelte is (and why it feels different)

Svelte is a compiler that produces vanilla JavaScript. You write .svelte components with HTML, CSS, and a <script> block; the build step turns assignments like count += 1 into direct DOM mutations. There is no reconciliation loop and no framework runtime comparable to React’s ~40 KB gzipped core.

  • Compile-time reactivity — the compiler tracks which variables each template expression reads and wires update statements automatically.
  • Scoped CSS by default — selectors in a component’s <style> block apply only to that file unless you use :global().
  • Less boilerplate — no useState, no ref() wrappers; top-level let declarations in <script> are reactive.
  • SvelteKit — the official application framework (routing, SSR, API endpoints, adapters) built on Vite, analogous to Next.js for React or Nuxt for Vue.

Svelte 5 introduced runes ($state, $derived, $effect) as a more explicit reactivity API. Svelte 4 syntax with let and $: labels remains widely used and is covered here because most production codebases still ship it; runes are the migration path for new Svelte 5 projects.

Reactivity: assignments, $: labels, and derived state

In Svelte 4, any top-level variable reassignment triggers UI updates:

<script>
  let draftTitle = ''
  let wordCount = 0

  $: wordCount = draftTitle.trim().split(/\s+/).filter(Boolean).length
  $: seoWarning = wordCount < 40 || wordCount > 70
</script>

<input bind:value={draftTitle} placeholder="Headline" />
<p>{wordCount} words {seoWarning ? '(adjust for SEO)' : ''}</p>

The $: prefix marks a reactive statement. Svelte re-runs it whenever a dependency changes — similar to Vue’s computed or a React useMemo, but without an explicit dependency array. Order matters: reactive blocks that depend on other reactive values must appear after their dependencies in the file.

Reactive declarations vs reactive assignments

$: doubled = count * 2 recalculates when count changes. $: if (count > 10) alert('High') runs a side effect reactively — use sparingly; prefer event handlers or lifecycle hooks for imperative work. Async reactive statements are legal but easy to race; fetch in onMount or SvelteKit load functions instead.

Arrays and objects need reassignment to trigger updates: todos = [...todos, newTodo], not todos.push(newTodo) alone. Svelte 5 runes solve this with deep reactivity on $state proxies; in Svelte 4, treat immutability as a habit.

Components: props, events, and slots

Parent components pass data with export let declarations (props). Child components communicate upward with custom events via createEventDispatcher or, in Svelte 5, callback props.

<!-- ArticleRow.svelte -->
<script>
  export let article
  export let selected = false
  import { createEventDispatcher } from 'svelte'
  const dispatch = createEventDispatcher()
</script>

<button
  class:selected
  on:click={() => dispatch('select', { id: article.id })}
>
  {article.title}
</button>

Slots let parents inject markup: default slot for children, named slots (<slot name="header" />) for layout regions. Slot props pass data upward: <slot item={row} /> enables <Row let:item>...</Row> patterns in tables and lists.

class: and style: directives toggle CSS conditionally without template string gymnastics. {#if}, {#each}, and {#await} blocks handle control flow; always key {#each} loops with a stable ID, not the array index.

Bindings, actions, and transitions

bind:value creates two-way binding on inputs, selects, and textareas. bind:checked, bind:group, and bind:this (DOM node reference) cover the common form cases. For components, expose bindable props with export let value and dispatch input events, or use Svelte 5’s $bindable() rune.

Actions are functions that run when an element mounts: use:clickOutside for dropdowns, use:lazyLoad for images. They return optional update and destroy hooks. Built-in transitions (fade, fly, slide) animate elements entering and leaving the DOM when paired with {#if} blocks — a few lines replace hundreds of CSS keyframe toggles.

Lifecycle functions mirror other frameworks: onMount for browser-only setup (charts, analytics), onDestroy for cleanup, beforeUpdate / afterUpdate for DOM-adjacent side effects. SSR-aware code must guard browser APIs inside onMount or browser checks from $app/environment in SvelteKit.

Stores: shared state outside the component tree

When distant components need the same data, Svelte provides lightweight stores:

import { writable, derived } from 'svelte/store'

export const editorFilter = writable('all')
export const draftQueue = writable([])

export const pendingCount = derived(
  draftQueue,
  $q => $q.filter(d => d.status === 'pending').length
)

Prefix store names with $ in templates to auto-subscribe: {$pendingCount}. In script, call pendingCount.subscribe or use the $ prefix only inside .svelte files. Prefer stores for cross-route UI state (theme, sidebar open); keep component-local state in let variables to avoid global spaghetti.

readable stores expose read-only values (clock ticks, geolocation). derived combines multiple stores. For complex apps, compare Svelte stores with the patterns in our frontend state management guide — TanStack Query still fits server cache layers in SvelteKit via @tanstack/svelte-query.

SvelteKit: routing, load functions, and deployment

SvelteKit organizes apps with file-based routing under src/routes/. A +page.svelte renders UI; a colocated +page.js or +page.server.js exports a load function that fetches data before render.

  • Universal load (+page.js) — runs on server and client during navigation; good for public APIs.
  • Server load (+page.server.js) — secrets and database queries stay on the server; results serialize to the client.
  • Form actions (+page.server.js actions) — progressive enhancement for mutations without a separate REST layer.
  • Layouts (+layout.svelte) — shared chrome, nested data loading via +layout.server.js.

Adapters target deployment environments: @sveltejs/adapter-static for CDN-hosted SPAs, adapter-node for VPS or container deploys, adapter-vercel / adapter-netlify for edge platforms. SvelteKit handles SSR, CSR, and prerendering through export const prerender and export const ssr page options — set prerender = true on marketing pages, ssr = false on heavy client-only tools.

Scaffold with npm create svelte@latest, enable TypeScript and ESLint, and use Vite’s instant HMR during development. Production builds tree-shake unused components and split code per route automatically.

Worked example: Harbor Archive editorial dashboard

Harbor Archive runs a content team publishing regional explainers. Editors need to queue drafts, preview meta descriptions, and bulk-publish approved articles — without loading a 200 KB React bundle on the public reader site.

Architecture split

Public article pages prerender as static HTML via SvelteKit’s adapter-static for Core Web Vitals. The password-protected /editor route uses adapter-node behind nginx with server load functions querying a Postgres drafts table. Shared TypeScript types live in src/lib/types.ts.

Draft list with reactive filters

EditorBoard.svelte imports editorFilter and draftQueue stores. A $: block filters drafts by status and search term. {#each filtered as draft (draft.id)} renders ArticleRow components; selecting a row sets bind:selected on a detail pane with live word-count and canonical URL preview computed reactively.

Publishing flow

The publish button POSTs through a SvelteKit form action publishDraft in +page.server.js. On success, the action returns updated draft status; the client invalidates via invalidateAll() from $app/navigation. Optimistic UI updates the store immediately, rolling back if the action throws — editors see instant feedback while static pages regenerate in a background job.

Styling and accessibility

Component-scoped styles use design tokens imported from a shared app.css with Tailwind v4 utilities where rapid layout iteration matters. Focus rings and ARIA labels on filter controls follow patterns from our accessibility guide. Vitest tests store derivations; Playwright covers the publish smoke path on staging.

Framework decision table

Need Prefer Why
Largest ecosystem and hiring pool React + Next.js Most libraries, jobs, and third-party integrations
Gradual adoption on legacy pages Vue 3 Drop-in widgets via CDN; HTML-like templates
Smallest JS payload, content-heavy sites Svelte / SvelteKit Compile-time updates; excellent Lighthouse scores
Enterprise modules, DI, RxJS integration Angular Opinionated structure for large teams
Highly dynamic dashboards with huge component trees React Mature devtools, concurrent features, vast component libraries
Marketing site + small interactive islands Astro + Svelte islands Ship mostly static HTML; hydrate Svelte only where needed
Full-stack SSR with form actions and typed routes SvelteKit Integrated load/actions model; adapters for every host

Common pitfalls

  • Mutating arrays/objects in place — Svelte 4 misses push and property assignments; reassign with spread or slice.
  • Reactive statement ordering — a $: block that reads filtered must appear after filtered is defined.
  • Using onMount for data that SEO needs — crawlers may miss client-only fetches; use SvelteKit load or prerender.
  • Over-globalizing stores — not every piece of state belongs in a writable; local let keeps components testable.
  • Browser APIs during SSRwindow and document throw on the server; guard with browser or onMount.
  • Ignoring Svelte 5 migration — new projects should evaluate runes; mixing runes and legacy syntax in one component fails at compile time.
  • Skipping adapter choice early — static adapter limitations (no server routes) bite mid-project; pick adapter to match hosting on day one.

Production checklist

  • Scaffold with npm create svelte@latest; enable TypeScript, ESLint, and Vitest.
  • Fetch in load functions; keep secrets in +page.server.js only.
  • Prerender public marketing and article routes; SSR or CSR only where personalization requires it.
  • Key every {#each} block with stable IDs; use immutable updates for lists.
  • Extract cross-cutting logic into src/lib/ modules and test stores in isolation.
  • Configure the correct SvelteKit adapter for your host before building CI pipelines.
  • Set preload and cache headers on static assets; analyze bundles with vite-bundle-visualizer.
  • Add Playwright E2E coverage for one critical user journey (login, publish, or checkout).
  • Document when to use stores vs load data vs TanStack Query in your project README.
  • Plan Svelte 4 to 5 rune migration if starting on legacy templates today.

Key takeaways

  • Svelte compiles components to efficient DOM updates — no virtual DOM runtime on the critical path.
  • Top-level let assignments and $: reactive statements are the core Svelte 4 mental model.
  • Stores share state across components; SvelteKit load functions own server and SEO-sensitive data.
  • SvelteKit unifies routing, SSR, form actions, and deployment adapters in one Vite-powered toolchain.
  • Choose Svelte when bundle size and first paint matter; pair with React or Vue knowledge when comparing trade-offs.

Related reading