Skip to main content
Listen to DOM events by adding attributes that start with on to elements. Svelte makes event handling simple and performant with automatic delegation for common events.

Basic Event Handlers

Add event handlers using on followed by the event name:
<button onclick={() => console.log('clicked')}>click me</button>

Event Attribute Syntax

Event attributes are case sensitive:
  • onclick listens to the click event
  • onClick listens to the Click event (different event)
This ensures you can listen to custom events with uppercase characters.

Common Event Handlers

Mouse Events

<div
  onclick={handleClick}
  ondblclick={handleDoubleClick}
  onmouseenter={handleMouseEnter}
  onmouseleave={handleMouseLeave}
  onmousemove={handleMouseMove}
>
  Interactive area
</div>

Keyboard Events

<input
  onkeydown={handleKeyDown}
  onkeyup={handleKeyUp}
  onkeypress={handleKeyPress}
  placeholder="Type here"
/>

Form Events

<form onsubmit={handleSubmit}>
  <input 
    oninput={handleInput}
    onchange={handleChange}
    onfocus={handleFocus}
    onblur={handleBlur}
  />
</form>

Touch Events

<div
  ontouchstart={handleTouchStart}
  ontouchmove={handleTouchMove}
  ontouchend={handleTouchEnd}
>
  Touch area
</div>

Event Object

Handlers receive the event object as the first parameter:
<script>
  function handleClick(event) {
    console.log('Button clicked at:', event.clientX, event.clientY);
    event.preventDefault();
  }
  
  function handleInput(event) {
    console.log('Current value:', event.target.value);
  }
</script>

<button onclick={handleClick}>Click me</button>
<input oninput={handleInput} />

Inline Handlers

Write inline arrow functions for simple handlers:
<script>
  let count = $state(0);
</script>

<button onclick={() => count++}>
  Clicked {count} times
</button>

Handler Shorthand

When the handler variable matches the event name, use shorthand:
<script>
  function onclick() {
    console.log('clicked');
  }
</script>

<!-- These are equivalent -->
<button {onclick}>click me</button>
<button onclick={onclick}>click me</button>

Spreading Event Handlers

Event handlers can be spread like other attributes:
<script>
  const handlers = {
    onclick: () => console.log('clicked'),
    onmouseenter: () => console.log('mouse entered')
  };
</script>

<button {...handlers}>Interactive button</button>

Event Timing

Event attributes fire after bindings update:
<script>
  let value = $state('');
  
  function handleInput(e) {
    // value is already updated from bind:value
    console.log('Input value:', value);
  }
</script>

<input bind:value oninput={handleInput} />

Event Delegation

Svelte uses event delegation for better performance. A single listener at the application root handles these events:
  • beforeinput
  • click
  • change
  • dblclick
  • contextmenu
  • focusin
  • focusout
  • input
  • keydown
  • keyup
  • mousedown
  • mousemove
  • mouseout
  • mouseover
  • mouseup
  • pointerdown
  • pointermove
  • pointerout
  • pointerover
  • pointerup
  • touchend
  • touchmove
  • touchstart

Delegation Gotchas

When using delegated events:
  • Set { bubbles: true } when manually dispatching events
  • Avoid stopPropagation() or events won’t reach the root
  • Handlers added with addEventListener inside the root run before declarative handlers

Passive Touch Events

ontouchstart and ontouchmove handlers are passive for better scrolling performance:
<!-- These are passive by default -->
<div 
  ontouchstart={handleTouchStart}
  ontouchmove={handleTouchMove}
>
  Swipe here
</div>
To call preventDefault(), use the on function from svelte/events:
<script>
  import { on } from 'svelte/events';
  
  function setup(element) {
    on(element, 'touchstart', (event) => {
      event.preventDefault(); // This works
    });
  }
</script>

<div use:setup>Touch area</div>

Real-World Examples

Form Validation

