Web Components - Slot policing
You can use CSS ::slotted() to create an allow-list of elements permitted in a web component slot. This doesn't require any Javascript or awareness of the HTML inside the slot, which makes it especially useful if you require SSR.
Whilst <slot></slot> elements are extremely powerful, it can be hard to ensure consumers of your components use them appropriately.
Let's say we have a custom-notification component that has two named slots for action buttons. The designs require the consumers only ever provide our custom-button components to this slot.
Ideally, you want consumers to write something like:
Show code
<custom-notification>
<!-- omitting stuff -->
<custom-button variant="primary" slot="leadingAction">Leading action</custom-button>
<custom-button variant="secondary" slot="trailingAction">Trailing action</custom-button>
</custom-notification>
However, sadly you've seen people writing things like this:
Show code
<custom-notification>
<!-- omitting stuff -->
<div class="nightmare-button" role="button" slot="leadingAction">Leading Action</div>
<div class="nightmare-button" role="button" slot="trailingAction">Trailing action</div>
</custom-notification>
You can certainly use Javascript to inspect the markup provided, but if your components must be compatible with SSR (server-side rendering) then this can be a challenge.
For example, when using Lit's queryAssignedElements decorator, the contents of a slot can only be read at runtime.
What we want, is a way to prevent people passing in inappropriate HTML, that doesn't interrupt SSR or cause any janky flashes or layout shifts at runtime.
CSS
If you find consumers are misusing your slots, you can just straight up create an allow-list of specific elements. This solution is quite simple, as you cannot query beyond a single immediate level in the slot, but it's still pretty effective:
Show code
::slotted(*) {
display: none;
}
/* Allow-list specific elements */
::slotted(custom-button) {
display: revert;
}
We can get even tougher, and disallow raw text content too in favour of specific text nodes:
Show code
/* 1. Visually hide raw text nodes directly inside the slot */
slot {
font-size: 0;
color: transparent;
}
/* 2. Hide all slotted elements by default */
::slotted(*) {
display: none;
}
/* 3. Allow-list specific elements */
::slotted(input),
::slotted(label) {
display: revert;
font-size: 1rem; /* whatever you need */
}
/* 4. Apply element-specific styling - set colours etc */
::slotted(label) {
color: red;
}
SCSS Mixin
We could even create an SCSS mixin to reuse this across our web component library:
Show code
@use "sass:string";
@use "sass:list";
@mixin enforce-slot-elements($allowed-elements, $restored-font-size: 1rem, $allow-raw-text: false) {
// 1. Only hide raw text if $allow-raw-text is set to false
@if not $allow-raw-text {
slot {
font-size: 0;
color: transparent;
}
}
/* 2. Hide all slotted elements by default */
::slotted(*) {
display: none;
}
/* 3. Allow-list specific elements */
$allowlist-selectors: ();
@each $el in $allowed-elements {
$allowlist-selectors: list.append($allowlist-selectors, string.unquote("::slotted(#{$el})"), comma);
}
#{$allowlist-selectors} {
display: revert;
// Only bother restoring the font-size if we explicitly shrunk it earlier
@if not $allow-raw-text {
font-size: $restored-font-size;
}
@content;
}
}
// usage
.my-strict-component {
@include enforce-slot-elements(('input', 'label'), 1rem);
}
.my-less-strict-component {
// Pass true to allow raw text alongside inputs and labels
@include enforce-slot-elements(('input', 'label'), 1rem, true);
}
It isn't perfect, but gets the point across. For example you'd likely not want to reset the font-size to the same value across all allowed elements (although nothing stops you overriding them individually later on).
You could even enforce certain attribute usage such as requiring aria-label or specific input types:
Show code
.my-strict-component {
@include enforce-slot-elements(('input[type="radio"]', 'span[aria-label]'), 1rem);
}
Good documentation is still key, but this forces people to immediately confront any misuse of your components. Although don't be surprised when they still raise a support request claiming the component is broken.