Actions are functions that are called when an element is created. They provide a way to add custom behavior to DOM elements, such as tooltips, click-outside detection, or custom event handlers.
Action Interface
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>;
}
Element
HTMLElement
default:"HTMLElement"
The type of element this action works on (e.g., HTMLDivElement, HTMLButtonElement)
The type of parameter the action accepts. Use undefined if the action takes no parameters
Attributes
Record<string, any>
default:"Record<never, any>"
Additional attributes and events the action enables on the element (TypeScript only)
ActionReturn Interface
interface ActionReturn<
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>
> {
update?: (parameter: Parameter) => void;
destroy?: () => void;
}
update
(parameter: Parameter) => void
Called whenever the action’s parameter changes, immediately after Svelte applies markup updates
Called when the element is unmounted from the DOM
Usage
Basic Action
Create a simple action that runs when an element is added to the DOM:
<script>
function greet(node) {
console.log(`Hello from ${node.tagName}`);
}
</script>
<div use:greet>
I will greet you in the console
</div>
Action with Parameters
Pass parameters to customize the action’s behavior:
<script>
function tooltip(node, text) {
const tooltip = document.createElement('div');
tooltip.textContent = text;
tooltip.className = 'tooltip';
function showTooltip() {
document.body.appendChild(tooltip);
const rect = node.getBoundingClientRect();
tooltip.style.left = rect.left + 'px';
tooltip.style.top = rect.bottom + 'px';
}
function hideTooltip() {
tooltip.remove();
}
node.addEventListener('mouseenter', showTooltip);
node.addEventListener('mouseleave', hideTooltip);
return {
destroy() {
node.removeEventListener('mouseenter', showTooltip);
node.removeEventListener('mouseleave', hideTooltip);
tooltip.remove();
}
};
}
</script>
<button use:tooltip="Click me!">
Hover for tooltip
</button>
Action with Update
Handle parameter changes with the update method:
<script>
function tooltip(node, text) {
let tooltipElement;
function createTooltip() {
tooltipElement = document.createElement('div');
tooltipElement.textContent = text;
tooltipElement.className = 'tooltip';
}
createTooltip();
return {
update(newText) {
// Called when the parameter changes
text = newText;
if (tooltipElement) {
tooltipElement.textContent = newText;
}
},
destroy() {
tooltipElement?.remove();
}
};
}
let tooltipText = $state('Initial text');
</script>
<button use:tooltip={tooltipText}>
Hover me
</button>
<input bind:value={tooltipText} />
TypeScript Example
Type your actions for better IDE support:
import type { Action } from 'svelte/action';
// Action that only works on div elements
export const myAction: Action<HTMLDivElement, { someProperty: boolean } | undefined> = (
node,
param = { someProperty: true }
) => {
// Implementation
console.log(node, param);
return {
update(newParam) {
console.log('Updated:', newParam);
},
destroy() {
console.log('Cleaned up');
}
};
};
Adding Type-Safe Attributes
Define custom attributes and events that the action enables:
import type { Action } from 'svelte/action';
interface Attributes {
'data-custom'?: string;
'on:customEvent'?: (e: CustomEvent<boolean>) => void;
}
export const myAction: Action<HTMLElement, string, Attributes> = (node, param) => {
// This enables TypeScript to recognize these attributes
// when the action is used on an element
node.setAttribute('data-custom', param);
node.dispatchEvent(new CustomEvent('customEvent', { detail: true }));
return {
update(newParam) {
node.setAttribute('data-custom', newParam);
}
};
};
Usage:
<script>
import { myAction } from './actions';
</script>
<!-- TypeScript recognizes these attributes -->
<div
use:myAction="value"
data-custom="also works"
on:customEvent={(e) => console.log(e.detail)}
>
Content
</div>
Common Patterns
Click Outside
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);
}
};
};
Focus Trap
import type { Action } from 'svelte/action';
export const focusTrap: Action<HTMLElement> = (node) => {
const focusableElements = node.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
node.addEventListener('keydown', handleKeydown);
return {
destroy() {
node.removeEventListener('keydown', handleKeydown);
}
};
};
Notes
- Actions are called when the element is created
- The
destroy method is called when the element is removed from the DOM
- The
update method is called whenever the parameter changes
- Multiple actions can be used on the same element
- Actions run after the element is added to the DOM
- Actions are useful for integrating third-party libraries
- The
Attributes generic is TypeScript-only and has no runtime effect
See Also