<script>
  let email = $state('');
  let errors = $state([]);
  
  function validateEmail() {
    errors = [];
    if (!email) {
      errors.push('Email is required');
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      errors.push('Invalid email format');
    }
  }
  
  function handleSubmit(event) {
    event.preventDefault();
    validateEmail();
    
    if (errors.length === 0) {
      console.log('Form submitted:', email);
    }
  }
</script>

<form onsubmit={handleSubmit}>
  <input 
    bind:value={email}
    onblur={validateEmail}
    placeholder="Enter email"
  />
  
  {#if errors.length > 0}
    <ul class="errors">
      {#each errors as error}
        <li>{error}</li>
      {/each}
    </ul>
  {/if}
  
  <button type="submit">Submit</button>
</form>

Keyboard Shortcuts

<script>
  let message = $state('');
  
  function handleKeydown(event) {
    // Ctrl/Cmd + S to save
    if ((event.ctrlKey || event.metaKey) && event.key === 's') {
      event.preventDefault();
      save();
    }
    
    // Escape to clear
    if (event.key === 'Escape') {
      message = '';
    }
  }
  
  function save() {
    console.log('Saving:', message);
  }
</script>

<textarea 
  bind:value={message}
  onkeydown={handleKeydown}
  placeholder="Type here (Ctrl+S to save, Esc to clear)"
></textarea>

Click Outside

<script>
  let isOpen = $state(false);
  let dropdown;
  
  function handleClickOutside(event) {
    if (dropdown && !dropdown.contains(event.target)) {
      isOpen = false;
    }
  }
  
  $effect(() => {
    if (isOpen) {
      document.addEventListener('click', handleClickOutside);
      return () => {
        document.removeEventListener('click', handleClickOutside);
      };
    }
  });
</script>

<div bind:this={dropdown}>
  <button onclick={() => isOpen = !isOpen}>
    Toggle menu
  </button>
  
  {#if isOpen}
    <ul class="menu">
      <li>Option 1</li>
      <li>Option 2</li>
      <li>Option 3</li>
    </ul>
  {/if}
</div>

Drag and Drop

<script>
  let isDragging = $state(false);
  let draggedItem = $state(null);
  
  function handleDragStart(event, item) {
    isDragging = true;
    draggedItem = item;
    event.dataTransfer.effectAllowed = 'move';
  }
  
  function handleDragOver(event) {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'move';
  }
  
  function handleDrop(event, targetItem) {
    event.preventDefault();
    isDragging = false;
    
    if (draggedItem !== targetItem) {
      console.log(`Dropped ${draggedItem.name} on ${targetItem.name}`);
    }
  }
</script>

{#each items as item (item.id)}
  <div
    draggable="true"
    ondragstart={(e) => handleDragStart(e, item)}
    ondragover={handleDragOver}
    ondrop={(e) => handleDrop(e, item)}
    class:dragging={isDragging && draggedItem === item}
  >
    {item.name}
  </div>
{/each}
<script>
  let searchTerm = $state('');
  let results = $state([]);
  let debounceTimer;
  
  function handleInput(event) {
    searchTerm = event.target.value;
    
    // Debounce search
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
      performSearch(searchTerm);
    }, 300);
  }
  
  async function performSearch(term) {
    if (!term) {
      results = [];
      return;
    }
    
    const res = await fetch(`/api/search?q=${term}`);
    results = await res.json();
  }
</script>

<input 
  type="search"
  oninput={handleInput}
  placeholder="Search..."
/>

{#if results.length > 0}
  <ul>
    {#each results as result}
      <li>{result.title}</li>
    {/each}
  </ul>
{/if}

Best Practices

  1. Prevent default carefully - Only call preventDefault() when necessary
  2. Clean up listeners - Remove event listeners in $effect cleanup functions
  3. Use delegation - Svelte’s delegation optimizes common events automatically
  4. Avoid stopPropagation - Can interfere with event delegation
  5. Debounce expensive operations - Use timeouts for search, resize, scroll handlers