use: Actions
Actions are reusable functions that run when an element is added to and removed from the DOM. They’re Svelte’s clean answer to imperative DOM work — click-outside, autosizing textareas, tooltips, observers — without dragging in a directive framework.
use:action, params, update, destroy
EXAMPLE
<script>
// 1) Signature — use:action on an element
// (node, parameters?) => { update?(newParams), destroy?() }
// 2) Click outside — a classic
function clickOutside(node, callback) {
function onClick(e) {
if (!node.contains(e.target)) callback();
}
document.addEventListener('click', onClick, true);
return {
destroy() { document.removeEventListener('click', onClick, true); },
};
}
</script>
<script>
let open = false;
function close() { open = false; }
</script>
{#if open}
<div use:clickOutside={close} class="menu">
<button on:click={() => alert('item')}>Item</button>
</div>
{/if}
<button on:click={() => (open = !open)}>Toggle</button>
<!-- 3) Update — reacts to parameter changes -->
<script>
function tooltip(node, text) {
let el;
function show() {
el = document.createElement('div');
el.className = 'tooltip';
el.textContent = text;
document.body.appendChild(el);
const r = node.getBoundingClientRect();
el.style.top = `${r.bottom + 8}px`;
el.style.left = `${r.left}px`;
}
function hide() {
el?.remove();
el = null;
}
node.addEventListener('mouseenter', show);
node.addEventListener('mouseleave', hide);
return {
update(newText) { text = newText; if (el) el.textContent = newText; },
destroy() {
node.removeEventListener('mouseenter', show);
node.removeEventListener('mouseleave', hide);
hide();
},
};
}
</script>
<button use:tooltip={'Save your changes'}>Save</button>
<!-- 4) Autosize textarea -->
<script>
function autosize(node) {
function resize() {
node.style.height = 'auto';
node.style.height = `${node.scrollHeight}px`;
}
node.addEventListener('input', resize);
resize();
return { destroy() { node.removeEventListener('input', resize); } };
}
</script>
<textarea use:autosize placeholder="Type…"></textarea>
<!-- 5) IntersectionObserver — lazy-load images -->
<script>
function lazySrc(node, src) {
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
node.src = src;
io.disconnect();
}
}, { rootMargin: '200px 0px' });
io.observe(node);
return { destroy() { io.disconnect(); } };
}
</script>
<img use:lazySrc={`/img/heavy.jpg`} alt="" data-placeholder="…" />
<!-- 6) Focus trap inside a modal -->
<script>
function trapFocus(node) {
const focusable = node.querySelectorAll('a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])');
const first = focusable[0];
const last = focusable[focusable.length - 1];
function onKey(e) {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
last.focus(); e.preventDefault();
} else if (!e.shiftKey && document.activeElement === last) {
first.focus(); e.preventDefault();
}
}
first?.focus();
node.addEventListener('keydown', onKey);
return { destroy() { node.removeEventListener('keydown', onKey); } };
}
</script>
<div role="dialog" use:trapFocus class="modal">
<button on:click={close}>Close</button>
<input />
<button>Save</button>
</div>
<!-- 7) Long press -->
<script>
function longpress(node, threshold = 500) {
let timer;
function start() { timer = setTimeout(() => node.dispatchEvent(new CustomEvent('longpress')), threshold); }
function cancel() { clearTimeout(timer); }
node.addEventListener('mousedown', start);
node.addEventListener('mouseup', cancel);
node.addEventListener('mouseleave', cancel);
node.addEventListener('touchstart', start);
node.addEventListener('touchend', cancel);
return {
update(newThreshold) { threshold = newThreshold; },
destroy() {
node.removeEventListener('mousedown', start);
node.removeEventListener('mouseup', cancel);
node.removeEventListener('mouseleave', cancel);
node.removeEventListener('touchstart', start);
node.removeEventListener('touchend', cancel);
},
};
}
</script>
<button use:longpress={750} on:longpress={() => alert('held!')}>Hold me</button>
<!-- 8) Composing actions — declare multiple on one element -->
<button
use:tooltip={'Delete project'}
use:longpress={1000}
on:longpress={confirmDelete}
class="danger"
>
Delete
</button>
<!-- 9) TypeScript support — type the parameters + emitted events -->
<script lang="ts">
import type { Action } from 'svelte/action';
const clickOutside: Action<HTMLElement, () => void> = (node, cb) => {
const fn = (e: MouseEvent) => { if (!node.contains(e.target as Node)) cb?.(); };
document.addEventListener('click', fn, true);
return {
destroy() { document.removeEventListener('click', fn, true); },
};
};
</script>
<!-- 10) SvelteKit / SSR caveat — actions only run in the browser -->
<!-- Wrap browser-only APIs (window, document, IntersectionObserver) inside the action's body -->
<!-- — Svelte only invokes actions client-side. -->
<!-- 11) Cleanup is mandatory -->
<!-- If you add event listeners, intervals, observers, or DOM nodes —
remove / disconnect / clearInterval / remove() in destroy().
Otherwise: memory leaks every time the consumer hides/shows the element. -->
<!-- 12) Action vs component -->
<!-- Use an action when: Use a component when: -->
<!-- • Behaviour attaches to an existing element • You're rendering new DOM -->
<!-- • You want to compose with other actions on one node • The thing has its own template / state -->
<!-- • The DOM API is the cleanest expression • The thing is reusable as a complete UI piece -->
<!-- 13) Common bugs -->
<!-- • Forgot to call destroy() → memory leaks; especially observers and listeners -->
<!-- • Action receives a param object but doesn't implement update() — stale closure -->
<!-- • Manipulating focus during a transition — call after onMount or in a tick().
Actions run AFTER element mount but BEFORE transitions complete -->
<!-- • Forgot capture-phase on click listeners → child stopPropagation defeats clickOutside -->
<!-- • Tried to use an action on a Svelte component — actions only work on DOM elements, not components -->
Why it matters
Actions are tiny, composable, framework-native — perfect for cross-cutting DOM concerns like click-outside, focus trapping, autosize, and IntersectionObserver. Always implement destroy() to undo what you set up, accept parameters with an update() handler, and use CustomEvent to dispatch back so callers wire them with the standard on: syntax.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
<script>
function autofocus(node) { node.focus(); }
</script>
<input use:autofocus>
Try it Yourself »
Discussion
Loading…