Slots
In this module, we will learn how to dynamically change a component's markup using the slot tag.Course Version History
Nov. 21, 2022 - Updated to SvelteKit v1.0.0-next.549. Changed
index.svelte
to+page.svelte
.
At this point, we've learned a few different ways to pass data between components, allowing us to dynamically update the component's content. But the element is not very flexible when it comes to the template. Just like HTML Tables let us pass arbitrary html into the table rows and table cells, we can make our custom Svelte components also accept arbitrary HTML using Slots. In this module, we will learn how.
Here we have a basic modal component with no content in it. It is a white card with a black transparent overlay. We are importing this component in our layout where we are dynamically displaying it based on the value of show modal which I've set to true by default.
<div
on:click|self
class="absolute inset-0 flex h-full w-full items-center justify-center bg-black bg-opacity-30"
>
<div class="rounded-md bg-white p-8"></div>
</div>
<script>
let showModal = false;
function closeModal() {
showModal = false;
}
</script>
<button on:click="{showModal" ="true}">Show Modal</button>
{#if showModal}
<Modal on:click="{closedModal}" />
{/if}
Let's say we have two instances where we need to use this modal. The first is to alert the user that there is a sale, and the second is to display a form for them sign up to a mailing list. We could make two separate modal components, each displaying the appropriate content, but this would mean lots of repeated code. Just like elements can have children, so can components. Since the two modals are nearly identical, there is a lot of code to be shared. Instead, we can pass in the HTML attributes as a child of the component. But before a component can accept children, it needs to know where to put them. We do this with the <slot>
element. Let's add a slot to our modal like this
<div
on:click|self
class="absolute inset-0 flex h-full w-full items-center justify-center bg-black bg-opacity-30"
>
<div class="rounded-md bg-white p-8">
<slot> </slot>
</div>
</div>
Now, in +page.svelte
we can add any content we want within our modal tags and it will be inserted into our slot. For example, let's add some text and a button as children of our modal component like this:
<script>
let showModal = true;
function closeModal() {
showModal = false;
}
</script>
{#if showModal}
<Modal on:click="{closedModal}">
<p>Everything is 20% off this week!</p>
<button on:click="{showModal" ="false}">Got It!</button>
</Modal>
{/if}
Now, our modal will display this content that we are passing in. This allows us to re-use the modal throughout the app displaying unique content each time.
We can also add a fallback to our slots. This way, if our modal component is ever left empty, the fallback content will be displayed. We can do this by adding content within the slot tag in our modal component. If we move into our modal component, we can add a default <p>
tag that says "no content provided" and a button to close the modal.
<div
on:click|self
class="absolute inset-0 flex h-full w-full items-center justify-center bg-black bg-opacity-30"
>
<div class="rounded-md bg-white p-8">
<slot>
<p>no content was provided</p>
<button on:click>Close</button>
</slot>
</div>
</div>
Now, if we were to delete the HTML we're passing in from our root page, rather than seeing a blank white card, this fallback content will be displayed instead.
In this example, the component renders the direct child, but sometimes we need more control over placement. For instance, let's say this modal will always have a title, some content, and a button. We can add three different slots, each with their own fallback content, and then we need to add a name parameter to each.
<div
on:click|self
class="absolute inset-0 flex h-full w-full items-center justify-center bg-black bg-opacity-30"
>
<div class="rounded-md bg-white p-8">
<div>
<slot name="title">
<p>no title was provided</p>
</slot>
</div>
<div>
<slot name="content">
<p>no content was provided</p>
</slot>
</div>
<div>
<slot name="button">
<button on:click>Close</button>
</slot>
</div>
</div>
</div>
<slot>
<p class="mb-6 text-center text-xl font-bold text-black">
No content was provided.
</p>
<button
on:click
class="rounded-md bg-black p-2 font-medium uppercase text-white"
>
Close
</button>
</slot>
Since we have fallback content for each slot, that's what will be displayed, but in our parent component, we can pass in three children to this component using the provided slot names.
<script>
let showModal = true;
function closeModal() {
showModal = false;
}
</script>
{#if showModal}
<Modal on:click="{closedModal}">
<span slot="title"> Sale </span>
<span slot="content"> The whole store is 20% off! </span>
<span slot="button">
<button on:click="{showModal" ="false}">Awesome!</button>
</span>
</Modal>
{/if}
Now, the modal will display the content that we are passing into each named slot.
In some cases, we may want to control parts of the component based on whether the parent passes in content for a certain slot. For instance, lets move back into our modal component and remove our title fallback and add a wrapper around out title slot to add padding and a background color, like this:
<div
on:click|self
class="absolute inset-0 flex h-full w-full items-center justify-center bg-black bg-opacity-30"
>
<div class="rounded-md bg-white p-8">
<div class="bg-gray-100 p-2">
<slot name="title"> </slot>
</div>
<div>
<slot name="content">
<p>no content was provided</p>
</slot>
</div>
<div>
<slot name="button">
<button on:click>Close</button>
</slot>
</div>
</div>
</div>
Now, the purple container will be visible even though there is no content being passed in. In this case, we will want to be able to hide the wrapper if the slot is empty. We can check if the slot is empty by checking the properties of the special $$slots
variable. $$slots
is an object whose keys are the names of the slots passed in by the parent component. If the parent leaves a slot empty, then $$slots
will not have an entry for that slot. In our modal component, we want to use $$slots
to make sure we only render the title container when the parent passes in content for the title slot. We can do that by checking if $$slots.title
has an entry, and wrap the container in an if block like this:
<div
on:click|self
class="absolute inset-0 flex h-full w-full items-center justify-center bg-black bg-opacity-30"
>
<div class="rounded-md bg-white p-8">
{#if $$slots.title}
<div class="bg-gray-100 p-2">
<slot name="title"> </slot>
</div>
{/if}
<div>
<slot name="content">
<p>no content was provided</p>
</slot>
</div>
<div>
<slot name="button">
<button on:click>Close</button>
</slot>
</div>
</div>
</div>
Now the title container won't render when the title
slot is empty.
The last thing to go over regarding slots is slot props. There may be a time when you need a slot to track some state and send that data back to the parent component in order to update the contents of that slot. For instance, let's say in our modal component we want to change the button text when it is hovered. To do this, let's create a new variable called hover
and toggle it to true
on mouseenter
and false
on mouseleave
.
<script>
let hover = false;
</script>
<div
on:click|self
class="absolute inset-0 flex h-full w-full items-center justify-center bg-black bg-opacity-30"
>
<div class="rounded-md bg-white p-8">
{#if $$slots.title}
<div class="bg-gray-100 p-2">
<slot name="title"> </slot>
</div>
{/if}
<div>
<slot name="content">
<p>no content was provided</p>
</slot>
</div>
<div on:mouseenter="{()" ="">
{hover = true}} on:mouseleave={()=> {hover = false}}>
<slot name="button">
<button
on:click
class="font-mediump-2 rounded-md bg-black uppercase text-white"
>
Close
</button>
</slot>
</div>
</div>
</div>
Now, we want to access the value of hover in our parent component in order to update the content being passed into the slot. To get this value in the parent, we can pass it as a property of the slot, just like how we pass props between components. We will add hover={hover}
in our slot tag.
<script>
let hover = false;
</script>
<div
on:click|self
class="absolute inset-0 flex h-full w-full items-center justify-center bg-black bg-opacity-30"
>
<div class="rounded-md bg-white p-8">
{#if $$slots.title}
<div class="bg-gray-100 p-2">
<slot name="title"> </slot>
</div>
{/if}
<div>
<slot name="content">
<p>no content was provided</p>
</slot>
</div>
<div on:mouseenter="{()" ="">
{hover = true}} on:mouseleave={()=> {hover = false}}>
<slot name="button" hover="{hover}">
<button on:click>Close</button>
</slot>
</div>
</div>
</div>
Now, to expose hover
to the contents of the modal component, we use the let
directive. In our button slot within the modal component we can add let:hover={hovering}
now the value of hovering
will toggle with that of hover from our modal component. This allows us to conditionally display different content in our slot depending on the value of hovering
. I will go ahead and add an if block to display ‘Are you sure you want to close' whenever they hover on the close button.
<script>
let showModal = true;
function closeModal() {
showModal = false;
}
</script>
{#if showModal}
<Modal on:click="{closedModal}">
<span slot="title"> Sale </span>
<span slot="content"> The whole store is 20% off! </span>
<span slot="button" let:hover="{hovering}">
<button on:click="{showModal" ="false}">
{#if hovering} Are you sure you want to close? {:else} Close {/if}
</button>
</span>
</Modal>
{/if}
That sums up everything related to slots and customizing svelte components! I'll see you in the next video where we will spice that app up with some animations and transitions.
Was this helpful?