or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

examples

edge-cases.mdreal-world-scenarios.md
index.md
tile.json

events-actions.mddocs/reference/

Svelte Events and Actions

Comprehensive documentation for event handling, actions, and attachments in Svelte 5.

Module: svelte/events

The svelte/events module provides event handling utilities that work seamlessly with Svelte's event delegation system.

on()

Attaches an event handler to a target and returns a cleanup function. Using this function rather than addEventListener will preserve the correct order relative to handlers added declaratively (with attributes like onclick), which use event delegation for performance reasons.

Overload 1: Window Events { .api }

function on<Type extends keyof WindowEventMap>(
  window: Window,
  type: Type,
  handler: (this: Window, event: WindowEventMap[Type] & { currentTarget: Window }) => any,
  options?: AddEventListenerOptions | undefined
): () => void

Attaches an event handler to the window and returns a function that removes the handler.

Parameters:

  • window: The window object
  • type: The event type (e.g., 'resize', 'scroll', 'load')
  • handler: Event handler function with proper this context and typed event
  • options: Optional event listener options (capture, passive, once, signal)

Returns:

  • A cleanup function that removes the event listener

Example:

<script>
  import { on } from 'svelte/events';
  import { onMount } from 'svelte';

  let windowWidth = $state(0);

  onMount(() => {
    const cleanup = on(window, 'resize', (event) => {
      windowWidth = event.currentTarget.innerWidth;
    });

    // Initial value
    windowWidth = window.innerWidth;

    // Cleanup is called automatically when component unmounts
    return cleanup;
  });
</script>

<p>Window width: {windowWidth}px</p>

Overload 2: Document Events { .api }

function on<Type extends keyof DocumentEventMap>(
  document: Document,
  type: Type,
  handler: (this: Document, event: DocumentEventMap[Type] & { currentTarget: Document }) => any,
  options?: AddEventListenerOptions | undefined
): () => void

Attaches an event handler to the document and returns a function that removes the handler.

Parameters:

  • document: The document object
  • type: The event type (e.g., 'click', 'keydown', 'DOMContentLoaded')
  • handler: Event handler function with proper this context and typed event
  • options: Optional event listener options

Returns:

  • A cleanup function that removes the event listener

Example:

<script>
  import { on } from 'svelte/events';
  import { onMount } from 'svelte';

  let clicks = $state(0);

  onMount(() => {
    // Track all clicks on the document
    return on(document, 'click', (event) => {
      clicks++;
      console.log('Clicked on:', event.target);
    });
  });
</script>

<p>Total document clicks: {clicks}</p>

Overload 3: HTMLElement Events { .api }

function on<Element extends HTMLElement, Type extends keyof HTMLElementEventMap>(
  element: Element,
  type: Type,
  handler: (this: Element, event: HTMLElementEventMap[Type] & { currentTarget: Element }) => any,
  options?: AddEventListenerOptions | undefined
): () => void

Attaches an event handler to an HTML element and returns a function that removes the handler.

Parameters:

  • element: The HTML element to attach the listener to
  • type: The event type (e.g., 'click', 'mouseover', 'input')
  • handler: Event handler function with proper this context and typed event
  • options: Optional event listener options

Returns:

  • A cleanup function that removes the event listener

Example:

<script>
  import { on } from 'svelte/events';
  import { onMount } from 'svelte';

  let buttonRef;
  let clickCount = $state(0);

  onMount(() => {
    // Programmatically add event listener
    const cleanup = on(buttonRef, 'click', (event) => {
      clickCount++;
      console.log('Button clicked!', event.currentTarget);
    });

    return cleanup;
  });
</script>

<button bind:this={buttonRef}>
  Clicked {clickCount} times
</button>

Overload 4: MediaQueryList Events { .api }

function on<Element extends MediaQueryList, Type extends keyof MediaQueryListEventMap>(
  element: Element,
  type: Type,
  handler: (this: Element, event: MediaQueryListEventMap[Type] & { currentTarget: Element }) => any,
  options?: AddEventListenerOptions | undefined
): () => void

Attaches an event handler to a MediaQueryList and returns a function that removes the handler.

Parameters:

  • element: The MediaQueryList object
  • type: The event type (typically 'change')
  • handler: Event handler function with proper this context and typed event
  • options: Optional event listener options

