svelte
-
SvelteKit Forms: Back to Basics (And Why That's a Good Thing)
Before we talk about SvelteKit, we should touch on how React form handling works. I’m writing this to remind myself of the
modernway to do it…- You set up
useStatefor every input field - Write
onChangehandlers to update state on every keystroke - Add an
onSubmithandler to prevent the browser’s default behavior, construct a JSON payload, fire off afetchrequest - Manage loading state to disable the submit button
- Manually parse the response to show success or error messages.
That’s a lot jumps to be hooped.
Frameworks like Next.js have improved this with server actions, but the fundamental approach remains the same: a cage match with the browsers native form handling.
SvelteKit takes a completely different approach. It just uses regular HTML.
How SvelteKit Forms Work
In SvelteKit, you write a standard
<form method="POST">in your+page.sveltefile. That’s your UI. Then, in a+page.server.tsfile in the same directory, you export anactionsobject that handles the submission.The server-side action receives a request object, extracts the form data, runs your logic, and returns either a
redirector afailwith validation errors. No client-side state management, no manual fetch calls, no JSON serialization.// +page.server.ts export const actions = { default: async ({ request }) => { const data = await request.formData(); const email = data.get('email'); const password = data.get('password'); if (!email) { return fail(400, { email, missing: true }); } // authenticate user... throw redirect(303, '/dashboard'); } };If you’re a backend developer coming from Python or Ruby, this should feel familiar. The browser collects the data, posts it to a URL, and the server processes it. Stateless, reliable, and native.
Progressive Enhancement: The Best of Both Worlds
You might be thinking, “Okay, but I don’t want full page reloads every time someone submits a form.” Fair point. SvelteKit handles this with something called progressive enhancement. You add
use:enhanceto your form tag:<form method="POST" use:enhance> <!-- your inputs --> </form>That single directive tells SvelteKit to automatically intercept the submission, prevent the page reload, submit the data via fetch under the hood, and update the
formprop with any validation errors returned from the server.But wait there’s more.
If JavaScript fails to load or the user is on a terrible connection, the form still works. It falls back to the browser’s native POST request.
Your form doesn’t break just because a script didn’t load. That’s how the web is supposed to work.
The React approach treats the browser as a rendering target and reimplements everything in JavaScript. SvelteKit treats the browser as a platform and builds on top of what’s already there.
Now, don’t get me wrong. I’ll write React code any day of the week at the ole w2, but I am really liking how Svelte and SvelteKit has decided to approach these sorts of features.
- You set up
-
Routing in SvelteKit vs Next.js vs Astro
Continuing my series on Svelte, I want to dig into how SvelteKit handles routing and how it compares with Next.js and Astro. These are the frameworks I am most familiar with, so apologies if you wanted a different framework.
Let’s start with everybody’s favorite topic: routing. Okay, maybe I’m the only one excited about this, but stick with me.
Astro: The Traditional Approach
Astro’s routing feels the most traditional of the three. You have a
src/pages/directory, and the file name is the route. Sosrc/pages/dashboard.astromaps to/dashboard. If you’ve worked with PHP or other languages with file-based routing, this will feel immediately familiar.Inside the
.astrofile, you separate your backend logic at the top of the file with---fences. That code runs entirely on the server, and everything below is your HTML template. Clean and straightforward.Next.js: Folder-Based with the App Router
Next.js (the current versions, at least) uses the App Router. The structure is folder-based:
app/dashboard/page.tsxmaps to/dashboard. The key difference from Astro is that the folder name determines the route, but the file must always be calledpage.tsx.By default, everything in the
appdirectory is a server component, meaning it runs on the server. If you want client-side interactivity, you explicitly add"use client"at the top of the file. You can also set"use server"if you want to be explicit, but it’s the default. I think Next.js does a really good job of making it clear what runs where.SvelteKit: Convention Over Configuration
SvelteKit also uses folder-based routing, similar to Next.js. The structure is
src/routes/dashboard/, but instead ofpage.tsx, SvelteKit uses a reserved+prefix for its special files:+page.svelte— Renders on the server, then hydrates on the client+page.server.ts— Runs only on the server (data loading, form actions)+server.ts— A raw API endpoint with no UI
There’s no
"use client"or"use server"directive. The file naming convention itself tells SvelteKit what should run where. If you fetch a database record in+page.server.ts, that data is returned as an object, fully typed, and available in your+page.sveltevia the$propsrune. It just works.The Case for Standard File Names
I can see if some people get annoyed that every route has files named
+page.svelteand+page.server.ts. The files will all look the same in the IDE, but there’s a real advantage here: you can group all related components in the same route folder.For example, if you’re building a dashboard, you can keep your
DashboardChart.svelte,DashboardFilters.svelte, and other components right alongside your+page.svelteand+page.server.ts. You always know which file is the route entry point, which handles server logic, and which are supporting components. It encourages logical grouping instead of scattering related files across the project.Quick Comparison
Feature Astro Next.js SvelteKit Route structure File name = route Folder + page.tsxFolder + +page.svelteServer/client split ---fences"use client"directiveFile naming convention API routes src/pages/api/app/api/route.ts+server.tsDefault rendering Server Server Server + hydration All three frameworks use file-based routing, but they each have a slightly different philosophy about how to organize and separate concerns. Astro keeps it simple with traditional file mapping. Next.js gives you explicit directives. SvelteKit leans on naming conventions to keep things clean.
I think I can get used to the
+prefix convention in SvelteKit. The type safety between your server file and your page component is nice.Next up in the series, I’ll dig into how each framework handles data loading and forms.
/ Astro / svelte / Web development / Sveltekit / Nextjs
-
Svelte 5 Runes: A React Developer's Guide to Reactivity
Continuing my series on Svelte topics, today we’re talking about runes. If you’re coming from React, this is is going to be a different way to work with reactivity in modern JavaScript.
These blog posts might be what is considered the basics, but it helps me learn and think through the topics if I work on blog posts around the important things that every developer needs to know.
What Are Runes?
In Svelte 5, runes are special symbols that start with the dollar sign (
$). They look like regular JavaScript functions, but they’re actually compiler directives, reserved keywords that tell the Svelte compiler how to wire up reactivity during the build step.If you’ve used decorators in Python or macros in other languages, runes fill a similar role. They look like standard JavaScript, but the compiler transforms them into something more powerful behind the scenes.
Let’s walk through the four runes you’ll use most.
$state— The Engine of Reactivity$stateis the foundation. It declares reactive state in your component.<script> let count = $state(0); </script> <button onclick={() => count++}>{count}</button>In React,
useStatereturns an immutable value and a setter function, so you always need thatsetCountcall. In Svelte,$statereturns a deeply reactive proxy. You just mutate the value directly, and the compiler handles the rest. No setter function, no spread operators for nested objects. It just works.$derived— Computed Values Without Dependency ArraysIn React, you’d reach for
useMemohere, and you’d need to explicitly declare a dependency array so React knows when to recalculate.<script> let count = $state(0); let doubled = $derived(count * 2); </script>Dependency arrays are prone to human error. We forget what depends on what, and that leads to stale data or unnecessary recalculations.
$derivedautomatically tracks whatever state variables are used inside of it. No dependency array needed. It just reads what it reads, and recalculates when those values change.$effect— Side Effects That Actually Make SenseThis is the equivalent of
useEffectin React, which is notoriously tricky. Missing dependencies, stale closures, infinite loops… all the big gotchas are in useEffect calls.In Svelte,
$effectis used to synchronize state with external systems, like writing to local storage or updating a canvas:<script> let theme = $state('dark'); $effect(() => { localStorage.setItem('theme', theme); }); </script>Just like
$derived, it automatically tracks its dependencies and only runs when the state it reads actually changes. No dependency array, no cleanup function gotchas. It runs when it needs to run. That’s it.$props— Clean Component InterfacesEvery framework needs a way to pass data into components. In Svelte 5,
$propsmakes this look like standard JavaScript object destructuring:<script> let { name, age, role = 'viewer' } = $props(); </script> <p>{name} ({age}) - {role}</p>Default values, rest parameters, renaming … it all works exactly how you’d expect from CommonJS. If you know destructuring, you already know
$props. It’s readable, predictable, and there’s nothing new to learn.Runes Me Over
You’ve probably noticed a theme. Svelte 5 runes eliminate a whole class of bugs that come from manually managing dependencies. React makes you think about when things should update. Svelte’s goal is to figure it out for you at compile time.
/ Web-development / javascript / svelte / React
-
Svelte vs React: State Management Without the Ceremony
Continuing my Svelte deep-dive series, let’s talk about state management and reactivity. This is where the differences between React and Svelte can ‘feel’ much different.
React’s State Ceremony
In React, state requires a specific ritual. You declare state with
useState, which gives you a getter and a setter:const [count, setCount] = useState(0); function increment() { setCount(count + 1); }Want to update a variable? You have to call a function. You can’t just reassign
count. React won’t know anything changed. This is fine once you internalize it, but it adds ceremony to what should be a simple operation.Then there’s
useEffect, which is where things get tricky. You need to understand dependency arrays, and if you get them wrong, you’re looking at infinite loops or stale data:useEffect(() => { document.title = `Count: ${count}`; }, [count]); // forget this array and enjoy your infinite loopSome of
useEffectusage is actually unnecessary and likely using it wrong. If you’re using it for data transformations, derived values from state or props, or responding to user events, you’re probably reaching for the wrong tool.The React docs themselves will tell you that you might not need an effect. It’s a common source of bugs and confusion, especially for developers who are still building their mental model of React’s render cycle.
Svelte: Reactivity Through the Language Itself
Svelte takes a fundamentally different approach. Reactivity is baked into the language semantics. Want to declare state? Just declare a variable:
<script> let count = $state(0); function increment() { count += 1; } </script> <button onclick={increment}>{count}</button>That’s it. You assign a new value, and the DOM updates. The Svelte compiler sees your assignments and automatically generates the code to update exactly the parts of the DOM that depend on that variable. No virtual DOM diffing, no setter functions, no dependency arrays to manage.
Need a derived value? Svelte has you covered with
$derived:<script> let count = $state(0); let doubled = $derived(count * 2); </script> <p>{count} doubled is {doubled}</p>In React, you’d either compute this inline, use
useMemowith a dependency array, or… if you didn’t know better reach foruseEffectand a second piece of state (please don’t do this).Svelte’s
$effectrune exists for side effects like updatingdocument.titleor logging, but you should reach for it far less often thanuseEffectin React. The compiler handles most of whatuseEffectgets used for automatically.More Svelte comparisons coming as I keep digging in. Thanks for Svelting with me.
/ javascript / svelte / React / Web development
-
Svelte vs React: The Virtual DOM Tax You Might Not Need
I’m diving more into Svelte and SvelteKit lately, and I’m going to be writing a few posts about it as I learn. Fair warning: some of these will be general knowledge posts, but writing things out helps me internalize the details.
The Virtual DOM Question
It’s well known that React relies on a virtual DOM. The basic idea is that React maintains a copy of the DOM in memory, diffs it against the actual DOM, and then batches the changes to update the real thing. This works, but having to maintain this virtual DOM can lead to complications and confusion around what triggers a render or a re-render. If you’ve ever stared at a
useEffectdependency array wondering why your component is re-rendering, you know what I mean.Svelte takes a completely different approach. It’s not a library you ship to the browser, it’s a compiler step. You write Svelte code, and it compiles down to highly optimized vanilla JavaScript that surgically updates the DOM directly. No virtual DOM to maintain. No diffing algorithm running in the background. The framework essentially disappears at build time, and what you’re left with is just… JavaScript.
Templating That Feels Like the Web
I like how Svelte handles the relationship between HTML, CSS, and JavaScript. React forces you to write HTML inside JavaScript using JSX. You get used to it, sure, but it’s a specific way of thinking about your UI that can take some getting used to.
Svelte flips this around. Your
.sveltefiles are structured more like traditional web pages — you’ve got<script>tags for your JavaScript, regular HTML for markup, and<style>tags for CSS. Everything lives in one file, but there’s a clear separation between the three concerns.If you’ve ever worked with Django templates, Laravel Blade, or Ruby on Rails views, this will feel immediately familiar. It’s a lot closer to how the web actually works than JSX’s “everything is JavaScript” approach. For someone coming from those backgrounds, the learning curve is noticeably gentler.
More Svelte posts coming as I dig deeper. That’s all for now!
/ javascript / svelte / React / Web development
-
What I Actually Mean When I Say "Vibe Coding"
Have you thought about what you actually mean when you tell someone you vibe code? The term gets thrown around a lot, but I think there’s a meaningful distinction worth spelling out. Here’s how I think about it.
Vibe coding, to me, doesn’t mean you’re using an LLM because at this point, it’s hard to avoid using an LLM no matter what type of work you’re doing. The distinction isn’t about the tools, it’s about the intent and the stakes.
Vibe Coding vs. Engineering Work
To me, vibe coding is when I have a simple idea and I want to build a self-contained app that expresses that idea. There’s no spec, no requirements doc, no code review. Just an idea and the energy to make it real.
Engineering work, whether that’s context engineering, spec-driven development, or the old-fashioned way; is when I’m working on open source or paid work. There’s structure, there are standards, and the code needs to hold up over time.
Both are fun. But I take vibe coding less seriously than engineering work.
Vibe coding is the creative playground. Engineering is the craft.
My Setup
All my vibe coded apps live in a single repository. The code isn’t public, but the results are. You can find all my ideas at logan.center.
I have it set up so that every time I add a new folder, it automatically gets its own subdomain.
I’m using Caddy for routing and deploying to Railway. I have an idea, create the app, and boom — it’s live.
I would like to open source a template version of this setup so other people could deploy to something like Railway easily, but I haven’t gotten around to building that yet. One day.
For my repository, I decided it would be fun to build everything in Svelte instead of React.
That’s why you may have seen a bunch of posts from me lately about learning how Svelte works. It’s been fun because the framework stays out of your way and lets you move fast, which is exactly what you want when you’re chasing the vibe.
So, for me, vibe coding is a specific thing: low stakes, high creativity, self-contained apps, and the freedom to just build without overthinking it.
I mean I’m not crazy, I still have testing setup…
/ Programming / Vibe-coding / svelte
-
Why Svelte 5 Wants `let` Instead of `const` (And Why Your Linter Is Confused)
If you’ve been working with Svelte 5 and a linter like Biome, you might have run into this sitauation:
use
constinstead ofletHowever, Svelte actually needs that
let.Have you ever wondered why this is?
Here is an attempt to explain it to you.
Svelte 5’s reactivity model is different from the rest of the TypeScript world, and some of our tooling hasn’t quite caught up yet.
The
constRule Everyone KnowsIn standard TypeScript and React, the
prefer-construle is a solid best practice.If you declare a variable and never reassign it, use
const. It communicates intent clearly: this binding won’t change. You would thinkBut there is confusion when it comes to objects that are defined as
const.Let’s take a look at a React example:
// React — const makes perfect sense here const [count, setCount] = useState(0); const handleClick = () => setCount(count + 1);count is a number (primitive). setCount is a function.
Neither can modify itself so it makes sense to use
const.Svelte 5 Plays by Different Rules
Svelte 5 introduced runes — reactive primitives like
$state(),$derived(), and$props()that bring fine-grained reactivity directly into JavaScript.Svelte compiler transforms these declarations into getters and setters behind the scenes.
The value does get reassigned, even if your code looks like a simple variable.
<script lang="ts"> let count = $state(0); </script> <button onclick={() => count++}> Clicked {count} times </button>Biome Gets This Wrong
But they are trying to make it right. Biome added experimental Svelte support in v2.3.0, but it has a significant limitation: it only analyzes the
<script>block in isolation.It doesn’t see what happens in the template. So when Biome looks at this:
<script lang="ts"> let isOpen = $state(false); </script> <button onclick={() => isOpen = !isOpen}> Toggle </button>It only sees
let isOpen = $state(false)and thinks: “this variable is never reassigned, useconst.”It completely misses the
isOpen = !isOpenhappening in the template markup.If you run
biome check --write, it will automatically changelettoconstand break your app.The Biome team has acknowledged this as an explicit limitation of their partial Svelte support.
For now, the workaround is to either disable the
useConstrule for Svelte files or addbiome-ignorecomments where needed.What About ESLint?
The Svelte ESLint plugin community has proposed two new rules to handle this properly:
svelte/rune-prefer-let(which recommendsletfor rune declarations) and a Svelte-awaresvelte/prefer-constthat understands reactive declarations. These would give you proper linting without the false positives.My Take
Svelte 5’s runes are special and deserve their own way of handling variable declarations.
React hooks are different.
Svelte’s compiler rewrites your variable.
I like Svelte.
Please fix Biome.
Don’t make me use eslint.
/ Programming / svelte / Typescript / Biome / Linting
-
Headless components for Svelte - flexible, unstyled, and accessible primitives that provide the foundation for building your own high-quality component library.
/ links / UIkit / javascript / svelte