Skip to content

Svelte 5

Guide for using Svelte 5 with runes in Laju applications.

Runes Overview

Svelte 5 introduces runes - a new reactive primitive system:

RunePurpose
$stateReactive state
$derivedComputed values
$effectSide effects
$propsComponent props
$bindableTwo-way bindable props

State Management

Basic State

svelte
<script>
  // Reactive state
  let count = $state(0);
  let name = $state('');
  let items = $state([]);
  
  function increment() {
    count++;
  }
  
  function addItem(item) {
    items.push(item);  // Mutating arrays works!
  }
</script>

<button onclick={increment}>Count: {count}</button>
<input bind:value={name} />

Object State

svelte
<script>
  let user = $state({
    name: '',
    email: '',
    preferences: {
      theme: 'light',
      notifications: true
    }
  });
  
  function updateTheme(theme) {
    user.preferences.theme = theme;  // Deep reactivity
  }
</script>

<input bind:value={user.name} />
<select bind:value={user.preferences.theme}>
  <option value="light">Light</option>
  <option value="dark">Dark</option>
</select>

Array State

svelte
<script>
  let todos = $state([
    { id: 1, text: 'Learn Svelte 5', done: false },
    { id: 2, text: 'Build app', done: false }
  ]);
  
  function addTodo(text) {
    todos.push({ id: Date.now(), text, done: false });
  }
  
  function toggleTodo(id) {
    const todo = todos.find(t => t.id === id);
    if (todo) todo.done = !todo.done;
  }
  
  function removeTodo(id) {
    const index = todos.findIndex(t => t.id === id);
    if (index > -1) todos.splice(index, 1);
  }
</script>

{#each todos as todo}
  <div>
    <input type="checkbox" checked={todo.done} onchange={() => toggleTodo(todo.id)} />
    <span class:done={todo.done}>{todo.text}</span>
    <button onclick={() => removeTodo(todo.id)}>Delete</button>
  </div>
{/each}

Props and Events

Receiving Props

svelte
<script>
  // Destructure props with $props()
  let { title, count = 0, items = [] } = $props();
</script>

<h1>{title}</h1>
<p>Count: {count}</p>

Props with Types (TypeScript)

svelte
<script lang="ts">
  interface Props {
    title: string;
    count?: number;
    onSubmit?: (data: FormData) => void;
  }
  
  let { title, count = 0, onSubmit }: Props = $props();
</script>

Event Callbacks

svelte
<!-- Parent.svelte -->
<script>
  import Child from './Child.svelte';
  
  function handleClick(message) {
    console.log('Child clicked:', message);
  }
</script>

<Child onClick={handleClick} />

<!-- Child.svelte -->
<script>
  let { onClick } = $props();
</script>

<button onclick={() => onClick?.('Hello from child')}>
  Click me
</button>

Two-way Binding with $bindable

svelte
<!-- TextInput.svelte -->
<script>
  let { value = $bindable('') } = $props();
</script>

<input bind:value class="border rounded px-3 py-2 focus:outline-none" />

<!-- Usage -->
<script>
  import TextInput from './TextInput.svelte';
  let name = $state('');
</script>

<TextInput bind:value={name} />
<p>Name: {name}</p>

Effects and Derived

Derived Values

svelte
<script>
  let items = $state([
    { name: 'Apple', price: 1.5 },
    { name: 'Banana', price: 0.75 }
  ]);
  
  // Computed value - recalculates when items change
  let total = $derived(items.reduce((sum, item) => sum + item.price, 0));
  let count = $derived(items.length);
  let isEmpty = $derived(items.length === 0);
</script>

<p>Items: {count}</p>
<p>Total: ${total.toFixed(2)}</p>

Effects

svelte
<script>
  let searchQuery = $state('');
  let results = $state([]);
  
  // Run effect when searchQuery changes
  $effect(() => {
    if (searchQuery.length > 2) {
      fetchResults(searchQuery);
    }
  });
  
  async function fetchResults(query) {
    const res = await fetch(`/api/search?q=${query}`);
    results = await res.json();
  }
</script>

<input bind:value={searchQuery} placeholder="Search..." />

Effect with Cleanup

svelte
<script>
  let count = $state(0);
  
  $effect(() => {
    const interval = setInterval(() => {
      count++;
    }, 1000);
    
    // Cleanup function
    return () => clearInterval(interval);
  });
</script>

<p>Seconds: {count}</p>

Debounced Effect

svelte
<script>
  let searchQuery = $state('');
  let results = $state([]);
  
  $effect(() => {
    const query = searchQuery;
    
    if (query.length < 3) {
      results = [];
      return;
    }
    
    const timeout = setTimeout(async () => {
      const res = await fetch(`/api/search?q=${query}`);
      results = await res.json();
    }, 300);
    
    return () => clearTimeout(timeout);
  });
</script>

Common Patterns

svelte
<!-- Modal.svelte -->
<script>
  let { open = $bindable(false), title, children } = $props();
  
  function close() {
    open = false;
  }
  
  function handleKeydown(e) {
    if (e.key === 'Escape') close();
  }
</script>

<svelte:window onkeydown={handleKeydown} />

{#if open}
  <div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onclick={close}>
    <div class="bg-white rounded-lg p-6 max-w-md w-full mx-4" onclick={(e) => e.stopPropagation()}>
      <div class="flex justify-between items-center mb-4">
        <h2 class="text-xl font-bold">{title}</h2>
        <button onclick={close} class="text-gray-500 hover:text-gray-700">X</button>
      </div>
      {@render children()}
    </div>
  </div>
{/if}

<!-- Usage -->
<script>
  import Modal from './Modal.svelte';
  let showModal = $state(false);
</script>

<button onclick={() => showModal = true}>Open Modal</button>

<Modal bind:open={showModal} title="Confirm Action">
  <p>Are you sure you want to proceed?</p>
  <div class="mt-4 flex gap-2">
    <button onclick={() => showModal = false}>Cancel</button>
    <button class="bg-blue-500 text-white px-4 py-2 rounded">Confirm</button>
  </div>
</Modal>

Toast Notifications

Use the Toast helper function for quick notifications:

svelte
<script>
  import { Toast } from '@/Components/helper.js';
  
  function showSuccess() {
    Toast('Operation successful!', 'success', 3000);
  }
  
  function showError() {
    Toast('Something went wrong', 'error', 3000);
  }
  
  function showWarning() {
    Toast('Please check your input', 'warning', 3000);
  }
  
  function showInfo() {
    Toast('New message received', 'info', 3000);
  }
</script>

<button onclick={showSuccess}>Show Success</button>
<button onclick={showError}>Show Error</button>
<button onclick={showWarning}>Show Warning</button>
<button onclick={showInfo}>Show Info</button>

Loading State

svelte
<script>
  let loading = $state(false);
  let data = $state(null);
  
  async function fetchData() {
    loading = true;
    try {
      const res = await fetch('/api/data');
      data = await res.json();
    } finally {
      loading = false;
    }
  }
</script>

{#if loading}
  <div class="flex justify-center p-4">
    <div class="animate-spin h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full"></div>
  </div>
{:else if data}
  <div>{JSON.stringify(data)}</div>
{:else}
  <button onclick={fetchData}>Load Data</button>
{/if}

Next Steps

Released under the MIT License.