web-frameworks Svelte Themes

Web Frameworks

Educational repository that implements the same counter app in plain HTML/CSS/JavaScript and in SvelteKit (Svelte 5) to clearly compare structure, state management, and framework tradeoffs

Web Frameworks: Vanilla vs SvelteKit

This repository builds the same counter application twice -- once with plain HTML, CSS, and JavaScript, and once with SvelteKit (Svelte 5). Both produce an identical UI: a heading, a large number display, and three buttons (-1, Reset, +1). By comparing the two side by side, we can see exactly what a modern framework gives you and what it costs.

Getting Started

npm install     # installs root deps + both sub-projects
npm run dev     # runs both apps concurrently

The vanilla app is served by live-server; the SvelteKit app is served by Vite.


1. State Management

This is the most important difference between the two approaches.

Vanilla -- imperative, manual DOM sync

State is a plain JavaScript variable declared inside an inline <script> tag. Every time the value changes, the developer must explicitly push the new value into the DOM:

let count = 0;
const display = document.getElementById("count");
const update = () => {
    display.textContent = count;
};

document.getElementById("increment").addEventListener("click", () => {
    count++;
    update(); // forget this line and the UI is stale
});

Key characteristics:

  • You own the sync loop. The variable and the DOM are two independent sources of truth. It is your job to keep them aligned after every mutation.
  • No abstraction. There is no reactive system, no virtual DOM, no diffing -- just a variable and textContent.
  • Scales poorly. As the number of stateful elements grows, the number of manual update() calls grows with it. Missing one call produces a bug that is silent and hard to catch.

SvelteKit -- declarative, automatic reactivity

State is declared with Svelte 5's $state rune. The framework tracks which parts of the DOM depend on that state and updates them automatically:

<!-- Counter.svelte -->
<script>
    import CounterDisplay from './CounterDisplay.svelte';
    import CounterButton from './CounterButton.svelte';

    let count = $state(0);
</script>

<CounterDisplay value={count} />
<CounterButton label="+1" onclick={() => count++} />

Key characteristics:

  • Single source of truth. count is the state, and every place that reads it re-renders automatically when it changes. There is no separate sync step.
  • Props flow data downward. CounterDisplay receives value via $props() and simply renders it. The display component does not need to know where the value came from or how it was updated.
  • Callbacks flow actions upward. CounterButton receives an onclick handler as a prop. The button fires it; the parent decides what happens. This keeps child components stateless and reusable.
  • Scales well. Adding more reactive state means adding more $state() declarations. The framework handles the rest.

Side-by-side

Aspect Vanilla SvelteKit
Declaration let count = 0 let count = $state(0)
Reading state display.textContent {count} in template
Mutating state count++; update() count++
Passing state down n/a (single file) Props via $props()
DOM sync Manual (update()) Automatic (compiler-generated)
Risk of stale UI High (forget update()) None (compiler guarantees sync)

2. Architecture and Code Organisation

Vanilla -- single-file, flat

The entire app lives in one HTML file (vanilla/app.html) with an external stylesheet. Markup, logic, and event wiring are all co-located in one place:

vanilla/
├── app.html       ← markup + inline <script>
├── app.css        ← all styles
├── package.json
└── package-lock.json

This is the simplest possible architecture. There are no imports, no modules, no components. For a counter this is fine -- for anything larger, the file would grow into a monolith.

SvelteKit -- component tree, file-based routing

The app is split into focused, single-responsibility components:

svelte/src/
├── app.html               ← shell template
├── app.css                ← global reset + design tokens
├── routes/
│   ├── +layout.svelte     ← root layout (fonts, global CSS)
│   └── +page.svelte       ← renders <Counter />
└── lib/
    ├── Counter.svelte         ← stateful parent
    ├── CounterDisplay.svelte  ← presentational (shows value)
    └── CounterButton.svelte   ← presentational (fires onclick)

The component tree looks like this:

+page.svelte
  └── Counter (owns state)
        ├── CounterDisplay (reads value prop)
        └── CounterButton x3 (fires onclick prop)

Each component encapsulates its own markup, logic, and scoped styles. Adding a new page means adding a new +page.svelte file -- routing is automatic.


3. Styling

Both approaches share the same design tokens (CSS custom properties) and the same CSS Cascade Layers for controlling specificity.

Vanilla -- global stylesheet, four layers

All styles live in a single app.css with four layers:

@layer reset, base, layout, components;