Returns:

  • A cleanup function that removes the event listener

Example:

<script>
  import { on } from 'svelte/events';
  import { onMount } from 'svelte';

  let isDarkMode = $state(false);

  onMount(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    isDarkMode = mediaQuery.matches;

    return on(mediaQuery, 'change', (event) => {
      isDarkMode = event.matches;
      console.log('Dark mode preference changed:', event.matches);
    });
  });
</script>

<p>Dark mode: {isDarkMode ? 'enabled' : 'disabled'}</p>

Overload 5: Generic EventTarget { .api }

function on(
  element: EventTarget,
  type: string,
  handler: EventListener,
  options?: AddEventListenerOptions | undefined
): () => void

Attaches an event handler to any EventTarget and returns a function that removes the handler. This is the most generic overload that works with any event target.

Parameters:

  • element: Any EventTarget object
  • type: The event type as a string
  • handler: Event handler function
  • options: Optional event listener options

Returns:

  • A cleanup function that removes the event listener

Example:

<script>
  import { on } from 'svelte/events';
  import { onMount } from 'svelte';

  let wsMessages = $state([]);

  onMount(() => {
    const ws = new WebSocket('wss://example.com/socket');

    const cleanups = [
      on(ws, 'message', (event) => {
        wsMessages = [...wsMessages, event.data];
      }),
      on(ws, 'error', (event) => {
        console.error('WebSocket error:', event);
      }),
      on(ws, 'close', () => {
        console.log('WebSocket closed');
      })
    ];

    return () => cleanups.forEach(cleanup => cleanup());
  });
</script>

Event Delegation Benefits

The on() function integrates with Svelte's event delegation system, which provides several benefits:

  1. Correct Order: Handlers added with on() execute in the proper order relative to declaratively-added handlers
  2. Performance: Takes advantage of event delegation for common events
  3. Cleanup: Returns a cleanup function for easy memory management
  4. Type Safety: Full TypeScript support with proper event types

Example: Event Order Preservation

<script>
  import { on } from 'svelte/events';
  import { onMount } from 'svelte';

  let buttonRef;

  function handleClickDeclarative(event) {
    console.log('1. Declarative handler');
  }

  onMount(() => {
    // This handler respects the delegation order
    return on(buttonRef, 'click', (event) => {
      console.log('2. Programmatic handler (via on)');
    });
  });
</script>

<button bind:this={buttonRef} onclick={handleClickDeclarative}>
  Click to see order
</button>

Module: svelte/action

The svelte/action module provides TypeScript types for creating reusable element behaviors called actions.

Action<Element, Parameter, Attributes>

An interface for typing action functions. Actions are functions that are called when an element is created, allowing you to enhance DOM elements with custom behavior.

Type Definition { .api }

interface Action<
  Element = HTMLElement,
  Parameter = undefined,
  Attributes extends Record<string, any> = Record<never, any>
> {
  <Node extends Element>(
    ...args: undefined extends Parameter
      ? [node: Node, parameter?: Parameter]
      : [node: Node, parameter: Parameter]
  ): void | ActionReturn<Parameter, Attributes>;
}

Type Parameters:

  • Element: The type of DOM element the action works with (default: HTMLElement)
  • Parameter: The type of parameter the action accepts (default: undefined for no parameter)
  • Attributes: Additional attributes and events the action adds to the element (type-checking only)

Parameters:

  • node: The DOM element the action is applied to
  • parameter: Optional parameter to configure the action

Returns:

  • void or an ActionReturn object with update and destroy methods

Example: Basic Action

import type { Action } from 'svelte/action';

// Simple action without parameters
export const autofocus: Action<HTMLElement> = (node) => {
  node.focus();
};
<script>
  import { autofocus } from './actions.js';
</script>

<input use:autofocus />

Example: Action with Parameter

import type { Action } from 'svelte/action';

interface TooltipParams {
  content: string;
  position?: 'top' | 'bottom' | 'left' | 'right';
}

export const tooltip: Action<HTMLElement, TooltipParams> = (node, params) => {
  const { content, position = 'top' } = params;

  let tooltipElement: HTMLDivElement;

  function showTooltip() {
    tooltipElement = document.createElement('div');
    tooltipElement.className = `tooltip tooltip-${position}`;
    tooltipElement.textContent = content;
    document.body.appendChild(tooltipElement);

    const rect = node.getBoundingClientRect();
    // Position tooltip based on element position...
    tooltipElement.style.left = `${rect.left + rect.width / 2}px`;
    tooltipElement.style.top = `${rect.top - tooltipElement.offsetHeight - 5}px`;
  }

  function hideTooltip() {
    if (tooltipElement) {
      tooltipElement.remove();
    }
  }

  node.addEventListener('mouseenter', showTooltip);
  node.addEventListener('mouseleave', hideTooltip);

  return {
    destroy() {
      node.removeEventListener('mouseenter', showTooltip);
      node.removeEventListener('mouseleave', hideTooltip);
      hideTooltip();
    }
  };
};
<script>
  import { tooltip } from './actions.js';
</script>

<button use:tooltip={{ content: 'Click me!', position: 'top' }}>
  Hover for tooltip
</button>

Example: Action with Specific Element Type

import type { Action } from 'svelte/action';

// Only works on div elements
export const resizable: Action<HTMLDivElement, boolean | undefined> = (
  node,
  enabled = true
) => {
  if (!enabled) return;

  node.style.resize = 'both';
  node.style.overflow = 'auto';
};
<div use:resizable={true}>
  Resizable content
</div>

Example: Click Outside Action

import type { Action } from 'svelte/action';

export const clickOutside: Action<HTMLElement, () => void> = (node, callback) => {
  function handleClick(event: MouseEvent) {
    if (!node.contains(event.target as Node)) {
      callback();
    }
  }

  document.addEventListener('click', handleClick, true);

  return {
    destroy() {
      document.removeEventListener('click', handleClick, true);
    }
  };
};
<script>
  import { clickOutside } from './actions.js';

  let isOpen = $state(false);

  function closeDropdown() {
    isOpen = false;
  }
</script>

{#if isOpen}
  <div class="dropdown" use:clickOutside={closeDropdown}>
    <p>Dropdown content</p>
  </div>
{/if}

ActionReturn<Parameter, Attributes>

The return type for actions that need lifecycle methods or want to specify additional attributes.

Interface Definition { .api }

interface ActionReturn<
  Parameter = undefined,
  Attributes extends Record<string, any> = Record<never, any>
> {
  update?: (parameter: Parameter) => void;
  destroy?: () => void;
  /**
   * ### DO NOT USE THIS
   * This exists solely for type-checking and has no effect at runtime.
   * Set this through the `Attributes` generic instead.
   */
  $$_attributes?: Attributes;
}

Type Parameters:

  • Parameter: The type of parameter passed to the update method
  • Attributes: Additional attributes and events the action enables (type-checking only)

Properties:

  • update?: (parameter: Parameter) => void

    • Called when the action's parameter changes
    • Receives the new parameter value
    • Called immediately after Svelte applies updates to the markup
  • destroy?: () => void

    • Called when the element is unmounted
    • Use for cleanup (removing event listeners, canceling timers, etc.)
  • $$_attributes?: Attributes

    • Internal property for TypeScript type-checking only
    • Has no effect at runtime
    • Use the Attributes generic parameter instead of this property

Example: Action with Update and Destroy

import type { Action, ActionReturn } from 'svelte/action';

interface HighlightParams {
  color: string;
  duration: number;
}

export const highlight: Action<HTMLElement, HighlightParams> = (
  node,
  params
): ActionReturn<HighlightParams> => {
  let timeoutId: number;

  function applyHighlight(color: string, duration: number) {
    const originalBg = node.style.backgroundColor;
    node.style.backgroundColor = color;

    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      node.style.backgroundColor = originalBg;
    }, duration);
  }

  applyHighlight(params.color, params.duration);

  return {
    update(newParams) {
      // Called when params change
      applyHighlight(newParams.color, newParams.duration);
    },
    destroy() {
      // Cleanup when element is removed
      clearTimeout(timeoutId);
    }
  };
};
<script>
  import { highlight } from './actions.js';

  let color = $state('yellow');
  let duration = $state(1000);
</script>

<p use:highlight={{ color, duration }}>
  This text will be highlighted
</p>

<input type="color" bind:value={color} />
<input type="number" bind:value={duration} />

Example: Action with Custom Attributes

import type { Action, ActionReturn } from 'svelte/action';

interface DraggableAttributes {
  ondragstart?: (e: CustomEvent<{ x: number; y: number }>) => void;
  ondragmove?: (e: CustomEvent<{ x: number; y: number }>) => void;
  ondragend?: (e: CustomEvent<{ x: number; y: number }>) => void;
  'data-draggable'?: boolean;
}

export const draggable: Action<HTMLElement, void, DraggableAttributes> = (
  node
): ActionReturn<void, DraggableAttributes> => {
  let x = 0;
  let y = 0;

  function handleMouseDown(event: MouseEvent) {
    x = event.clientX;
    y = event.clientY;

    node.dispatchEvent(new CustomEvent('dragstart', {
      detail: { x, y }
    }));

    window.addEventListener('mousemove', handleMouseMove);
    window.addEventListener('mouseup', handleMouseUp);
  }

  function handleMouseMove(event: MouseEvent) {
    const dx = event.clientX - x;
    const dy = event.clientY - y;
    x = event.clientX;
    y = event.clientY;

    node.dispatchEvent(new CustomEvent('dragmove', {
      detail: { x: dx, y: dy }
    }));
  }

  function handleMouseUp(event: MouseEvent) {
    node.dispatchEvent(new CustomEvent('dragend', {
      detail: { x: event.clientX, y: event.clientY }
    }));

    window.removeEventListener('mousemove', handleMouseMove);
    window.removeEventListener('mouseup', handleMouseUp);
  }

  node.addEventListener('mousedown', handleMouseDown);

  return {
    destroy() {
      node.removeEventListener('mousedown', handleMouseDown);
      window.removeEventListener('mousemove', handleMouseMove);
      window.removeEventListener('mouseup', handleMouseUp);
    }
  };
};
<script>
  import { draggable } from './actions.js';

  let position = $state({ x: 0, y: 0 });

  function handleDragMove(event: CustomEvent<{ x: number; y: number }>) {
    position.x += event.detail.x;
    position.y += event.detail.y;
  }
</script>

<div
  use:draggable
  ondragmove={handleDragMove}
  style="transform: translate({position.x}px, {position.y}px)"
>
  Drag me!
</div>

Common Action Patterns

Lazy Loading Images

import type { Action } from 'svelte/action';

export const lazyLoad: Action<HTMLImageElement, string> = (node, src) => {
  const observer = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting) {
      node.src = src;
      observer.disconnect();
    }
  });

  observer.observe(node);

  return {
    destroy() {
      observer.disconnect();
    }
  };
};
<img use:lazyLoad="large-image.jpg" alt="Lazy loaded" />

Clipboard Copy

import type { Action } from 'svelte/action';

export const copyToClipboard: Action<HTMLElement, string> = (node, text) => {
  async function handleClick() {
    try {
      await navigator.clipboard.writeText(text);
      node.dispatchEvent(new CustomEvent('copied', { detail: { text } }));
    } catch (err) {
      console.error('Failed to copy:', err);
    }
  }

  node.addEventListener('click', handleClick);

  return {
    update(newText) {
      text = newText;
    },
    destroy() {
      node.removeEventListener('click', handleClick);
    }
  };
};
<button use:copyToClipboard="Hello, World!" oncopied={() => alert('Copied!')}>
  Copy to clipboard
</button>

Trap Focus

import type { Action } from 'svelte/action';

export const trapFocus: Action<HTMLElement> = (node) => {
  const focusableElements = node.querySelectorAll<HTMLElement>(
    'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])'
  );

  if (focusableElements.length === 0) return;

  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];

  function handleKeyDown(event: KeyboardEvent) {
    if (event.key !== 'Tab') return;

    if (event.shiftKey) {
      if (document.activeElement === firstElement) {
        event.preventDefault();
        lastElement.focus();
      }
    } else {
      if (document.activeElement === lastElement) {
        event.preventDefault();
        firstElement.focus();
      }
    }
  }

  firstElement.focus();
  node.addEventListener('keydown', handleKeyDown);

  return {
    destroy() {
      node.removeEventListener('keydown', handleKeyDown);
    }
  };
};
<div class="modal" use:trapFocus>
  <button>First</button>
  <button>Second</button>
  <button>Last</button>
</div>

Module: svelte/attachments

The svelte/attachments module provides a new API (introduced in Svelte 5.29) for attaching behavior to elements. Attachments are similar to actions but use a different syntax and can be more flexible.

Attachment<T>

An attachment is a function that runs when an element is mounted to the DOM, and optionally returns a function that is called when the element is later removed.

Type Definition { .api }

interface Attachment<T extends EventTarget = Element> {
  (element: T): void | (() => void);
}

Type Parameters:

  • T: The type of event target the attachment applies to (default: Element)

Parameters:

  • element: The DOM element being attached to

Returns:

  • void or a cleanup function that runs when the element is removed

Attachments can be used in two ways:

  1. With the {@attach ...} tag syntax
  2. By spreading an object with a key created by createAttachmentKey()

Example: Basic Attachment

<script>
  function logMount(element) {
    console.log('Element mounted:', element);

    return () => {
      console.log('Element unmounted:', element);
    };
  }
</script>

<div {@attach logMount}>
  Content
</div>

Example: Attachment with EventTarget

import type { Attachment } from 'svelte/attachments';

export const logEvents: Attachment<Window> = (window) => {
  function handleEvent(event: Event) {
    console.log('Window event:', event.type);
  }

  window.addEventListener('resize', handleEvent);
  window.addEventListener('scroll', handleEvent);

  return () => {
    window.removeEventListener('resize', handleEvent);
    window.removeEventListener('scroll', handleEvent);
  };
};

createAttachmentKey()

Creates an object key that will be recognized as an attachment when the object is spread onto an element. This provides a programmatic alternative to using {@attach ...} and is useful for library authors.

Function Signature { .api }

function createAttachmentKey(): symbol

Returns:

  • A unique symbol that can be used as an attachment key

Example: Basic Usage

<script>
  import { createAttachmentKey } from 'svelte/attachments';

  const props = {
    class: 'cool',
    onclick: () => alert('clicked'),
    [createAttachmentKey()]: (node) => {
      node.textContent = 'attached!';
    }
  };
</script>

<button {...props}>click me</button>

Example: Library API

// library.js
import { createAttachmentKey } from 'svelte/attachments';

const attachmentKey = createAttachmentKey();

export function createComponent(config) {
  return {
    ...config.props,
    [attachmentKey]: (node) => {
      // Initialize library behavior
      console.log('Initializing component:', node);

      // Setup
      const cleanup = setupBehavior(node, config);

      // Return cleanup
      return cleanup;
    }
  };
}
<script>
  import { createComponent } from './library.js';

  const componentProps = createComponent({
    props: { class: 'custom' },
    behavior: 'special'
  });
</script>

<div {...componentProps}>
  Enhanced by library
</div>

Example: Multiple Attachments

<script>
  import { createAttachmentKey } from 'svelte/attachments';

  function createMultipleAttachments() {
    return {
      [createAttachmentKey()]: (node) => {
        console.log('First attachment');
      },
      [createAttachmentKey()]: (node) => {
        console.log('Second attachment');
      },
      [createAttachmentKey()]: (node) => {
        console.log('Third attachment');
        return () => console.log('Third cleanup');
      }
    };
  }

  const props = {
    class: 'multi',
    ...createMultipleAttachments()
  };
</script>

<div {...props}>Multiple attachments</div>

fromAction()

Converts an action to an attachment, keeping the same behavior. This is useful when you want to start using attachments but have actions provided by a library.

Overload 1: Action with Parameter { .api }

function fromAction<E extends EventTarget, T extends unknown>(
  action: Action<E, T> | ((element: E, arg: T) => void | ActionReturn<T>),
  fn: () => T
): Attachment<E>

Converts an action that takes a parameter into an attachment. Note that the second argument must be a function that returns the argument, not the argument itself.

Type Parameters:

  • E: The event target type the action works with
  • T: The parameter type the action accepts

Parameters:

  • action: The action function to convert
  • fn: A function that returns the parameter value for the action

Returns:

  • An attachment that wraps the action

Example:

<script>
  import { fromAction } from 'svelte/attachments';
  import { tooltip } from './actions.js'; // Existing action

  let tooltipText = $state('Hello!');
</script>

<!-- Old way with action -->
<button use:tooltip={tooltipText}>Hover (action)</button>

<!-- New way with attachment -->
<button {@attach fromAction(tooltip, () => tooltipText)}>
  Hover (attachment)
</button>

Overload 2: Action without Parameter { .api }

