Svelte action (different from SvelteKit form action) is a technique introduced first-party by Svelte that helps implement and package reusable logic and interaction with the DOM:

<element use:action></element>

Svelte action is highly versatile and is one of the features I often mention when talking about Svelte. If you have used one of the @svelte-put/* libraries I wrote, you may have noticed that most of the packages in that collection are built upon Svelte action.

Let’s dive deeper into this feature through some concrete examples.

Examples

No need to go far, this very page you are reading on sveltevietnam.dev has quite a few Svelte actions in action (pun intended):

Example 1: Clickoutside

Perhaps one of the most common applications of Svelte action is handling click event outside an element. If you click on the icon > at the top right corner of this page, or the icon > in the ”Share” section, a corresponding modal will be activated. When you click on the backdrop of such modal, the modal will close. To achieve this, the clickoutside action from the @svelte-put/clickoutside package is utilized:

<div use:clickoutside on:clickoutside={close}>...modal...</div>

Example 2: Portal

Sometimes, we need to render some UI at a specific location in the app, detached from the triggering location, such as the modals mentioned in the previous section, or the following push notification system, built on @svelte-put/noti:

Where the modal or notification needs to be displayed, a portal action is used:

<aside use:portal={notiStore}>...notifications will be rendered here...</aside>

Example 3: Tooltip

At the section below the post title, you will notice a language indicator with a circular “i” icon next to it. Hovering over this icon will trigger a tooltip, providing additional information to the user:

Manual translation >

This tooltip is applied to the icon through the textTip Svelte action, built with @svelte-put/tooltip:

<svg use:textTip={{ content: 'You are reading a manual translation of the blog post.' }}>...icon...</svg>

Tooltips should be used sparingly and with caution as they often do not provide a friendly experience, especially for users who rely on assistive technologies. I also discuss about this in the docs page of @svelte-put/tooltip.

Example 4: Inlining SVG

Sometimes, we need to inline an SVG element directly into the app but do not know which SVG it is until the app is running on the browser. A common example for this situation is an SVG icon that we need to change the color based on the theme of the website. To solve this problem, we can use the inlineSvg action from @svelte-put/inline-svg:

<svg
  use:inlineSvg={"https://raw.githubusercontent.com/sveltejs/branding/master/svelte-logo.svg"}
  class="special svelte"
></svg>
Svelte

To ensure progressive enhancement, sveltevietnam.dev does not use inline-svg but instead uses the sibling @svelte-put/preprocess-inline-svg library, with the same feature but executed at build time, built on Svelte preprocessor.

Example 5: Table of Contents

The Table of Contents of this page also utilizes a toc action, built with @svelte-put/toc:

<main use:toc>
  <section>...Table of content...</section>
  <!-- ... -->
</main>

That is probably enough examples to show you how versatile Svelte action can be. But how can action be so flexible?

Dissecting a Svelte Action

Input and Initialization

A Svelte action is simply a regular Javascript function, with the input being the DOM of the element the action is placed on.

function action(node: HTMLElement) {
  // necessary DOM manipulation
}

As you can see, the power of action lies in its simplicity. Action simply opens a window to interact with the DOM, so almost everything you can do with vanilla Javascript, CSS, and HTML is feasible in Svelte action. Because it does not depend on any special Svelte syntax (e.g. $ syntax), you can easily package action into a file and reuse it where needed.

The function content of action will be executed when the component has been mounted into the DOM (hydration has been completed), aka. at “runtime”.

JS Runtime vs Progressive Enhancement

As discussed, action is only executed at runtime, meaning that during prerendering or server-side-rendering, any code within action will not take effect. For example, if you add a class to node.classList in action, this class will only be added after the webpage has been fully loaded in the browser.

For this reason, to ensure progressive enhancement, you should only use action when you really need to interact with the DOM - which is often the case when the task involves user interaction. For tasks not related to user interaction, you most likely do not need action or even Javascript at runtime; Svelte preprocessor might be a good solution for this case that you should consider. To learn more about Svelte preprocessor, read the article ”Writing a simple Svelte preprocessor“.

Configuration and Update

The second parameter to action is any Javascript primitive or object, which can be used to configure the action as needed:

type ActionParameter = {
  enabled?: boolean;
};
function action(node: HTMLElement) {
function action(node: HTMLElement, param?: ActionParameter) {
  if (param?.enabled) { // ... }
}

As Svelte variables are “reactive”, and in fact param can change at any time, action gives us the ability to update its behavior when param changes through the update method in the output:

type ActionParameter = {
  enabled?: boolean;
};
function action(node: HTMLElement, param?: ActionParameter) {
  return {
    update: (newParam?: ActionParameter) => {
      if (newParam?.enabled) {
        // turn on
      } else {
        // turn off
      }
    },
  };
}

Finally, we can clean up resources (e.g. removeEventListener) in the destroy method of the returned output:

type ActionParameter = {
  enabled?: boolean;
};
function action(node: HTMLElement, param?: ActionParameter) {
  return {
    update: (newParam?: ActionParameter) => {
      if (newParam?.enabled) {
        // turn on
      } else {
        // turn off
      }
    },
    destroy: () => {
      // clean up
    },
  };
}

That is it, Svelte action is that simple! This API should take you only five minutes to get familiar with, and the rest is just standard web APIs that we all (should) know about.

Dispatching CustomEvent

This is not a part of the Svelte action API, but still a common need when writing action. In the following example, through the CustomEvent web API, we will dispatch a custom event when the user clicks on the element:

type ActionParameter = {
  enabled?: boolean;
};
export function action(node: HTMLElement, param?: ActionParameter) {
  function handleClick() {
    const customEvent = new CustomEvent('hello', { detail: 'hello' });
    node.dispatch(customEvent);
  }
  if (param?.enabled) {
    node.addEventListener('click', handleClick);
  }
  return {
    update: (newParam?: ActionParameter) => {
      // truncated
    },
    destroy: () => {
      node.removeEventListener('click', handleClick);
    },
  };
}

With this setup, we can listen to the hello event on the element:

<script>
  import { action } from './my-action';

  function emitHandler(event) {
    console.log(event.detail); // 'hello'
  }
</script>

<element use:action on:hello={emitHandler}>...</element>

Adding Typescript Support

If possible, use Typescript to set up types for your action to support intellisense during development. Declaring types for action is very easy, you just need to extend the types exported from svelte/action:

import type { ActionReturn } from 'svelte/action';

type ActionParameter = {
  enabled?: boolean;
};

type ActionAttributes = {
  'on:hello': (event: CustomEvent<'hello'>) => void;
};

export function action(node: HTMLElement, param?: ActionParameter): {
export function action(node: HTMLElement, param?: ActionParameter): ActionReturn<ActionParameter | undefined, ActionAttributes>{
  // truncated
}

Notice that in this example, we use a union ActionParameter | undefined in ActionReturn because action allows users to not pass any parameters.

Action or Component?

If you come from frameworks that heavily use components for everything, you may wonder if the examples in this article can be written with components:

<Clickoutside>
  <element>...</element>
</Clickoutside>

<Tooltip>...</Tooltip>

The answer is most likely yes. However, when using components, you often need to wrap the target element in a parent (or use some workaround to limit DOM pollution). Take clickoutside as an example, how would you implement this feature with a component?

<!-- source code from joeattardi/svelte-click-outside: https://github.com/joeattardi/svelte-click-outside/blob/master/src/index.svelte -->
<script>
  // truncated
</script>

<svelte:body on:click={onClickOutside} />
<div bind:this={child}>
  <slot></slot>
</div>

Comparing to the source code of @svelte-put/clickoutside, you can decide for yourself which method is more readable and suitable for your style. However, we can agree that, in real-world applications, using components will be more verbose and sometimes even clunky since you need to go through a wrapper div, and especially because you do not have direct access to the element you want to manipulate.

<element use:clickoutside class="absolute ...">...</element>
<!-- vs -->
<Clickoutside class="absolute ...">
  <element>...</element>
</Clickoutside>

For me personally, using component for such tasks do not follow the ”single responsibility” principle. Component is a good solution for encapsulating UI. However, in the case of clickoutside, we only want to encapsulate the event handling logic, while decisions about markup or style are irrelevant and should be kept separate.

In short, when using Svelte, I often avoid using components as much as possible, and instead utilize features like action, context, or store to handle tasks that are not related to UI.

Change in Svelte 5

Svelte 5 should be announced any moment now, with many significant changes in its syntax. Fortunately, the way we write and use action in this article will still work. There is only one change that you may need to pay attention to in the future: the on... syntax to listen to events will no longer have the colon:

<element use:action on:hello={...}>
<element use:action onhello={...}>

Remember to update your ActionAttributes interface:

type ActionAttributes = {
  'on:hello': (event: CustomEvent<'hello'>) => void;
  'onhello': (event: CustomEvent<'hello'>) => void;
};

Again, it is worth acknowledging that action is just a regular Javascript function that does not depend too much on Svelte syntax. Therefore, action tends to stay stable even if Svelte changes in the future. Improvements in Svelte 5, especially runes, which now can also run in js or ts files, will open even more doors for action to better interact with other parts of the application.

Svelte action is really one of the best API design I have seen!

Closing

Thank you for reading. You can see more examples of Svelte action in packages from the svelte-put collection. There are also a couple more articles out there if you can’t get enough of Svelte action:

What about you? How have or will you use action in your project? Share your thoughts in our Svelte Vietnam Discord!


Found a typo or need correction? Edit this page on Github