Every selector is global. Button variants are targeted by ID (#reset), and layout classes like .container and .buttons exist in a flat namespace. In a larger project, naming collisions become a real concern.

SvelteKit -- global base + scoped component styles

The global app.css is trimmed to just two layers (reset, base) for the universal reset and design tokens. Everything else moves into scoped <style> blocks inside each component:

<!-- CounterButton.svelte -->
<style>
    button {
        /* these rules only apply to <button> inside this component */
        background: var(--color-btn);
    }
    button.alt {
        background: var(--color-btn-alt);
    }
</style>

Svelte scopes these styles at compile time by adding unique class attributes, so button in CounterButton.svelte will never leak into other components. You can write simple, semantic selectors without worrying about collisions.

Aspect Vanilla SvelteKit
Scope Global Scoped per component
Layer count 4 (reset, base, layout, components) 2 global (reset, base) + scoped
Naming risk Collisions possible No collisions (compiler-scoped)
Colocation Separate file <style> lives with its component

4. Tooling and Build Pipeline

Vanilla -- no build step

"devDependencies": {
    "live-server": "^1.2.2"
}

That is the entire toolchain. live-server watches for file changes and reloads the browser. There is no bundler, no transpiler, no minifier. The browser receives exactly the files you wrote.

Pros: zero configuration, instant startup, nothing to debug in the build layer. Cons: no module system (no import/export), no tree-shaking, no code-splitting, no TypeScript, no asset hashing for cache busting.

SvelteKit -- Vite-powered pipeline

"devDependencies": {
    "@sveltejs/adapter-auto": "^7.0.0",
    "@sveltejs/kit": "^2.50.2",
    "@sveltejs/vite-plugin-svelte": "^6.2.4",
    "svelte": "^5.51.0",
    "vite": "^7.3.1"
}

The SvelteKit build pipeline includes:

  • Vite for fast HMR (Hot Module Replacement) during development -- changes reflect in the browser without a full page reload, and component state is preserved across edits.
  • Svelte compiler that transforms .svelte files into optimised JavaScript at build time.
  • Adapter system that can output to Vercel, Netlify, Cloudflare, Node, or static files depending on your deployment target.
  • SSR / SSG / CSR support out of the box -- choose server-side rendering, static-site generation, or client-side rendering per route.

Pros: module system, tree-shaking, code-splitting, TypeScript support, asset hashing, SSR, adapters for any hosting platform. Cons: more dependencies, more configuration surface, longer initial setup, build errors are possible.


5. Scalability

The counter is trivial. The real question is: what happens when the app grows?

Concern Vanilla SvelteKit
Adding pages Create new HTML files, manually link between them, duplicate shared markup Add a +page.svelte file -- routing is automatic
Shared state Global variables on window, or hand-rolled pub/sub Svelte stores, context API, or shared $state modules
Data fetching fetch() in <script>, manual loading/error states +page.server.js load functions with built-in loading UI
Forms Manual FormData, manual validation, manual error display SvelteKit form actions with progressive enhancement
Code reuse Copy-paste or <template> element cloning Import components, pass props
Testing Attach test scripts to the global scope Standard module imports, component testing with Vitest

In vanilla, every new feature requires you to build the plumbing yourself. In SvelteKit, the framework provides the plumbing and you focus on the feature.


6. Performance

Vanilla

  • Zero framework overhead. The browser parses HTML, applies CSS, and runs a small <script>. There is nothing to download, parse, or initialise beyond your own code.
  • No abstraction cost. DOM updates are direct textContent assignments -- the fastest possible mutation.
  • Scales poorly under complexity. As the app grows, naive DOM manipulation (e.g., rebuilding lists by setting innerHTML) becomes a performance bottleneck. You would need to implement your own diffing or virtualisation.

SvelteKit

  • Compiles away the framework. Unlike React or Vue, Svelte does not ship a runtime to the browser. The compiler generates surgical, imperative DOM update code at build time.
  • Granular updates. The compiled output updates only the exact DOM nodes that depend on changed state -- no virtual DOM diffing.
  • Optimised asset delivery. Vite handles code-splitting, tree-shaking, and asset hashing automatically. Users download only the code they need.
  • SSR reduces time-to-first-paint. The server can send pre-rendered HTML so the page appears before JavaScript loads.

For a counter, both approaches are instantaneous. At scale, SvelteKit's compiled output stays fast without manual optimisation, while vanilla requires increasing effort to maintain performance.


7. Developer Experience

Aspect Vanilla SvelteKit
Setup time Seconds (create an HTML file) Minutes (npm create svelte, install deps)
Learning curve Just HTML/CSS/JS -- nothing new Svelte syntax, runes, SvelteKit conventions
Editor support Basic HTML/CSS/JS intellisense Svelte language server with autocomplete, diagnostics, go-to-definition
Hot reload Full page reload via live-server HMR via Vite (preserves component state)
Accessibility No built-in checks Compiler warns about missing alt text, label associations, etc.
Type safety None (unless you add JSDoc) Optional TypeScript with full inference across components
Debugging Browser DevTools, console.log Browser DevTools + Svelte DevTools extension for inspecting component state
Error messages Runtime errors only Compile-time errors catch mistakes before the browser

8. When to Use Which

Choose vanilla HTML/CSS/JS when:

  • The page is static or has minimal interactivity (landing pages, documentation, blogs).
  • You want zero dependencies and the simplest possible deployment (any static file host).
  • You are learning the fundamentals and want to understand what frameworks abstract away.
  • The project is a prototype or throwaway experiment.

Choose SvelteKit when:

  • The app has meaningful interactivity and state (dashboards, forms, real-time UIs).
  • You need multiple pages or routes.
  • You want scoped styles, component reuse, and a module system.
  • You plan to scale the codebase with a team.
  • You need SSR, SSG, or flexible deployment targets.

Summary

Dimension Vanilla SvelteKit
State management Manual, imperative Automatic, reactive ($state)
DOM updates Explicit (textContent, innerHTML) Compiler-generated, surgical
Architecture Single file, flat Component tree, file-based routing
Styling Global CSS Global base + scoped component styles
Build step None Vite (HMR, bundling, tree-shaking)
Dependencies 1 (live-server) 5 (Svelte, SvelteKit, Vite, adapter, plugin)
Scalability Low (manual plumbing) High (framework handles plumbing)
Performance baseline Excellent (zero overhead) Excellent (compiles to vanilla JS)
Performance at scale Degrades without manual work Stays fast (optimised compilation)
Learning curve Low (just the platform) Moderate (framework concepts)
Best for Simple/static pages, learning Interactive apps, growing codebases

Top categories

Loading Svelte Themes