Svelte 5 Runes: What Starter Kit Authors Need to Know
Svelte 5 shipped over a year ago. Many starters still use the old syntax. Here's how to tell if a starter is actually up-to-date — and why it matters.
Svelte 5 shipped in October 2024. It’s been over eighteen months. If you’re starting a new project today, you’re using Svelte 5 by default — the CLI scaffolds it, the docs assume it, the ecosystem has moved on.
And yet, a surprising number of starter kits still ship with Svelte 4 patterns. They work — Svelte 5 maintains backward compatibility — but you’re starting a new project on deprecated syntax. That’s a maintenance debt you’re choosing to inherit on day one.
Here’s what you need to know to evaluate whether a starter has actually adopted modern Svelte.
The shift: from magic to explicit
Svelte 4 used implicit reactivity. Declare a variable with let, and Svelte tracks it. Prefix a statement with $:, and it’s reactive. It felt magical, which was the point — but magic has costs. The compiler had to guess your intent, and sometimes it guessed wrong.
Svelte 5 replaces this with runes: explicit primitives that tell the compiler exactly what you mean.
The core runes:
$state— declares reactive state. Replaces reactiveletdeclarations.$derived— declares computed values. Replaces$:reactive statements that derive data.$effect— declares side effects. Replaces$:reactive statements that do things (log, fetch, sync).$props— declares component props. Replacesexport let.$bindable— marks a prop as two-way bindable.
The key insight: $: was overloaded. It meant both “compute this value” and “run this side effect.” Svelte 5 separates those concerns into $derived and $effect, which makes intent clearer and bugs rarer.
How to spot a Svelte 4 starter
Open any .svelte file in the starter and look for these patterns:
Svelte 4 (legacy):
<script>
export let title;
export let count = 0;
$: doubled = count * 2;
$: console.log('count changed:', count);
</script>
Svelte 5 (current):
<script>
let { title, count = 0 } = $props();
let doubled = $derived(count * 2);
$effect(() => {
console.log('count changed:', count);
});
</script>
Other tells:
createEventDispatcher— replaced by callback props in Svelte 5<slot />— replaced by snippets ({#snippet}and{@render})on:click— replaced byonclickattribute syntax- Svelte stores (
writable,readable) — not deprecated, but$statehandles most use cases now
Where our directory starters stand
We reviewed the starters in our directory for Svelte 5 adoption. The reality is mixed:
Fully migrated (runes throughout):
- SK Minimal — being minimal, there was less to migrate. Clean runes usage throughout.
- SvelteBlog Pro — the MDX content layer doesn’t require much reactivity, so the migration was straightforward.
- LaunchKit — a landing page with scroll animations. Simple state, clean
$effectusage for intersection observers. - SveltePWA — service worker logic lives outside Svelte components, so the component layer migrated cleanly.
Partially migrated (mixed syntax):
- OpenSaaS Svelte — newer components use runes, but some auth flows still use
export letand$:patterns. - SvelteSaaS — the UI components (shadcn-svelte) are Svelte 5 native, but custom pages mix old and new.
- EdgeKit — database and auth layers use modern patterns, some UI files lag behind.
Still largely Svelte 4 syntax:
- SvelteStack Pro — its size works against it. Hundreds of files means a slower migration.
- AdminKit Svelte — the chart components and RBAC logic still rely heavily on stores and
$:. - SvelteFire Kit — Firebase SDK wrappers use stores extensively.
Why it matters for your project
“But it still works” is the obvious counterargument. Svelte 5 runs legacy syntax without errors. So why care?
1. Library compatibility is shifting.
Component libraries are dropping Svelte 4 support. shadcn-svelte, Melt UI, and Bits UI all assume Svelte 5. If your starter uses legacy event dispatching or slots, you’ll hit friction integrating modern libraries.
2. The migration tool only goes one direction.
npx sv migrate svelte-5 converts Svelte 4 to Svelte 5 syntax. It handles export let to $props(), $: to $derived/$effect, on: to event attributes, and slots to snippets. But it’s not perfect — it struggles with ambiguous $: statements and $$restProps patterns. The more code you have to migrate later, the more manual cleanup you’ll need.
3. Runes are simply better for complex state.
Fine-grained reactivity means Svelte only updates what changed, not the entire component. For starters with dashboards, admin panels, or real-time features, this translates to measurably better performance.
4. New developers will learn runes first.
The official docs and tutorials teach Svelte 5 exclusively. Hiring someone to work on your codebase six months from now means they’ll know $state, not let. Legacy syntax becomes a knowledge gap.
What good migration looks like
If a starter claims Svelte 5 support, verify these specifics:
Props: Every component should use let { ... } = $props() — not export let. Destructured props with defaults are the standard pattern.
Derived state: Anywhere you see $: followed by an assignment, it should be $derived() or $derived.by(() => { ... }) for multi-line logic.
Effects: Side effects (logging, external syncs, subscriptions) should use $effect() with proper cleanup via the return function.
Events: Child-to-parent communication should use callback props, not createEventDispatcher. If you see dispatch('event') in a starter, it hasn’t migrated.
Slots to snippets: This is the most visible change in templates. <slot /> becomes {@render children()}, and named slots become named snippets.
Stores: Svelte stores (writable, readable, derived) still work and aren’t deprecated. But for component-local state, $state is preferred. Global stores are fine for cross-component shared state — the Svelte team hasn’t pushed a runes-only approach for that use case yet.
The migration path for existing starters
If you’ve already started building on a Svelte 4 starter, don’t panic. The coexistence is real — you can migrate one file at a time.
The practical order:
- Run
npx sv migrate svelte-5for the automated pass - Fix the ambiguous cases manually (the tool flags them)
- Convert stores to
$statewhere they’re only used within a single component tree - Leave truly global stores (theme, auth state, toast notifications) as-is — they still work fine
- Update event dispatchers to callback props during your next feature pass
Our recommendation
If you’re choosing a starter today, pick one that’s fully migrated to Svelte 5 runes. Not because legacy syntax is broken — it isn’t — but because you’re choosing the foundation for months or years of development. Starting modern is always cheaper than migrating later.
For simple projects, SK Minimal is cleanly written Svelte 5 from top to bottom. For content sites, SvelteBlog Pro shows runes in a real-world context without overwhelming complexity.
If you need a more complex starter that’s still partially migrated, factor in a day of cleanup work to bring the remaining files up to date. The migration tool gets you 80% there. The last 20% is just pattern recognition.
Get new starters in your inbox
Monthly picks, reviews, and the occasional deep-dive.
You're in! Watch your inbox.
Something went wrong.
No spam. Unsubscribe anytime.
Need pre-built UI components for your SvelteKit project? Check out svelteblocks.com →