Svelte actions allow you to reuse DOM interaction logic across components, similar to directives in other frameworks.
What Are Actions?
Actions are functions that are called when an element is mounted and can return cleanup logic when unmounted.
Basic Action
svelte
<script>
function hello(node) {
console.log('Element mounted:', node);
return {
destroy() {
console.log('Element destroyed');
}
};
}
</script>
<div use:hello>
Hello Action
</div>Action Parameters
svelte
<script>
function log(node, message) {
console.log(message, node);
}
function tooltip(node, text) {
const span = document.createElement('span');
span.textContent = text;
span.className = 'tooltip';
node.appendChild(span);
return {
destroy() {
node.removeChild(span);
}
};
}
</script>
<button use:log={'Button created'}>
Click me
</button>
<div use:tooltip={'Hover for info'}>
Hover me
</div>Click Outside Action
javascript
// actions/clickOutside.js
export function clickOutside(node, callback) {
const handleClick = (event) => {
if (node && !node.contains(event.target) && !event.defaultPrevented) {
callback();
}
};
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
};
}svelte
<script>
import { clickOutside } from './actions/clickOutside';
let open = false;
function close() {
open = false;
}
</script>
{#if open}
<div use:clickOutside={close} class="modal">
<p>Click outside to close</p>
</div>
{/if}
<button on:click={() => open = true}>Open</button>Lazy Loading Action
javascript
// actions/lazyLoad.js
export function lazyLoad(node, src) {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
node.src = src;
observer.unobserve(node);
}
});
});
observer.observe(node);
return {
destroy() {
observer.disconnect();
}
};
}Focus Trap Action
javascript
// actions/focusTrap.js
export function focusTrap(node) {
const focusableElements = node.querySelectorAll(
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
function handleKeyDown(e) {
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);
firstElement.focus();
return {
destroy() {
node.removeEventListener('keydown', handleKeyDown);
}
};
}Long Press Action
javascript
// actions/longPress.js
export function longPress(node, callback, duration = 500) {
let timer;
const start = () => {
timer = setTimeout(callback, duration);
};
const clear = () => {
clearTimeout(timer);
};
node.addEventListener('mousedown', start);
node.addEventListener('touchstart', start);
node.addEventListener('mouseup', clear);
node.addEventListener('mouseleave', clear);
node.addEventListener('touchend', clear);
node.addEventListener('touchcancel', clear);
node.addEventListener('touchmove', clear);
return {
destroy() {
node.removeEventListener('mousedown', start);
node.removeEventListener('touchstart', start);
node.removeEventListener('mouseup', clear);
node.removeEventListener('mouseleave', clear);
node.removeEventListener('touchend', clear);
node.removeEventListener('touchcancel', clear);
node.removeEventListener('touchmove', clear);
}
};
}Auto Resize Action
javascript
// actions/autoResize.js
export function autoResize(node) {
const resize = () => {
node.style.height = 'auto';
node.style.height = node.scrollHeight + 'px';
};
node.addEventListener('input', resize);
resize();
return {
destroy() {
node.removeEventListener('input', resize);
}
};
}Copy to Clipboard Action
javascript
// actions/copy.js
export function copy(node, text) {
async function handleCopy() {
try {
await navigator.clipboard.writeText(text);
node.dispatchEvent(new CustomEvent('copy', { detail: text }));
} catch (err) {
console.error('Failed to copy:', err);
}
}
node.addEventListener('click', handleCopy);
return {
destroy() {
node.removeEventListener('click', handleCopy);
}
};
}svelte
<script>
import { copy } from './actions/copy';
function handleCopy(e) {
alert(`Copied: ${e.detail}`);
}
</script>
<button use:copy={'Hello World'} on:copy={handleCopy}>
Copy Text
</button>Infinite Scroll Action
javascript
// actions/infiniteScroll.js
export function infiniteScroll(node, callback) {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
callback();
}
}, { threshold: 0.1 });
observer.observe(node);
return {
destroy() {
observer.disconnect();
}
};
}Action Composition
javascript
// actions/compose.js
export function compose(...actions) {
return (node) => {
const cleanups = actions.map(action => action(node));
return {
destroy() {
cleanups.forEach(cleanup => cleanup?.destroy());
}
};
};
}Svelte actions provide a powerful way to encapsulate and reuse DOM interaction logic.