Svelte and SvelteKit course · Module 10: Actions

Mjolnir - Actions

2 min read ·
In this lesson4

Welcome to the forge of Mjolnir! Thor's hammer has extraordinary powers. Actions in Svelte are just like those powers - they extend the capabilities of DOM elements.

What are Actions?

Actions are functions called when an element is mounted in the DOM. In Svelte 5 you put setup and cleanup in an $effect inside the action - the function returned from $effect runs when the element goes away:

<script>
  function myAction(node) {
    // node is the DOM element - it is already in the document
    console.log('Element mounted:', node);

    $effect(() => {
      // setup goes here: listeners, libraries

      return () => {
        // and cleanup here, when the element goes away
        console.log('Element unmounted');
      };
    });
  }
</script>

<div use:myAction>
  Element with action
</div>

Actions do not run during server-side rendering (SSR) - they only start in the browser.

The classic API: an object with destroy

Before $effect existed, an action returned an object with a destroy() method (and update(), covered in the next lesson). This API still works and is handy in plain .js files - runes such as $effect are only available in .svelte and .svelte.js files.

// actions.js - a plain JS file, no runes
export function myAction(node) {
  console.log('Element mounted:', node);

  return {
    destroy() {
      console.log('Element unmounted');
    }
  };
}

Example: Click Outside

// actions.js
export function clickOutside(node, callback) {
  function handleClick(event) {
    if (!node.contains(event.target)) {
      callback();
    }
  }

  document.addEventListener('click', handleClick, true);

  return {
    destroy() {
      document.removeEventListener('click', handleClick, true);
    }
  };
}
<script>
  import { clickOutside } from './actions.js';

  let isOpen = $state(false);
</script>

{#if isOpen}
  <div use:clickOutside={() => isOpen = false} class="dropdown">
    Click outside me to close
  </div>
{/if}

Actions with parameters

<script>
  function tooltip(node, text) {
    let tooltipEl;

    function show() {
      tooltipEl = document.createElement('div');
      tooltipEl.className = 'tooltip';
      tooltipEl.textContent = text;
      document.body.appendChild(tooltipEl);

      const rect = node.getBoundingClientRect();
      tooltipEl.style.left = rect.left + 'px';
      tooltipEl.style.top = rect.bottom + 5 + 'px';
    }

    function hide() {
      tooltipEl?.remove();
    }

    $effect(() => {
      node.addEventListener('mouseenter', show);
      node.addEventListener('mouseleave', hide);

      return () => {
        hide();
        node.removeEventListener('mouseenter', show);
        node.removeEventListener('mouseleave', hide);
      };
    });
  }
</script>

<button use:tooltip={'This is a button'}>
  Hover over me
</button>

Mjolnir grants power - Actions grant power to elements!

Code for this lesson: App.svelte
<script>
  // Mjölnir - Powers assigned to elements
  // use:action - additional DOM behaviors

  // Simple action - focus on mount
  function autofocus(node) {
    node.focus();

    // Cleanup in Svelte 5: the function returned from $effect
    $effect(() => {
      return () => console.log('Element removed');
    });
  }

  // Action with parameters
  function tooltip(node, text) {
    let tooltipEl;

    function show() {
      tooltipEl = document.createElement('div');
      tooltipEl.textContent = text;
      tooltipEl.className = 'tooltip';
      document.body.appendChild(tooltipEl);

      const rect = node.getBoundingClientRect();
      tooltipEl.style.left = rect.left + rect.width / 2 + 'px';
      tooltipEl.style.top = rect.top - 10 + 'px';
    }

    function hide() {
      if (tooltipEl) {
        tooltipEl.remove();
        tooltipEl = null;
      }
    }

    node.addEventListener('mouseenter', show);
    node.addEventListener('mouseleave', hide);

    // Classic action API (still works): update and destroy
    return {
      // update - when the parameter changes
      update(newText) {
        text = newText;
      },
      // cleanup
      destroy() {
        hide();
        node.removeEventListener('mouseenter', show);
        node.removeEventListener('mouseleave', hide);
      }
    };
  }

  let tooltipText = $state('I am a tooltip!');
</script>

<main class="mjolnir">
  <h1>Mjölnir - Actions</h1>
  <p class="subtitle">use: - add powers to elements</p>

  <section>
    <h2>use:autofocus</h2>
    <input use:autofocus placeholder="Auto-focus on start!" />
  </section>

  <section>
    <h2>use:tooltip</h2>
    <input
      bind:value={tooltipText}
      placeholder="Change tooltip text"
    />
    <button use:tooltip={tooltipText}>
      Hover over me!
    </button>
  </section>
</main>

<style>
  .mjolnir {
    font-family: 'Georgia', serif;
    max-width: 500px;
    margin: 0 auto;
    padding: 2rem;
    background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
    color: #e8e8f7;
    min-height: 100vh;
  }

  h1 {
    color: #7cb3f0;
    text-align: center;
  }

  .subtitle {
    text-align: center;
    color: #6a8ab9;
  }

  section {
    background: rgba(124, 179, 240, 0.1);
    border: 1px solid #7cb3f0;
    border-radius: 8px;
    padding: 1rem;
    margin: 1rem 0;
  }

  h2 {
    color: #7cb3f0;
    font-size: 1rem;
    margin-top: 0;
  }

  input {
    width: 100%;
    padding: 0.75rem;
    background: rgba(0,0,0,0.3);
    border: 1px solid #3a5f8a;
    border-radius: 4px;
    color: #e8e8f7;
    margin-bottom: 0.5rem;
  }

  input:focus {
    border-color: #7cb3f0;
    outline: none;
  }

  button {
    padding: 0.75rem 1.5rem;
    background: #3a5f8a;
    color: #e8e8f7;
    border: 1px solid #7cb3f0;
    border-radius: 4px;
    cursor: pointer;
  }

  :global(.tooltip) {
    position: fixed;
    background: #ffd700;
    color: #1a1a2e;
    padding: 0.5rem 1rem;
    border-radius: 4px;
    font-size: 0.9rem;
    transform: translate(-50%, -100%);
    z-index: 1000;
    pointer-events: none;
  }
</style>

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. How do we use an action on a DOM element?

  2. 2. What can an action function return?

Useful articles