โ† Back to DevBytes

Svelte from Beginner to Expert: A Learning Path

Svelte from Beginner to Expert: A Learning Path

Svelte has emerged as one of the most exciting frontend frameworks in recent years. Unlike React or Vue, which do most of their work in the browser at runtime, Svelte shifts that work into a compile step that happens when you build your app. This fundamental difference makes Svelte applications faster, smaller, and often simpler to write. This tutorial walks you through everything you need to know to go from a complete beginner to an expert Svelte developer.

What Is Svelte?

Svelte is a component-based JavaScript framework created by Rich Harris. It allows developers to build user interfaces using a syntax that closely resembles standard HTML, CSS, and JavaScript. The key distinction is that Svelte is a compiler, not a runtime framework. When you build your application, Svelte transforms your components into highly optimized vanilla JavaScript that surgically updates the DOM when state changes.

This compile-time approach means your users never download a large framework runtime. The bundle only contains the code your application actually needs, which results in smaller payloads and faster load times.

Why Svelte Matters

Setting Up Your First Svelte Project

The easiest way to start a new Svelte project is using the official scaffolding tool, Vite. Vite provides a fast development server and an optimized build pipeline.

npm create vite@latest my-svelte-app -- --template svelte
cd my-svelte-app
npm install
npm run dev

Once the dev server is running, open your browser to the displayed URL. You should see a default Svelte welcome page. The project structure is intentionally minimal, with your components living in the src directory.

Svelte Component Basics

A Svelte component is a single .svelte file that contains three sections: markup, a script tag for logic, and a style tag for CSS. Let us look at a simple example.

<script>
  let name = 'world';
</script>

<h1>Hello {name}!</h1>

<style>
  h1 {
    color: tomato;
  }
</style>

The curly braces in the markup indicate a JavaScript expression. Svelte evaluates these expressions and inserts the result into the DOM. When the name variable changes, Svelte automatically updates the DOM.

Reactivity in Svelte

Reactivity in Svelte is triggered by assignment. When you assign a new value to a variable that is referenced in the markup, Svelte updates the affected parts of the DOM. This is a much simpler model than the hook-based approach in React.

<script>
  let count = 0;

  function increment() {
    count += 1;
  }
</script>

<button on:click={increment}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

For reactive declarations that depend on other values, Svelte provides the $: label syntax. Any statement prefixed with $: will re-run whenever its dependencies change.

<script>
  let a = 1;
  let b = 2;

  $: sum = a + b;
  $: console.log(`The sum is ${sum}`);
</script>

<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>Sum: {sum}</p>

Props and Component Composition

Components can receive data from their parents through props. In Svelte, props are declared using the export keyword inside the script block.

<!-- Child.svelte -->
<script>
  export let title = 'Default Title';
  export let items = [];
</script>

