Skip to content
All essays
WebMarch 27, 202511 min

Svelte Actions: Reusable DOM Interactions

Master Svelte actions for reusable DOM behavior. Create click outside, lazy loading, and custom actions.

Ü
Ümit Uz
Mobile & Full Stack Developer

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.

Related essays

Next essay
Building Forms in Svelte