Skip to content
ExponaBLOG
SVELTE & SVELTEKIT · DAY 008 / 100

Events in Svelte 5: a practical Svelte & SvelteKit guide

Event handling is just props now.

Expona Admin6 min read

Events in Svelte 5: a practical Svelte & SvelteKit guide

If you've worked with Svelte 4 and earlier, you're used to the on: directive syntax for event handling. Svelte 5 flips the script entirely: event handling is just props now. This shift simplifies your mental model, makes components more testable, and aligns better with how JavaScript frameworks typically work. In this Events in Svelte 5: a practical Svelte & SvelteKit guide, we'll walk through the reasoning, the mechanics, and how to adapt your event-heavy components to this cleaner approach.

The shift from on: directives to onclick

In Svelte 4, you'd bind events using directives:

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

Svelte 5 removes this syntactic sugar. Instead, events become regular callback props. The onclick prop receives a function that the component invokes when the event fires:

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

On the surface, this looks like a minor syntax change. But the implications are significant. Your component isn't "listening" to an event anymore—it's accepting a callback function as a prop, just like any other data. This makes the event flow explicit and traceable through your props interface.

The pattern extends to all DOM events: onmouseenter, onkeydown, onfocus, onchange, and so on. If you can set it as an HTML attribute in standard JavaScript, you can use it as a prop in Svelte 5.

For teams moving a large codebase, this is straightforward to migrate. A find-and-replace from on: to on handles most of the mechanical work, though you'll want to validate the results.

Pass callback props to children

The real power emerges when you think about component composition. In Svelte 4, child components would dispatch events upward using createEventDispatcher. In Svelte 5, children simply accept callback props and call them.

Imagine a shopping cart component that needs to notify its parent when an item is removed. In Svelte 4, you'd use the dispatcher pattern. In Svelte 5:

typescript
// CartItem.svelte (child)
<script>
  let { item, onRemove } = $props();
</script>

<div>
  <span>{item.name}</span>
  <button onclick={() => onRemove(item.id)}>
    Remove
  </button>
</div>

The parent passes the callback:

typescript
// ShoppingCart.svelte (parent)
<script>
  let items = $state([
    { id: 1, name: 'Widget' },
    { id: 2, name: 'Gadget' }
  ]);

  function handleRemoveItem(itemId) {
    items = items.filter(item => item.id !== itemId);
  }
</script>

<div>
  {#each items as item (item.id)}
    <CartItem item={item} onRemove={handleRemoveItem} />
  {/each}
</div>

This is more intuitive than learning the dispatcher pattern. The data flows down as props, and callbacks flow up the same way. There's no separate mental model for events. If you've built React components, this pattern is immediately familiar.

When you have deeply nested components, you can thread callbacks through multiple levels without ceremony. No need to destructure and re-dispatch at each layer—just pass the prop down.

Use event modifiers via wrapper functions

Svelte 4's event modifiers like on:click|stopPropagation or on:keydown|preventDefault were handy shortcuts. Svelte 5 doesn't have syntactic modifiers, but you don't lose the functionality—you just write it explicitly with wrapper functions.

Say you have a form input that should prevent the default browser behavior on keydown:

typescript
// TextInput.svelte
<script>
  let { onKeydown } = $props();

  function handleKeydown(event) {
    if (event.key === 'Enter') {
      event.preventDefault();
      onKeydown?.(event);
    }
  }
</script>

<input onkeydown={handleKeydown} />

Or, if you want to stop propagation on a click event:

typescript
// ModalContent.svelte
<script>
  let { onClick } = $props();

  function handleClick(event) {
    event.stopPropagation();
    onClick?.(event);
  }
</script>

<div onmousedown={handleClick}>
  <!-- modal content -->
</div>

The wrapper function approach is more verbose than the modifier syntax, but it's also more explicit. You see exactly what's happening. For teams that use event modifiers frequently, creating a utility layer can reduce repetition:

typescript
// eventModifiers.ts
export function stopPropagation(handler) {
  return (event) => {
    event.stopPropagation();
    handler?.(event);
  };
}

export function preventDefault(handler) {
  return (event) => {
    event.preventDefault();
    handler?.(event);
  };
}

Then use it in your components:

typescript
import { stopPropagation } from './eventModifiers';

<div onmousedown={stopPropagation(onClick)}>

This keeps your component code cleaner while making the modifier logic reusable across your codebase.

Simpler than createEventDispatcher

The createEventDispatcher API in Svelte 4 added conceptual overhead. You had to import it, instantiate it, and remember the dispatch syntax. It worked, but it introduced a pattern that wasn't part of standard JavaScript.

Svelte 5 eliminates this. Callbacks are just props. If you need to send data back to the parent, you call the callback with that data. That's it. No special API, no runtime magic—just JavaScript.

Consider a date picker component. In Svelte 4, you might have done:

typescript
// Svelte 4 approach (old)
<script>
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  function selectDate(date) {
    dispatch('select', { date });
  }
</script>

<button on:click={() => selectDate(new Date())}>
  Today
</button>

In Svelte 5:

typescript
// Svelte 5 approach (new)
<script>
  let { onSelect } = $props();

  function selectDate(date) {
    onSelect(date);
  }
</script>

<button onclick={() => selectDate(new Date())}>
  Today
</button>

The second version is shorter, clearer, and requires no imports. It also makes the parent's responsibility explicit: if you use the DatePicker, you must provide an onSelect callback or TypeScript will flag it.

This change also improves testability. When testing a component, you can pass a mock function directly as a prop, just like you would in React. No need to listen for dispatched events in your test harness.

Common mistakes

Forgetting to destructure callbacks from props. New developers sometimes write onclick={onhandleClick} when they haven't destructured onhandleClick from the props object yet. Remember to use let { onhandleClick } = $props() at the top of your script block.

Not handling undefined callbacks. If a parent doesn't pass a callback prop, it will be undefined. Always use optional chaining or a guard: onRemove?.(id) or if (onRemove) onRemove(id).

Passing callbacks without binding context. If your callback is a method, remember to bind it or use an arrow function. onclick={this.handleClick} will lose its context; use onclick={() => this.handleClick()} or arrow function properties instead.

Over-wrapping simple callbacks. It's tempting to create a wrapper function for every event, but if you're not doing anything special (no modifiers, no extra logic), just pass the callback directly. Keep it simple.

Key takeaways

  • Svelte 5 replaces on: directives with callback props like onclick, making events regular data.
  • Child components receive callbacks as props and invoke them; no need for createEventDispatcher.
  • Event modifiers become explicit wrapper functions, which are more verbose but clearer and more testable.
  • Callbacks flowing up through props is the same pattern for all event communication—simpler mental model than separate event APIs.
  • Always handle undefined callbacks with optional chaining to avoid runtime errors.
  • Utility functions for common modifiers (stopPropagation, preventDefault) reduce repetition across your codebase.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles