Comprehensive documentation for event handling, actions, and attachments in Svelte 5.
svelte/eventsThe svelte/events module provides event handling utilities that work seamlessly with Svelte's event delegation system.
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.
function on<Type extends keyof WindowEventMap>(
window: Window,
type: Type,
handler: (this: Window, event: WindowEventMap[Type] & { currentTarget: Window }) => any,
options?: AddEventListenerOptions | undefined
): () => voidAttaches an event handler to the window and returns a function that removes the handler.
Parameters:
window: The window objecttype: The event type (e.g., 'resize', 'scroll', 'load')handler: Event handler function with proper this context and typed eventoptions: Optional event listener options (capture, passive, once, signal)Returns:
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>function on<Type extends keyof DocumentEventMap>(
document: Document,
type: Type,
handler: (this: Document, event: DocumentEventMap[Type] & { currentTarget: Document }) => any,
options?: AddEventListenerOptions | undefined
): () => voidAttaches an event handler to the document and returns a function that removes the handler.
Parameters:
document: The document objecttype: The event type (e.g., 'click', 'keydown', 'DOMContentLoaded')handler: Event handler function with proper this context and typed eventoptions: Optional event listener optionsReturns:
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>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
): () => voidAttaches an event handler to an HTML element and returns a function that removes the handler.
Parameters:
element: The HTML element to attach the listener totype: The event type (e.g., 'click', 'mouseover', 'input')handler: Event handler function with proper this context and typed eventoptions: Optional event listener optionsReturns:
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>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
): () => voidAttaches an event handler to a MediaQueryList and returns a function that removes the handler.
Parameters:
element: The MediaQueryList objecttype: The event type (typically 'change')handler: Event handler function with proper this context and typed eventoptions: Optional event listener optionsReturns:
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>function on(
element: EventTarget,
type: string,
handler: EventListener,
options?: AddEventListenerOptions | undefined
): () => voidAttaches 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 objecttype: The event type as a stringhandler: Event handler functionoptions: Optional event listener optionsReturns:
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>The on() function integrates with Svelte's event delegation system, which provides several benefits:
on() execute in the proper order relative to declaratively-added handlersExample: 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>svelte/actionThe svelte/action module provides TypeScript types for creating reusable element behaviors called actions.
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.
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 toparameter: Optional parameter to configure the actionReturns:
void or an ActionReturn object with update and destroy methodsExample: 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}The return type for actions that need lifecycle methods or want to specify additional attributes.
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 methodAttributes: Additional attributes and events the action enables (type-checking only)Properties:
update?: (parameter: Parameter) => void
destroy?: () => void
$$_attributes?: Attributes
Attributes generic parameter instead of this propertyExample: 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>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" />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>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>svelte/attachmentsThe 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.
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.
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 toReturns:
void or a cleanup function that runs when the element is removedAttachments can be used in two ways:
{@attach ...} tag syntaxcreateAttachmentKey()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);
};
};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 createAttachmentKey(): symbolReturns:
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>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.
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 withT: The parameter type the action acceptsParameters:
action: The action function to convertfn: A function that returns the parameter value for the actionReturns:
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>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 withParameters:
action: The action function to convertReturns:
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>Actions (using use: directive):
Attachments (using {@attach} or spreading):
When to use Actions:
update lifecycle for reactive parametersWhen to use Attachments:
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><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>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 />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>on() in lifecycle hooksoptions: { passive: true } for scroll/touch eventsAction interface for better DXfromAction() to gradually migrate from actionsBefore:
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);
});
});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>svelte/attachments module with createAttachmentKey()fromAction() to convert actions to attachmentssvelte/events module with on() function