Effective modals in SvelteKit

  • — SvelteKit

  • 4 min. read

  • — 5/22/2023

Let's learn how to build effective modals in SvelteKit. I actually ran head-on into this problem for an app I'm building. I'll explain a bit below what the problem is and why it's important.

The problem

Okay, so I had a regular <Modal/> component that I would place at various places in my app. I passed a prop to it that would determine its open/close state. The modal itself was styled to be fixed and had a z-index set to 50 (in TailwindCSS, this is the highest z-index you can use!)

The problem: z-indexing

You see, placing content with components at various places in your app is a smart way to reuse code and keep your codebase streamlined. However, apps are big, there's various levels and nesting within the DOM where your component can go, and in my case, it just so happens that placing a <Modal /> component deep within my app caused issues where parts of the modal were cut-off or glitched due to other elements with higher z-indexes at different levels of the DOM taking precedence.

You think this would work but you'd be in for a long day of debugging and head-smashing your keyboard... 

Okay so, put differently, we need to come up with a way so that our <Modal /> component always renders on top of our site content, regardless of where we call a modal in our app. Not to be Dora, but do you know the answer?

tenor.com

The solution

So, this is gonna sound really weird, but the answer is portals.

tenor.com

I know. Portals aren't really a concept in Svelte (I checked). Portals were introduced in React - I know this is a SvelteKit post but just for context - as a way to define component at a certain level in your app's DOM but have the contents of the component render somewhere completely differently.

Here's a basic diagram of what that means:

In this example, our green component was called somewhere deep in our component tree. However, in our rendered output, it actually rendered completely outside the tree! This is the concept of portals.

And we need to re-create it in Svelte.

Fortunately, it's actually really easy and straightforward.

Building a portal functionality in Svelte

First, let's build our <Modal /> component, for this we're going to use a very straightforward modal with some basic styling (using TailwindCSS):

// Modal.svelte
<script>
let show = false;
</script>
{#if show}
<div class="fixed inset-0 bg-black/60 flex flex-col justify-center items-center">
	<div class="bg-white rounded-2xl p-6 max-w-md mx-auto w-full">
		<slot/>
	</div>
</div>
{/if}

This will give us a super basic modal with a blackish background and white foreground. We also use show here to determine if the modal should be showing or not (this is called a "controlled" modal).

Now, let's use our modal. I'm going to simulate a "deeply nested" <Modal /> component by wrapping it in a ton of divs.

// +page.svelte

<script>
   import Modal from "./Modal.svelte"
</script>
<svelte:head>
   <script src="https://cdn.tailwindcss.com"></script>
</svelte:head>
<main>
   <div>
      <div>
         <Modal>
            <div class="space-y-2">
               <h1 class="text-2xl font-bold">
                  Hello World!
               </h1>
               <p>
                  I'm a stylized modal.
               </p>
            </div>
         </Modal>
      </div>
   </div>
</main>

Remember, we want our modal component to be rendered somewhere else so it stays on top of all other content. In this example, we want <Modal/> to be a child of main.

Let's now implement the portal in Svelte. We're going to do this in two parts: we'll first write a utility function that will handle the render, then, we'll use a Svelte action that calls the utility function.

Create a new file in your project named portal.js, this will serve as a utility for the portal.

// portal.js

export const portal = (node) => {
	document.querySelector('main')?.appendChild(node).focus();
};

Now, in our <Modal/> component, we just need to call this function using a Svelte action:

// Modal.svelte

<script>
let show = false;
import { portal } from "~/lib/portal.js"
</script>
{#if show}
<div use:portal class="fixed inset-0 bg-black/60 flex flex-col justify-center items-center">
	<div class="bg-white rounded-2xl p-6 max-w-md mx-auto w-full">
		<slot/>
	</div>
</div>
{/if}

By the way, I'm assuming you're using a ~ import alias here, if not, just import portal from wherever your portal.js file is.

And... that's it, our modal will now render completely outside of where we define it! This will work for every modal you use in your app.

You might be wondering how exactly this works, put simply, we intercept Svelte's rendering by using an action (actions in Svelte get called when the component is created), we then tell Svelte to put this component as a child of the body. We do this using a utility function we defined in portal.js. When the component unmounts (ie someone closes the modal), the rendered content is removed from the DOM, allowing for another modal (or the same one) to take its place when it's opened.

👏 We just made effective modals in SvelteKit.

Here's the Svelte REPL if you're interested in the code: https://svelte.dev/repl/08bc493b86f340d69eb7b03110025d06?version=3.59.1

Shaun Chander

hey (again), I'm shaun

I'm posting 3 tips on creative web development daily. Subscribe below to to get tips delivered straight into your inbox!