function fromAction<E extends EventTarget>(
  action: Action<E, void> | ((element: E) => void | ActionReturn<void>)
): Attachment<E>

Converts an action that takes no parameter into an attachment.

Type Parameters:

  • E: The event target type the action works with

Parameters:

  • action: The action function to convert

Returns:

  • An attachment that wraps the action

Example:

<script>
  import { fromAction } from 'svelte/attachments';
  import { autofocus } from './actions.js'; // Existing action
</script>

<!-- Old way with action -->
<input use:autofocus />

<!-- New way with attachment -->
<input {@attach fromAction(autofocus)} />

Example: Converting Click Outside Action

// actions.js
import type { Action } from 'svelte/action';

export const clickOutside: Action<HTMLElement, () => void> = (node, callback) => {
  function handleClick(event: MouseEvent) {
    if (!node.contains(event.target as Node)) {
      callback();
    }
  }

  document.addEventListener('click', handleClick, true);

  return {
    destroy() {
      document.removeEventListener('click', handleClick, true);
    }
  };
};
<script>
  import { fromAction } from 'svelte/attachments';
  import { clickOutside } from './actions.js';

  let isOpen = $state(false);

  function close() {
    isOpen = false;
  }
</script>

{#if isOpen}
  <!-- Using as attachment -->
  <div class="modal" {@attach fromAction(clickOutside, () => close)}>
    <p>Click outside to close</p>
  </div>
{/if}

Example: Converting Draggable Action

// draggable-action.js
import type { Action, ActionReturn } from 'svelte/action';

interface DraggableConfig {
  disabled?: boolean;
  onDragStart?: (x: number, y: number) => void;
  onDragEnd?: (x: number, y: number) => void;
}

export const draggable: Action<HTMLElement, DraggableConfig> = (
  node,
  config = {}
): ActionReturn<DraggableConfig> => {
  let { disabled = false, onDragStart, onDragEnd } = config;

  function handleMouseDown(event: MouseEvent) {
    if (disabled) return;

    onDragStart?.(event.clientX, event.clientY);
    // ... dragging logic
  }

  node.addEventListener('mousedown', handleMouseDown);

  return {
    update(newConfig) {
      disabled = newConfig.disabled ?? false;
      onDragStart = newConfig.onDragStart;
      onDragEnd = newConfig.onDragEnd;
    },
    destroy() {
      node.removeEventListener('mousedown', handleMouseDown);
    }
  };
};
<script>
  import { fromAction } from 'svelte/attachments';
  import { draggable } from './draggable-action.js';

  let position = $state({ x: 0, y: 0 });
  let isDragging = $state(false);

  const config = {
    disabled: false,
    onDragStart: (x, y) => {
      isDragging = true;
    },
    onDragEnd: (x, y) => {
      isDragging = false;
      position = { x, y };
    }
  };
</script>

<div
  class:dragging={isDragging}
  {@attach fromAction(draggable, () => config)}
>
  Drag me!
</div>

Attachments vs Actions

Actions (using use: directive):

  • Original Svelte pattern (available since Svelte 3)
  • Good for element-specific enhancements
  • Can accept parameters that update reactively
  • Well-established ecosystem

Attachments (using {@attach} or spreading):

  • New pattern introduced in Svelte 5.29
  • More flexible - can be spread with other props
  • Better for library authors building component APIs
  • Can create multiple attachment points programmatically

When to use Actions:

  • You need the update lifecycle for reactive parameters
  • Using existing action libraries
  • Building traditional Svelte components

When to use Attachments:

  • Building library APIs that need flexible prop spreading
  • Creating programmatic component enhancements
  • Working with highly dynamic component configurations

Example: Side-by-Side Comparison

<script>
  import { fromAction } from 'svelte/attachments';

  // Action version
  function myAction(node, param) {
    console.log('Action:', node, param);
    return {
      update(newParam) {
        console.log('Updated:', newParam);
      },
      destroy() {
        console.log('Destroyed');
      }
    };
  }

  // Attachment version
  function myAttachment(node) {
    console.log('Attachment:', node);
    return () => {
      console.log('Cleanup');
    };
  }

  let value = $state('test');
</script>

<!-- Using action -->
<div use:myAction={value}>Action</div>

<!-- Using attachment directly -->
<div {@attach myAttachment}>Attachment</div>

<!-- Converting action to attachment -->
<div {@attach fromAction(myAction, () => value)}>Converted</div>

Advanced Patterns

Combining Events and Actions

<script>
  import { on } from 'svelte/events';
  import type { Action } from 'svelte/action';

  // Action that uses the `on` function internally
  export const trackClicks: Action<HTMLElement, { target: string }> = (
    node,
    { target }
  ) => {
    const cleanup = on(node, 'click', (event) => {
      console.log(`Click tracked on ${target}:`, event);
      // Send analytics, etc.
    });

    return {
      destroy: cleanup
    };
  };
</script>

<button use:trackClicks={{ target: 'cta-button' }}>
  Track my clicks
</button>

Reusable Action Composition

import type { Action, ActionReturn } from 'svelte/action';

// Higher-order action that composes multiple actions
export function composeActions<T extends HTMLElement>(
  ...actions: Action<T, any>[]
): Action<T> {
  return (node: T): ActionReturn => {
    const returns = actions.map(action => action(node));

    return {
      destroy() {
        returns.forEach(ret => ret?.destroy?.());
      }
    };
  };
}
<script>
  import { composeActions } from './action-utils.js';
  import { autofocus, tooltip, trackClicks } from './actions.js';

  const enhanced = composeActions(autofocus, tooltip, trackClicks);
</script>

<input use:enhanced />

Type-Safe Event Handlers

import { on } from 'svelte/events';

// Utility for creating type-safe event handlers
export function createEventHandler<K extends keyof HTMLElementEventMap>(
  element: HTMLElement,
  eventType: K,
  handler: (event: HTMLElementEventMap[K]) => void
) {
  return on(element, eventType, handler);
}
<script>
  import { createEventHandler } from './utils.js';
  import { onMount } from 'svelte';

  let buttonRef: HTMLButtonElement;

  onMount(() => {
    // Type-safe: handler receives typed MouseEvent
    return createEventHandler(buttonRef, 'click', (event) => {
      console.log('Click at:', event.clientX, event.clientY);
    });
  });
</script>

<button bind:this={buttonRef}>Click me</button>

Best Practices

Event Handling

  1. Always cleanup event listeners: Return the cleanup function from on() in lifecycle hooks
  2. Use proper overloads: Choose the most specific overload for better type safety
  3. Consider passive listeners: Use options: { passive: true } for scroll/touch events
  4. Avoid memory leaks: Store cleanup functions and call them when components unmount

Actions

  1. Keep actions focused: Each action should do one thing well
  2. Always provide destroy method: Clean up event listeners, timers, and observers
  3. Use TypeScript: Type your actions with the Action interface for better DX
  4. Make actions reusable: Accept parameters to customize behavior
  5. Document side effects: Clearly document what your action does and any events it dispatches

Attachments

  1. Use for libraries: Attachments are great for building library APIs
  2. Prefer actions for simple cases: Use actions when you don't need programmatic spreading
  3. Convert when needed: Use fromAction() to gradually migrate from actions
  4. Return cleanup functions: Always clean up resources in the returned function

Migration Guide

From addEventListener to on()

Before:

onMount(() => {
  const handler = (event) => {
    console.log('Resize:', event);
  };

  window.addEventListener('resize', handler);

  return () => {
    window.removeEventListener('resize', handler);
  };
});

After:

import { on } from 'svelte/events';

onMount(() => {
  return on(window, 'resize', (event) => {
    console.log('Resize:', event);
  });
});

From Actions to Attachments

Before (Action):

<script>
  function myAction(node, param) {
    console.log(node, param);
    return { destroy: () => {} };
  }

  let value = $state('test');
</script>

<div use:myAction={value}>Content</div>

After (Attachment):

<script>
  import { fromAction } from 'svelte/attachments';

  function myAction(node, param) {
    console.log(node, param);
    return { destroy: () => {} };
  }

  let value = $state('test');
</script>

<div {@attach fromAction(myAction, () => value)}>Content</div>

Version History

  • Svelte 5.29: Added svelte/attachments module with createAttachmentKey()
  • Svelte 5.32: Added fromAction() to convert actions to attachments
  • Svelte 5.0: Introduced svelte/events module with on() function

See Also

  • Svelte Actions Documentation
  • Svelte Attachments Documentation
  • Event Handling in Svelte