Sometimes you want the content of a child component to live elsewhere in a parent. Django has template inheritance. Rails has content_for. 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:
<!-- Home.svelte -->
<div class="content">
<Main>...</Main>
<Sidebar />
</div>
<!-- Sidebar.svelte -->
<div class="sidebar">
<Banner />
</div>
<!-- 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 to send that modal up to .
The solution
We still want to define our modal inside 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.
// 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()
}
}
}
<!-- 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
<!-- 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:
<!-- Home.svelte -->
<Navbar id="navbar">
<Logo />
<Menu />
</Navbar>
<div class="container">
<Main />
<Sidebar>
<div id="sidebar-logged-in-actions" />
<RelatedContent />
</Sidebar>
</div>
<LoggedInUser />
<!-- 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 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.
Top comments (0)