The what and why of slot { all: inherit; }
A couple weeks ago I posted this survey on Mastodon:
The yeas and nays were pretty evenly divided, but the wuts won by a mile, so I guess I should answer the “wut”.
Say you have a tooltip component. The tooltip works by wrapping the thing you want a tooltip on, like this[1]:
<my-tooltip tip-text="Click to do a thing">
<button>Click me</button>
</my-tooltip>
The implementation details of the tooltip aren’t important except that the tooltip component itself has display: contents because wrapping an element with a tooltip shouldn’t impact the layout at all, and the tooltip trigger/anchor is slotted in.
:host {
display: contents;
}
slot {
/* already display: contents by default */
}
Now let’s say that rather than making consuming apps wrap buttons in tooltips you want to build the tooltip functionality right into the button. (For example, you could make it so that it automatically adds a tooltip to a button if the button text is truncated.) So let’s create a button component that looks something like this:
<my-button>
#shadow-root
<my-tooltip>
<button><slot></slot></button>
</my-tooltip>
Click me
</my-button>
Styling the button component is a little tricky because we want to style the <button> in its shadow tree, but want a consuming app to be able to override some of that styling from the outside. We could do that with custom properties or a part, but the simplest API for a consuming app is to let someone set certain properties on the component itself.
Without the tooltip, we could do something like
:host {
display: inline flow-root;
color: white;
background-color: darkorchid;
border-radius: calc(1px * infinity);
}
button {
/* styles that can't be modified from the outside */
border: none;
padding: 8px 12px;
font-family: system-ui;
font-weight: bold;
line-height: 1;
cursor: pointer;
/* styles that can be modified from the outside */
background-color: transparent; /* so the host color shows through */
color: inherit; /* color is normally inherited, but button UA styles set it to ButtonText */
border-radius: inherit; /* apply the same radius to the button as to host */
}
But when you add the tooltip in between, you need to make sure that those properties set on :host/my-button that you want to be able to pass through the tooltip component to the <button> are able to.
The fairly obvious thing to do is to add a ruleset to the button component that opts the tooltip component into inheriting those properties from :host that aren’t inheritable by default.
my-tooltip {
border-radius: inherit;
}
But there’s still a problem. If you look back to the top, you’ll see that there’s a <slot> element inside the tooltip component’s shadow root. That <slot> is in between the tooltip host and the slotted button element, so we also need to ensure that the <slot> can inherit the properties we want.
In the button component, the property in question is border-radius, but the tooltip can be used in situations outside of the button component, so being overly opinionated inside the tooltip is setting us up for a maintenance headache down the road: what if another component wants different properties to be inherited?
Then it occurred to me: maybe you can add this to the tooltip:
slot {
all: inherit;
}
Now any inheritable property on :host will also be inherited by slotted content.
It seemed pretty out there, so I really appreciated hearing from Westbrook Johnson, who had actually done something pretty similar recently.
In most circumstances you wouldn’t want to do something as wide-reaching as this. As Amelia Bellamy-Royds points out, inheriting most box properties could lead to some pretty weird results in a lot of cases. In this case, though, it’s pretty low risk because the only properties that will actually get inherited are (a) those that are inherited by default so would be inherited anyway, (b) those properties set directly on the parent/host element, and (c) those that are set to inherit through any continuous ancestral line (i.e., parent and grandparent).
The biggest risk with the slot element is inheriting display. slot defaults to display: contents, and we’d like to keep it that way. Luckily, the tooltip’s host element has display: contents set directly on it, so that’s no problem (category (b)).
So the only properties we need to concern ourselves with are those that are opted into above (category (c)), and the only effect they’ll have is on slotted content that has also opted in to inheriting those properties.
So we have one specific property that isn’t normally inherited — border-radius — with the following inheritance chain:
my-button
Default value set on:hostbut can be changed by a consumermy-tooltip
Specific properties set toinheritin the button component stylesheetslot
all: inheritset in tooltip stylesheetbutton
Slotted intomy-tooltipwith specific properties set toinheritin button component stylesheet
So that’s why I have slot { all: inherit; } in one of my components. At least for now — until I shoot myself in the foot.
Custom attributes may ultimately be a better way to do this, but those don’t exist yet. ↩︎