<h2>{title}</h2>
<ul>
  {#each items as item}
    <li>{item}</li>
  {/each}
</ul>
<!-- Parent.svelte -->
<script>
  import Child from './Child.svelte';

  const fruits = ['Apple', 'Banana', 'Cherry'];
</script>

<Child title="Fruit List" items={fruits} />

Two-Way Binding

Svelte supports two-way binding using the bind: directive. This is particularly useful for form inputs.

<script>
  let name = '';
  let agree = false;
  let color = '#ff0000';
</script>

<input bind:value={name} placeholder="Your name">
<input type="checkbox" bind:checked={agree}>
<input type="color" bind:value={color}>

<p>Hello, {name || 'stranger'}!</p>
<p>You {agree ? 'agree' : 'do not agree'}.</p>
<p>Your color: {color}</p>

Conditional Rendering and Loops

Svelte provides clean syntax for conditional rendering and list iteration. The {#if} block handles conditionals, while {#each} handles loops.

<script>
  let user = { name: 'Alice', loggedIn: true };
  let todos = [
    { id: 1, text: 'Learn Svelte', done: true },
    { id: 2, text: 'Build an app', done: false },
    { id: 3, text: 'Deploy to production', done: false }
  ];
</script>

{#if user.loggedIn}
  <p>Welcome back, {user.name}!</p>
{:else}
  <p>Please log in.</p>
{/if}

<ul>
  {#each todos as todo, i}
    <li class:done={todo.done}>
      {i + 1}. {todo.text}
      {#if todo.done}โœ“{/if}
    </li>
  {:else}
    <li>No todos yet.</li>
  {/each}
</ul>

<style>
  .done { text-decoration: line-through; opacity: 0.6; }
</style>

Event Handling

Event listeners are attached using the on: directive. Svelte also supports event modifiers like preventDefault, stopPropagation, and once.

<script>
  function handleClick(event) {
    alert('Button clicked!');
  }

  function handleSubmit(event) {
    event.preventDefault();
    console.log('Form submitted');
  }

  function handleKeydown(event) {
    if (event.key === 'Enter') {
      console.log('Enter pressed');
    }
  }
</script>

<button on:click={handleClick}>Click me</button>

<form on:submit={handleSubmit}>
  <input on:keydown={handleKeydown} placeholder="Type and press Enter">
  <button type="submit">Submit</button>
</form>

<button on:click|once={() => alert('This only fires once')}>
  One-time button
</button>

Component Events

Child components can dispatch custom events using createEventDispatcher. Parents listen to these events using the same on: directive.

<!-- Child.svelte -->
<script>
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  function notify() {
    dispatch('notify', {
      message: 'Something happened!',
      timestamp: Date.now()
    });
  }
</script>

<button on:click={notify}>Notify Parent</button>
<!-- Parent.svelte -->
<script>
  import Child from './Child.svelte';

  function handleNotify(event) {
    console.log(event.detail.message);
    console.log(event.detail.timestamp);
  }
</script>

<Child on:notify={handleNotify} />

State Management with Stores

For state that needs to be shared across many components, Svelte provides stores. A store is simply an object with a subscribe method. Svelte includes two writable store types: writable and readable, plus a derived store for computed values.

<!-- stores.js -->
import { writable, derived } from 'svelte/store';

export const count = writable(0);

export const doubled = derived(count, $count => $count * 2);

export const reset = () => count.set(0);
<!-- Counter.svelte -->
<script>
  import { count, doubled, reset } from './stores.js';

  function increment() {
    count.update(n => n + 1);
  }
</script>

<h1>Count: {$count}</h1>
<p>Doubled: {$doubled}</p>
<button on:click={increment}>+</button>
<button on:click={reset}>Reset</button>

The $ prefix is Svelte shorthand for subscribing to a store. It automatically subscribes when the component mounts and unsubscribes when it unmounts, preventing memory leaks.

Creating a Custom Store

Any object that implements the subscribe contract can be used as a store. This lets you create stores with custom logic.

<script>
  import { writable } from 'svelte/store';

  function createTodoStore() {
    const { subscribe, set, update } = writable([]);

    return {
      subscribe,
      add: (text) => update(todos => [...todos, { id: Date.now(), text, done: false }]),
      toggle: (id) => update(todos => todos.map(t => t.id === id ? { ...t, done: !t.done } : t)),
      remove: (id) => update(todos => todos.filter(t => t.id !== id)),
      clear: () => set([])
    };
  }

  const todos = createTodoStore();
  let newText = '';
</script>

<form on:submit|preventDefault={() => { todos.add(newText); newText = ''; }}>
  <input bind:value={newText} placeholder="New todo">
  <button type="submit">Add</button>
</form>

<ul>
  {#each $todos as todo (todo.id)}
    <li>
      <input type="checkbox" bind:checked={todo.done}>
      <span class:done={todo.done}>{todo.text}</span>
      <button on:click={() => todos.remove(todo.id)}>x</button>
    </li>
  {/each}
</ul>

<style>
  .done { text-decoration: line-through; }
</style>

Lifecycle Methods

Svelte provides four lifecycle functions: onMount, onDestroy, beforeUpdate, and afterUpdate. These let you run code at specific points in a component's life.

<script>
  import { onMount, onDestroy, beforeUpdate, afterUpdate } from 'svelte';

  let data = [];
  let timer;

  onMount(async () => {
    const response = await fetch('https://api.example.com/data');
    data = await response.json();
    timer = setInterval(() => console.log('tick'), 1000);
  });

  onDestroy(() => {
    clearInterval(timer);
  });

  beforeUpdate(() => {
    console.log('DOM is about to update');
  });

  afterUpdate(() => {
    console.log('DOM just updated');
  });
</script>

<ul>
  {#each data as item}
    <li>{item.name}</li>
  {/each}
</ul>

Transitions and Animations

One of Svelte's standout features is its built-in transition system. You can animate elements entering and leaving the DOM with minimal code.

<script>
  import { fade, fly, slide, scale } from 'svelte/transition';
  import { quintOut } from 'svelte/easing';

  let visible = true;
  let items = [1, 2, 3];
</script>

<label>
  <input type="checkbox" bind:checked={visible}>
  Toggle
</label>

{#if visible}
  <p transition:fade>I fade in and out</p>
  <p in:fly={{ x: 100, duration: 500 }} out:scale>I fly in and scale out</p>
{/if}

<button on:click={() => items = [...items, items.length + 1]}>Add</button>
<button on:click={() => items = items.slice(0, -1)}>Remove</button>

{#each items as item (item)}
  <div transition:slide={{ duration: 300, easing: quintOut }}>
    Item {item}
  </div>
{/each}

Actions

Actions are functions that run when an element is added to the DOM. They are perfect for integrating third-party libraries or adding reusable behaviors like tooltips, lazy loading, or drag and drop.

<script>
  function longpress(node, duration) {
    let timer;

    const handleMousedown = () => {
      timer = setTimeout(() => {
        node.dispatchEvent(new CustomEvent('longpress'));
      }, duration);
    };

    const handleMouseup = () => {
      clearTimeout(timer);
    };

    node.addEventListener('mousedown', handleMousedown);
    node.addEventListener('mouseup', handleMouseup);

    return {
      update(newDuration) {
        duration = newDuration;
      },
      destroy() {
        node.removeEventListener('mousedown', handleMousedown);
        node.removeEventListener('mouseup', handleMouseup);
      }
    };
  }
</script>

<button use:longpress={500} on:longpress={() => alert('Long pressed!')}>
  Press and hold me
</button>

Slots and Content Projection

Slots allow you to pass content into a component from its parent. This is similar to children in React or ng-content in Angular.

<!-- Card.svelte -->
<div class="card">
  <div class="card-header">
    <slot name="header">Default Header</slot>
  </div>
  <div class="card-body">
    <slot>Default body content</slot>
  </div>
  <div class="card-footer">
    <slot name="footer" />
  </div>
</div>

<style>
  .card { border: 1px solid #ccc; border-radius: 8px; overflow: hidden; }
  .card-header, .card-footer { background: #f5f5f5; padding: 12px; }
  .card-body { padding: 16px; }
</style>
<!-- App.svelte -->
<script>
  import Card from './Card.svelte';
</script>

<Card>
  <svelte:fragment slot="header">
    <h3>My Card Title</h3>
  </svelte:fragment>

  <p>This is the main content of the card.</p>
  <p>You can put anything here.</p>

  <div slot="footer">
    <button>Save</button>
    <button>Cancel</button>
  </div>
</Card>

Context API

For sharing state deeply through a component tree without prop drilling, Svelte offers the Context API. Context is set by a parent component and accessed by any descendant.

<!-- Parent.svelte -->
<script>
  import { setContext } from 'svelte';
  import Child from './Child.svelte';

  setContext('theme', {
    color: 'dark',
    toggle: () => console.log('toggle theme')
  });
</script>

<Child />
<-- DeepChild.svelte -->
<script>
  import { getContext } from 'svelte';

  const theme = getContext('theme');
</script>

<p>Current theme: {theme.color}</p>
<button on:click={theme.toggle}>Toggle Theme</button>

Special Elements

Svelte includes several special elements that give you control over rendering behavior.

<script>
  import BrowserWarning from './BrowserWarning.svelte';
  import ServerOnly from './ServerOnly.svelte';
</script>

<!-- Render only on the client -->
<svelte:head>
  <title>My Svelte App</title>
  <meta name="description" content="Built with Svelte">
</svelte:head>

<!-- Access window dimensions -->
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />

<!-- Access document body -->
<svelte:body on:mouseenter={handleMouseEnter} />

<!-- Dynamic component rendering -->
<svelte:component this={currentComponent} {...props} />

<!-- Self-rendering component -->
<svelte:self />

SvelteKit: The Full-Stack Framework

Once you are comfortable with Svelte components, the next step is SvelteKit. SvelteKit is the official application framework built on top of Svelte. It provides routing, server-side rendering, API endpoints, and deployment adapters.

npm create svelte@latest my-app
cd my-app
npm install
npm run dev

SvelteKit uses file-based routing. Each file in src/routes becomes a route in your application.

<!-- src/routes/+page.svelte -->
<script>
  export let data;
</script>

<h1>{data.title}</h1>
<p>{data.description}</p>
// src/routes/+page.server.js
export async function load() {
  return {
    title: 'Welcome to SvelteKit',
    description: 'Server-side loaded data'
  };
}

Layouts in SvelteKit

Layout components wrap every page in their directory and below. This is useful for navigation, headers, and footers.

<!-- src/routes/+layout.svelte -->
<script>
  import { page } from '$app/stores';
  let { children } = $props();
</script>

<nav>
  <a href="/" class:active={$page.url.pathname === '/'}>Home</a>
  <a href="/about" class:active={$page.url.pathname === '/about'}>About</a>
  <a href="/blog" class:active={$page.url.pathname.startsWith('/blog')}>Blog</a>
</nav>

<main>
  {@render children()}
</main>

<style>
  nav { display: flex; gap: 16px; padding: 16px; background: #333; }
  nav a { color: white; text-decoration: none; }
  nav a.active { text-decoration: underline; }
</style>

Best Practices

Keep Components Small and Focused

Each component should do one thing well. If a component grows beyond 200 lines, consider splitting it into smaller pieces. This improves readability, testability, and reusability.

Use Stores for Shared State

Do not pass props through many layers. If more than two components need the same state, lift it into a store. This keeps your component interfaces clean and avoids prop drilling.

Leverage Scoped Styles

Svelte scopes styles to components by default. Use this to your advantage. Avoid global styles unless absolutely necessary, and prefer class-based styling over inline styles for maintainability.

Use Reactive Statements Wisely

The $: syntax is powerful but can lead to confusing code if overused. Keep reactive statements simple and avoid chaining too many together. If logic becomes complex, extract it into a function or a derived store.

Handle Errors Gracefully

In SvelteKit, use error boundaries and the error store to handle unexpected errors. Always validate data on the server before sending it to the client.

// src/routes/+error.svelte
<script>
  import { page } from '$app/stores';
</script>

<h1>{$page.status}: {$page.error.message}</h1>
<a href="/">Go home</a>

Optimize Bundle Size

Use dynamic imports for large dependencies that are only needed on certain routes. SvelteKit automatically code-splits by route, but you can further optimize with manual imports.

<script>
  let chart;

  async function loadChart() {
    const { default: Chart } = await import('chart.js');
    chart = new Chart(/* ... */);
  }
</script>

<button on:click={loadChart}>Load Chart</button>

Write Tests

Use Vitest and Testing Library to test your components. Focus on user interactions rather than implementation details.

import { render, fireEvent } from '@testing-library/svelte';
import Counter from './Counter.svelte';

test('increments when clicked', async () => {
  const { getByText } = render(Counter);
  const button = getByText('Clicked 0 times');

  await fireEvent.click(button);
  expect(button.textContent).toBe('Clicked 1 time');
});

Use TypeScript

Svelte has excellent TypeScript support. Type your props, stores, and API responses to catch errors at compile time.

<script lang="ts">
  interface User {
    id: number;
    name: string;
    email: string;
  }

  export let user: User;

  function updateUser(field: keyof User, value: string): void {
    user = { ...user, [field]: value };
  }
</script>

<input value={user.name} on:input={(e) => updateUser('name', e.target.value)} />
<input value={user.email} on:input={(e) => updateUser('email', e.target.value)} />

Advanced Patterns

Higher-Order Components

While Svelte does not have HOCs in the React sense, you can achieve similar composition using wrapper components and slots or context.

Reactive Context with Stores

Combine the Context API with stores to create reactive, scoped state that updates across the component tree.

<script>
  import { setContext } from 'svelte';
  import { writable } from 'svelte/store';

  const cart = writable([]);

  setContext('cart', {
    subscribe: cart.subscribe,
    add: (item) => cart.update(items => [...items, item]),
    remove: (id) => cart.update(items => items.filter(i => i.id !== id)),
    clear: () => cart.set([])
  });
</script>

Using Svelte in Existing Projects

Svelte can be embedded into existing applications. You can mount a Svelte component into any DOM element using the client-side API.

import App from './App.svelte';

const app = new App({
  target: document.getElementById('svelte-root'),
  props: {
    initialData: window.__INITIAL_DATA__
  }
});

export default app;

Conclusion

Svelte offers a refreshingly simple approach to building web applications. Its compile-time strategy eliminates the need for a virtual DOM, resulting in smaller bundles and better performance. By starting with the basics of components, reactivity, and props, then progressing through stores, lifecycle methods, transitions, and finally SvelteKit for full-stack development, you can build a solid foundation as a Svelte developer. The key to mastery is practice: build small projects, experiment with the patterns covered here, and gradually take on more complex applications. With its gentle learning curve, excellent documentation, and growing ecosystem, Svelte is a framework worth investing your time in. Whether you are building a small widget or a large-scale application, Svelte provides the tools you need to ship fast, maintainable, and delightful user experiences.

๐Ÿ›  Tools from DevBytes

Inventory Tracker Pro โ€” Excel inventory system, low-stock alerts ยท $19
AI Dev Kit for Mac โ€” local AI dev environment templates ยท $9.99
KeyMapper for Mac โ€” custom keyboard shortcut toolkit ยท $7.99

โ† Back to all articles