07 - Event Handlers

What Changed?

Svelte 5 uses standard DOM event attributes (onclick, oninput, etc.) instead of Svelte 4's custom directive syntax (on:click, on:input). Event handlers are now just props — no special syntax needed.

Svelte 4 (Legacy)

<button on:click={handleClick}>Click</button>
<button on:click|preventDefault={handleSubmit}>Submit</button>

Svelte 5

<button onclick={handleClick}>Click</button>
<!-- No more modifiers like |preventDefault — handle it in the function -->

Basic Usage

<!-- src/lib/components/Counter.svelte -->
<script>
let count = $state(0);

function handleClick() {
  count++;
}
</script>

<button onclick={handleClick}>Count: {count}</button>

Inline Handlers

For simple logic, you can write the handler directly inline:

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

<button onclick={() => count++}>Count: {count}</button>

Event Object

The handler receives the native DOM event object as its first argument:

<script>
function handleClick(event) {
  console.log(event.target);  // the <button> element
}
</script>

<button onclick={handleClick}>Click</button>

Since there are no more modifiers like on:click|preventDefault, you handle it yourself:

<script>
function handleSubmit(event) {
  event.preventDefault();
  // form logic here
}
</script>

<form onsubmit={handleSubmit}>
  <button>Submit</button>
</form>

Custom Events in Child Components

In Svelte 4, you used createEventDispatcher. In Svelte 5, custom events are just callback props — pass a function from parent to child.

Child:

<!-- src/lib/components/SearchForm.svelte -->
<script>
let { onsubmit } = $props();  // callback prop from parent
let value = $state('');
</script>

<form onsubmit={(e) => {
  e.preventDefault();
  onsubmit?.(value);  // call the parent's function
}}>
  <input bind:value />
  <button>Submit</button>
</form>

Parent:

<!-- src/routes/+page.svelte -->
<script>
import SearchForm from '$lib/components/SearchForm.svelte';

function handleSubmit(value) {
  console.log('Submitted:', value);
}
</script>

<SearchForm onsubmit={handleSubmit} />

The ?. in onsubmit?.(value) is optional chaining — it safely does nothing if the parent didn't pass an onsubmit prop.

Passing Additional Arguments

onclick={handleClick} only passes the native event object. Wrap in an arrow function to pass extra arguments:

<script>
function handleClick(message, num, event) {
  console.log(message, num, event.target);
}
</script>

<button onclick={(event) => handleClick('hello', 42, event)}>Click</button>

Especially useful in loops:

{#each items as item, index}
  <button onclick={() => handleClick(item, index)}>
    {item.name}
  </button>
{/each}

Multiple Handlers

No more on:click chaining. Wrap multiple calls in a single handler:

<script>
function handler1() { console.log('Handler 1'); }
function handler2() { console.log('Handler 2'); }
</script>

<button onclick={(e) => {
  handler1();
  handler2();
}}>
  Click
</button>

Key Points

  • Use onclick not on:click — standard DOM attribute names
  • Event handlers are just props, no special Svelte syntax
  • No more modifiers (|preventDefault, |stopPropagation) — call event.preventDefault() etc. in the handler
  • No more createEventDispatcher — pass callback props instead
  • Use optional chaining onsubmit?.() for optional callback props

Interactive Example

<script>
import Child from './07-events-child.svelte';

let count = $state(0);
let clickLog = $state([]);
let submissions = $state([]);

function handleClick(event) {
  count++;
  clickLog.push(`Clicked at ${new Date().toLocaleTimeString()}`);
  if (clickLog.length > 5) clickLog.shift();
}

function handleSubmit(value) {
  submissions.push(value);
}
</script>

<div>
  <h2>07 - Event Handlers Example</h2>
  
  <div>
    <h3>Basic Events (onclick)</h3>
    <button onclick={handleClick}>
      Click me! (Count: {count})
    </button>
    <div>
      {#each clickLog as log}
        <div>{log}</div>
      {/each}
    </div>
  </div>
  
  <div>
    <h3>Inline Handlers</h3>
    <button onclick={() => count++}>Increment</button>
    <button onclick={() => count--}>Decrement</button>
    <button onclick={() => count = 0}>Reset</button>
    <p>Count: {count}</p>
  </div>
  
  <div>
    <h3>Custom Events (via callback props)</h3>
    <Child onsubmit={handleSubmit} />
    <div>
      <strong>Submissions:</strong>
      {#each submissions as sub}
        <div>{sub}</div>
      {/each}
    </div>
  </div>
</div>

<style>
div { margin: 20px; }
h3 { margin-top: 20px; }
button { margin: 5px; }
</style>