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, noref()wrappers; top-levelletdeclarations 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.jsactions) — 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
pushand property assignments; reassign with spread or slice. - Reactive statement ordering — a
$:block that readsfilteredmust appear afterfilteredis defined. - Using onMount for data that SEO needs — crawlers may miss client-only fetches; use SvelteKit
loador prerender. - Over-globalizing stores — not every piece of state belongs in a writable; local
letkeeps components testable. - Browser APIs during SSR —
windowanddocumentthrow on the server; guard withbrowseroronMount. - 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
loadfunctions; keep secrets in+page.server.jsonly. - 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
preloadand cache headers on static assets; analyze bundles withvite-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
letassignments and$:reactive statements are the core Svelte 4 mental model. - Stores share state across components; SvelteKit
loadfunctions 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
- Vue.js fundamentals explained — runtime reactivity with templates and composables
- React fundamentals explained — hooks, JSX, and the reconciliation model Svelte avoids
- TypeScript fundamentals explained — typing SvelteKit loads and component props
- SSR vs CSR vs SSG vs ISR explained — rendering strategies SvelteKit implements natively