Introduction to Svelte Actions
Tags: svelte, webdev, tutorial, programming
Published: 2021-12-02 17:58:32

**Written by [Geoff Rich](https://blog.logrocket.com/author/geoffrich/)** ✏️

Actions are one of Svelte’s less commonly used features. An action allows you to run a function when an element is added to the DOM. While that sounds simple, an action used in the right way can greatly simplify your code and allow you to reuse bits of logic without creating an entirely separate component.

In this post, I’ll give two examples where a Svelte action would be useful and show why an action is the right tool for the job.

## Using Svelte actions to focus an input

Let’s start with the following [Svelte component](https://blog.logrocket.com/build-your-own-component-library-svelte/). We have some static text with an edit button next to it. When the edit button is clicked, a text field is revealed. Typing in the field updates the text, and you can confirm to save your changes.

```html
<script>
    let name = 'world';    
    let editing = false;

    function toggleEdit() {
        editing = !editing
    }
</script>

<p>
    Name: {name}
</p>

{#if editing}
<label>
    Name
    <input type="text" bind:value={name}>
</label>
{/if}

<button on:click={toggleEdit}>
    {editing ? 'Confirm' : 'Edit'}
</button>
```

This UI is a little annoying, since you need to click (or tab into) the edit field after clicking the edit button. It would be a better experience if it was automatically focused, so you could start typing right away. How can we do that?

### Option 1: `bind:this`

If you’re familiar with [binding to DOM elements](https://svelte.dev/docs#bind_element) in Svelte, you might think of doing something like this:

```html
<script>
    let name = 'world';    
    let editing = false;
    let input;

    function toggleEdit() {
        editing = !editing
            if (editing) {
                    input.focus();
            }
    }
</script>

<p>
    Name: {name}
</p>

{#if editing}
<label>
    Name
    <input bind:this={input} type="text" bind:value={name}>
</label>
{/if}

<button on:click={toggleEdit}>
    {editing ? 'Confirm' : 'Edit'}
</button>
```

However, if you try to run that code, you get an error in the console:

```bash
Uncaught TypeError: input is undefined
```

This is because the input is not added to the DOM yet, so you can’t focus it after setting `editing` to `true`.

Instead, we need to call Svelte’s `[tick function](https://svelte.dev/docs#tick)`, which returns a promise that resolves when Svelte has finished applying any pending state changes. Once `tick` resolves, the DOM will be updated and we can focus the input.

```javascript
function toggleEdit() {
    editing = !editing
    if (editing) {
            tick().then(() => input.focus());
    }
}
```

That works, but it doesn’t feel very intuitive. It’s also not very reusable — what if we want to apply this behavior to other inputs?

### Option 2: Move the input into a separate component

Another option is to move the input into its own component, and focus the input when that component mounts. Here’s what that looks like:

```javascript
<script>
    export let value;
    export let label;
    let input;

    import { onMount } from 'svelte';

    onMount(() => {
            input.focus();
    });
</script>

<label>
    {label}
    <input type="text" bind:this={input} bind:value>
</label>
```

Then it can be used in the parent component, like so:

```javascript
{#if editing}
<Input bind:value={name} label="name" />
{/if}
```

However, with this approach you have to incur the cost of creating a new component, which you didn’t need to do otherwise. If you want to apply this behavior to another input element, you would need to make sure to expose props for every attribute that is different.

You are also limited to input elements with this method, and would need to reimplement this behavior if you wanted to apply it to another element.

### Option 3: Use Svelte actions

Though these are all viable solutions, it feels like you’re having to work around Svelte instead of with it. Thankfully, Svelte has an API to make this sort of thing easier: [actions](https://svelte.dev/docs#use_action).

An action is just a function. It takes a reference to a DOM node as a parameter and runs some code when that element is added to the DOM.

Here’s a simple action that will call focus on the node. We don’t have to call `tick` this time because this function will only run when the node already exists.

```javascript
function focusOnMount(node) {
    node.focus();
}
```

We can then apply it to a node with the `use:` directive.

```javascript
{#if editing}
<label>
Name
<input use:focusOnMount type="text" bind:value={name}>
</label>
{/if}
```

That’s a lot cleaner! This is just a few lines of code to solve the same problem we were dealing with before, and it’s reusable without needing to create a separate component. It’s also more composable, since we can apply this behavior to any DOM element that has a `focus` method.

You can [see the final demo](https://svelte.dev/repl/8808de159c4a410693122074b83fe0e4?version=3.43.2) in this Svelte REPL.

## Example 2: Integrating Svelte actions with Tippy

Actions are also great when you want to integrate with a vanilla JavaScript library that needs a reference to a specific DOM node. This is another strength of Svelte — while the [Svelte-specific ecosystem](https://blog.logrocket.com/whats-new-svelte-summer-2021/) is still growing, it’s still easy to integrate with the [vast array of vanilla JS packages](https://blog.logrocket.com/tag/js-libraries)!

Let’s use the tooltip library [Tippy.js](https://atomiks.github.io/tippyjs/) as an example. We can pass a DOM element to initialize Tippy on that node, and also pass an object of parameters.

For example, here’s how we can add a tooltip using vanilla JS:

```javascript
import tippy from 'tippy.js';

tippy(document.getElementById('tooltip'), { content: 'Hello!' });
```

We can use a Svelte action to run this code so we have a reference to the node without calling `document.getElementById`. Here’s what that might look like:

```javascript
function tooltip(node) {
    let tip = tippy(node, { content: 'Hello!' });
}
```

And it can be used on an element like so:

```javascript
<button use:tooltip>
    Hover me
</button>
```

But how do we customize the properties we use to initialize the tooltip? We don’t want it to be the same for every use of the action.

### Passing parameters to actions

Actions can also take parameters as a second argument, which means we can easily customize the tooltip and allow consumers to pass in the parameters they want.

```javascript
function tooltip(node, params) {
    let tip = tippy(node, params);
}
```

And here’s how you use it on an element:

```html
<button use:tooltip={{
    content: 'New message'
}}>
    Hover me
</button>
```

Note the double curly brackets. You put the parameters you want to pass to the action inside the curly brackets. Since we’re passing an object to this action, there are two sets of curly brackets: one to wrap the parameters and one for the parameter object itself.

This works, but there’s a few problems:

1.  There’s no way to update the parameters after the action runs
2.  We aren’t [destroying the tooltip](https://atomiks.github.io/tippyjs/v6/methods/#destroy) when the element is removed

Thankfully, actions can return an object with `update` and `destroy` methods that handle both these problems.

The `update` method will run whenever the parameters you pass to the action change, and the `destroy` method will run when the DOM element that the action is attached to is removed. We can use the Tippy `setProps` function to update the parameters, and `destroy` to remove the element when we’re done.

Here’s what the action looks like if we implement these methods:

```javascript
function tooltip(node, params) {
    let tip = tippy(node, params);
    return {
        update: (newParams) => {
            tip.setProps(newParams);
        },
        destroy: () => {
            tip.destroy();
        }
    }
}
```

This allows us to write a more complicated example that updates the placement and message of the tooltip after initial creation:

```javascript
<script>
    import tippy from 'tippy.js';

    function tooltip(node, params) {
        let tip = tippy(node, params);
        return {
            update: (newParams) => {
                tip.setProps(newParams);
            },
            destroy: () => {
                tip.destroy();
            }
        }
    }

    const placements = ['top', 'right', 'bottom', 'left'];
    let selectedPlacement = placements[0];
    let message = "I'm a tooltip!";
</script>

<label for="placement">Placement</label>
<select bind:value={selectedPlacement} id="placement">
    {#each placements as placement}
        <option>{placement}</option>
    {/each}
</select>

<label>Message <input bind:value={message} type="text"></label>

<button use:tooltip={{
    content: message,
    placement: selectedPlacement
}}>
    Hover me
</button>
```

You can find the final example in [this Svelte REPL](https://svelte.dev/repl/59afe883e6d84533a612f137dc56c4b8?version=3.43.2). ![Our tooltip example](https://blog.logrocket.com/wp-content/uploads/2021/11/tooltip-example.png)

## Alternate approaches without using actions

As with the example before, we didn’t _need_ actions to be able to do this. We could also attach the tooltip when the component mounts and update the parameters using reactive statements. Here’s what that might look like:

```javascript
><script>
    import tippy from 'tippy.js';
    import { onMount, onDestroy } from 'svelte';

    let button;
    let tip;

    onMount(() => {
        tip = tippy(button, { content: message, placement: selectedPlacement});
    });

    $: if (tip) {
        tip.setProps({ content: message, placement: selectedPlacement });
    }

    onDestroy(() => {
        tip.destroy();
    });

    const placements = ['top', 'right', 'bottom', 'left'];
    let selectedPlacement = placements[0];
    let message = "I'm a tooltip!";
</script>

<label for="placement">Placement</label>
<select bind:value={selectedPlacement} id="placement">
    {#each placements as placement}
        <option>{placement}</option>
    {/each}
</select>

<label>Message <input bind:value={message} type="text"></label>

<button bind:this={button}>
    Hover me
</button>
```

This approach is totally valid. However, it is less reusable across multiple components and becomes tricky if the tooltip element is conditionally rendered or in a loop.

You might also think of creating a component like `<TooltipButton>` to encapsulate the logic. This will also work, though it limits you to one type of element. Implementing it as an action lets you apply the tooltip to any element, not just a button.

## Wrapping up

Actions are a very powerful Svelte feature. Now that you’re familiar with them, make sure you check out the [official tutorial](https://svelte.dev/tutorial/actions) and [docs](https://svelte.dev/docs#use_action) to see other ways to use actions. They aren’t always the right solution — many times, it is better to encapsulate the behavior in other ways, such as in a separate component or with a simple event handler. However, there are times like the examples above where they make your component code much cleaner and more reusable.

There is also an [open RFC](https://github.com/sveltejs/rfcs/pull/24) to add inbuilt actions to Svelte, similar to how Svelte includes inbuilt transitions. As part of that RFC, the community created a [POC library](https://github.com/sw-yx/svelte-actions) with some commonly-used actions such as `longpress`, `clickOutside`, and `lazyload`.

You may also be interested in a post I wrote last year on [using actions to detect when a sticky-positioned element becomes stuck to the viewport](https://geoffrich.net/posts/svelte-action-sticky/).

---

## [LogRocket](https://logrocket.com/signup/): Full visibility into your web apps

[![LogRocket Dashboard Free Trial Banner](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://logrocket.com/signup/)

[LogRocket](https://logrocket.com/signup/) is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

[Try it for free](https://logrocket.com/signup/).