Web Components and You (part 6): Slots and how to use them

Published 2024-04-01, About 8 minute read.

It's time to talk about the real workhorse of the shadow DOM: slots! Slots enable you to accept markup from the consumer. The elements are styled as if they're in the light DOM, but they're placed where you determine in your web component. This is a pretty well-known patter in frameworks like Ember, Svelte, Vue, and others.

The simplest example: the default slot

The default slot is is whatever children markup elements you put inside your web component.

Here's an example where the content is being placed in a particular spot in the shadow DOM (in this case it's being put inside the div):

And if you were to inspect the DOM, you can see that the element is rendering in the "light dom" portion of the component outside of the shadow DOM. Yet there is a link between the slot and the revealed content:

You're probably already used to this sort of thing. This is what happens in textarea tags. If you place text inside those tags, that pre-populates the text value to the element

But this is a bit more powerful. The content lives and breaths! Meaning that if you make changes to the content in the slot, the slot contents reflect those changes. Here's an example that hopefully shows how the placed content can still be changed:

Okay, I understand if that's not mind blowing. But this is very helpful when we want to let consumers provide their own markup. We don't have to worry if this custom element is being used in a framework, because even when the framework re-renders or updates the contents in the slot, it will automagically be reflected in the custom element.

An example of augmenting the provided slot contents

Case study time! Let's say you're creating an accordion of sorts, and you want the consumer to be able to provide the content and the look of the button that will expand the content? There are two considerations here. First, we need to be able to specify where slots go when there are multiple slots. Second, we need to be able to attach the click action to that consumer-provided element.

First: multiple slots

This is the easier part. You can actually specify a name attribute for slots. When you do, you can then tag the element you want to be reflected into that slot. For example:

Examples are worth a thousand words:

One really cool thing about slots is that you don't necessarilly have to have a custom element defined with a class to use slots. Since slots are a feature of a shadow DOM you can actually create a declarative shadow DOM and place content in slots without a class!

Second: Attaching functionality to items in the slot

Okay, so we can create an accordion sketch here. We'll use named slots to specify where the content should be rendered. Just for this example, we will accept basically anything for the "content" slot, but we will expect the consumer to know they must provide a clickable element like a button to toggle the accordion open and closed.

In this first part the actual click event listener is on the slot element itself, and this is not going to fly for accessability. Tabbing through the DOM you would end up with focus on the button and the click event would bubble up to the slot. It's not accurate or ideal for accessibility reasons: one being that if you were to handle whether the accordion is expanded with aria-expanded attribute you would want it to be on the focused element- the button

So let's try that again, but let's attach the attribute and the event listener on the button instead. This will require digging into the shadow DOM and the slot!

This is not perfect but much better! Let's break it down:

Now this example is a teensy bit contrived because it actually can be simplified in one way: instead of reaching into the slot and using the assignedElements() method, we can just adjust the getter to query the host element. Remember that elements are rendered into the light DOM and then reflected into place in the shadow DOM? Well, why don't we just edit the light DOM because it's easier to access?

But, there's an alternative to this dom drilling

We can leverage custom events instead of attaching handlers.

If you don't like the feeling of modifying the elements that the consumer gives you (in this case, the trigger button,) you can also just make the component expect certain events. In this next example, we'll let the consumer emit a custom event "please-expand" and the accordion will react to that:

I'm not sure that's any better. The downside to this is that you're expecting the consumer to handle aria attribute changes along with instructing the accordion to open, but maybe that's okay? In general you would expect a component to just take care of state and aria attributes, so this seems like the worse of the two options. But it is an option!

Whew. Let's zoom out about and talk about CSS

How about we chat about styling contents that were added into a slot?

Let's say that you want to create a list of things. For some reason, you want to add some spacing between all the elements in the slot. There's a selector for that!

Alright, so let's say that you accept any number of elements in your default slot. But! You want all those elements to have background blue and be spaced with an extra line-space between them. You could do this:

Note that ::slotted() selects any direct child element that is in a slot. If you want to specify which slot to work with, you can use the [name="..."] as a selector inside the slotted function parens. That is how the last slotted element turned orange in the above example.

One last item: watching change events in the slot

There may be a time when you need to watch for changes inside a slot, and react to those changes. There's an event for that: 'slotchange'. This event will always run once when the component is connected and the slot contents are filled the first time.

One important thing to note: this event will only be emitted if the direct child nodes of the slot change or are added or subtracted. Any children or other descendants of the slotted node will not trigger this event. Check out the console after adding and removing nodes in the next playground.

I think this is mostly so you can attach handlers and cleanup after yourself if you're programatically doing things to elements in the slots. The annoying thing here is that text change or descendent nodes changing do not trigger this event. If you really need control of mutations in the DOM at a finer level, you can use MutationObserver

That about does it

Slots are very powerful and allow you to compose elements into your web components. It allows you to give some control to your consumer. However, it is still a bit awkward to give functionality to components in slots.

What do you think? Is there a pattern that's really nice that I didn't cover? Let me know in the comments or on bluesky!

⬅️ Previous Post Next Post ➡️