Create portal slots in Svelte using actions
Tags: svelte, javascript, tutorial
Published: 2021-09-01 20:57:26

Sometimes you want the content of a child component to live elsewhere in a parent. Django has [template inheritance](https://levelup.gitconnected.com/django-template-inheritance-and-reusability-e97b0fed8bcc). Rails has [content_for](https://guides.rubyonrails.org/layouts_and_rendering.html#using-the-content-for-method). Svelte has slots, but they send content from parent to child, not the other way around.

In this article we'll look into a solution for that.

---

### The problem

Say you have a nice generic `<Modal />` component that shows some content over the screen. 

As you start creating multiple Modals across your app, you have to define them on the same component that triggers it to keep things readable, share state, have event listeners…

So this structure is not uncommon:

```html
<!-- Home.svelte -->
<div class="content">
  <Main>...</Main>
  <Sidebar />
</div>
```

```html
<!-- Sidebar.svelte -->
<div class="sidebar">
  <Banner />
</div>
```

```html
<!-- Banner.svelte -->
<div class="banner">
  <button>View promotion</button>
</div>

<Modal>
   My custom promotion content in a modal
</Modal>
```

And so far that's all fine. Your modal is rendered inside the `.sidebar` div, but you're using `position: fixed` to make it "break out" and appear over everything, so doesn't really matter where it is in the Dom, right?

Well... If your sidebar has `overflow: hidden` or any `transform` set, then your modal will be clipped inside it, and that's not what you want.

So it **does** matter where the modal is in the Dom. Your modal should be all the way up in `Home.svelte` so nothing "contains" it, but Svelte slots don't work that way, and there's no way for <Banner> to send that modal up to <Home>.

---

### The solution

We still want to define our modal inside <Banner> and take advantage of all that gives us, but have it be rendered outside the box.

We're talking about manipulating the Dom, and that made me look into Svelte Actions. If you're not familiar, an action is just a function that gets a Dom node to play with. 

```js
// actions.js

// Portal action
export function portal(node, name) {
  // find an element with this ID somewhere in the document
  let slot = document.getElementById(name);
  // move this node to that element
  slot?.appendChild(node);

  return {
    destroy() {
      // remove the node when component is destroyed
      node.remove()
    }
  }
}
```

```html
<!-- Modal.svelte -->
<script>
  import { portal } from "./actions.js";
  export let isOpen = false;
</script>

{#if isOpen}

  <!-- Modal div with portal action -->
  <div use:portal={'modals'} class="modal">
    <slot></slot>
  </div>

{/if}
```

Whenever `isOpen` is true, our modal div is created and the `portal` action is called. Then we find an element with `id="modals"` in our layout and move the modal div to that element.

So we just need to have a `#modals` div all the way up in our Dom, let's put it in `Home.svelte`

```html
<!-- Home.svelte -->
<div class="content">
  <Main>...</Main>
  <Sidebar />
</div>

<!-- Portal slot for Modals -->
<div id="modals"></div>
```

---

### Does it work?

Yes! Surprisingly all Svelte features still work: Props, bindings, custom event listeners, lifetime cycle events, instance references…

We only moved the node, we didn't clone it or duplicate it, so Svelte's reference remains unchanged.

I admit this made me nervous at first, but I threw everything I could think of at it and it didn't break.

In any case let me know if I missed something and should not be doing this.

---

### Multiple portals

Our portal action is generic, it takes a slot name as a parameter. This means we can use it with different slots in different parts of our app.

Here's a more extreme example:

```html
<!-- Home.svelte -->
<Navbar id="navbar">
  <Logo />
  <Menu />
</Navbar>

<div class="container">
  <Main />
  <Sidebar>
    <div id="sidebar-logged-in-actions" />
    <RelatedContent />
  </Sidebar>
</div>

<LoggedInUser />
```

```html
<!-- LoggedInUser.svelte -->
<script>
  ...
</script>

{#if isLoggedIn}

  <!-- append this to navbar -->
  <div use:portal={'navbar'}>
    <!-- Todo: logged in user avatar with dropdown options -->
  </div>

  <!-- actions only available for logged in users -->
  <div use:portal={'sidebar-logged-in-actions'}>
    <FeedbackForm />
  </div>

{/if}
```

---

Here's a Repl with some tests: https://svelte.dev/repl/2122ac70a8494ff4a6fca4ba61b512be?version=3.42.4

---

### Update & Disclosure

After writing this article I found out that React has "Portals", which lead me to find the [svelte-portal](https://github.com/romkor/svelte-portal) project that has pretty much the same solution but is already packaged and accounts for server-side rendering. Consider using it instead of hard-coding your own solution.

I renamed my action name from "Layout Slots" to "Portal" so people can find this article more easily and because it's a cooler name. Also updated article title to reflect the name change.