Inheritance, the cascade, and shadow dom encapsulation
Like a lot of my posts, this one comes straight from the lunch table. I’m pretty lucky to have a great group of devs to rant to about all the crazy front-end stuff. They’re probably reading this. What’s up lunch crew! Particular shout out to Jesse Warden for this topic and a fun discussion over asian-style pork belly and grilled salmon the other day.
I was describing to the crew a bit about how I like to write reusable components. I’m a design system guy, so reusable components are kinda my thing. We got on the subject by talking about how Tailwind can actually fight against using component-specific CSS custom properties to configure a component because the components my team has made all use Tailwind and I am thinking about how best to refactor them for simpler style configuration. Don’t worry, my criticism of Tailwind will be in my next post and I welcome the onslaught of Tailwind bros soon. Anyway, I model the way I like to write for reusability after the way Kevin Powell tends to write his CSS in all of his videos.
Shameless plug: I am working on a design system course, called Thoughtful Design Systems, with Kevin that will hopefully be out soon! I’m stoked for the opportunity to work with a CSS legend that I’ve long admired. The signup is available now so please head over there and sign up if building thoughtful design systems is your thing also.
The way Kevin writes all his CSS demos is that he sets up the functionality, gets it working how he wants, then adds a bunch of CSS custom properties into his CSS so that if you wanted to tweak anything about the demo you could just be setting them to different values. If you know anything about me, you know that I’m a web components guy and that one of my mantras is “Don’t use JS when CSS will do the job”.
Don’t use JS when CSS does the trick#
If you find yourself about to write a prop for a component with a name that even remotely resembles a CSS property name, that should be your cue to stop and turn that config into a component-specific CSS custom property. Any time you want to write a buttonPadding or anything like it, stop yourself and write CSS instead. Setting JS props usually means re-rendering a component just to change a style, when CSS and the browser will repaint it for you for free. Also, as we’ll talk about in a second, CSS custom properties inherit by default, JS properties don’t. So you can set CSS custom properties to configure your components all the way at the root level of your app and never have to worry about messy prop-drilling or context providers.
This might not apply to many of you. Whether or not sprinkling loads of CSS custom properties into your components is actually useful entirely depends on whether or not you are making that component for someone else to use. If you are, giving them CSS custom properties is giving them a dead simple way to make powerful, flexible customizations which might prevent your consumers from using some other component instead of yours that is easier to bend to their will. Ask me how I know.
So, I’m sitting with the boys and I say:
Michael: “CSS Custom properties are great because they inherit through the Shadow DOM and other styles like those Tailwind classes dont (without some manual effort at least)”
And Jesse says:
Jesse (who has been out of the front end game for a long time): Wait, I thought shadow DOM was encapsulated and styles can’t get in and that‘s why they are so good because no one‘s styles can break the component and the component styles can’t break the app.
And that triggered a great discussion about the difference between “the cascade”—the “C” in “CSS”—and ”inheritance”, a feature thereof.
What is the Cascade?#
First off, I’ll just say that I am by no means the foremost expert on this topic and you should totally supplement this article with content from Kevin Powell or Miriam Suzanne. They are the true experts and I‘m just going to paraphrase their content and some content from MDN.
In short, the Cascade is the algorithm browsers implement that decides what styles finally land on all of the elements defined in HTML (and in CSS) according to the result of the 5 steps of said algorithm. The 5 steps are:
- Relevance
- Origin & importance
- Specificity
- Scoping proximity
- Order of appearance
The cascade is all about deciding what styles apply to what elements and what values ultimately control the visual design you see on the page after the algorithm is run. There is a point to be made here that the cascade only really deals with CSS selectors, properties and values that exist and have been written by either a dev writing their styles, or the default user-agent styles that all browsers ship with just so bare-bones HTML looks kinda ok.
That might seem that it goes without saying, but we’ll see why I wanted to make that point in the next section.
When you are working on your web site and you are thinking about which style will win over which, and you are checking out things like IDs & classes, cascade layers, specificity rules and calculation, you are thinking about how the Cascade algorithm will work when your HTML and CSS are parsed and rendered.
Inheritance#
Inheritance is another aspect of how we get web stuff built, but it is not the same concept as the Cascade. Inheritance only really comes into play when after the Cascade has run (or during, I’m not 100% sure about the ordering of tasks during browser paint) and the result is that there is no value specified for some property. Inheritance can only apply if/when there is no value for a CSS property after all the Cascade results have been run. The idea of inheritance comes from the fact that certain values in CSS probably just have to have some sort of value to work. The most common example of an inherited property is color.
To explore how inheritance works, ask yourself this question:
You (to yourself): If I write some HTML but write no CSS at all, how does all my text end up black? How does the
colorproperty for my<p>tags end up being set to black?
Inheritance is how. If there is no specified value for a CSS property, and that property is one that can inherit, then the value of that property for that element is computed to be whatever its parent had. That’s how all your <p> tags end up black even if you write zero CSS for some HTML page. The browser’s default styles set color: #000 on the root of the page, and from that point on down the HTML tree, all elements that do not have a specified color will get the root color value from the user-agent styles copypasta’d into the color property.
The list of CSS properties that are inherited unless otherwise specified isn’t crazy long. You can also control inheritance with other keywords like inherit, initial, revert, unset, etc. The point is that inheritance is what happens when the value of a CSS property for some element isn’t specified.
The other point is that CSS Custom properties DO inherit by default, and they inherit through the shadow DOM boundary
Inheritance and the shadow DOM#
You might be scratching your head at that last sentence. If you have been reading my blog and/or have heard of web components, then you have doubtless heard that the major supposed benefit (depends on who you ask) of the shadow DOM for web components is the encapsulation of styles. You probably have heard the same thing that I said above—that styles that are not adopted into the shadow root do not apply to elements within that shadow root and vice versa. But shadow DOM encapsulation is only about the Cascade, not inheritance. So a more technically correct (the best kind of correct, imo) way to say that last sentence is that shadow roots have their own Cascade passes and do not apply to the root Cascade.
But along with the popularity of the understanding that shadow DOM means that “styles don’t get in or out” comes the misunderstanding that inheritance still applies normally. Web components need a custom element tag, <my-component>. The element participates in inheritance, and the shadow root inherits from that :host element, so all the shadow DOM elements inherit just like anything else.
That’s what makes CSS custom properties so powerful for web components with shadow DOM. Values for CSS custom properties inherit into shadow DOM HTML and can be a much more straight-forward way of offering configuration options over other approaches like CSS parts. Us web component people usually do CSS custom properties AND CSS Parts because CSS properties can only really apply as the value for individual CSS properties. CSS parts are still CSS, but offer a way to style ALL properties of some element in the shadow DOM without needing JS or weird proprietary syntax like :ng-deep (ewww).
See it in action#
We all love code samples, and I hardly ever write them in my articles. Let’s see how the code highlighting is working today, shall we?
Here is an example of how CSS Custom properties inherit through the shadow DOM and how we web component devs use that to enable easy configuration of our components.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web components with CSS Custom properties</title>
<style>
:root {
/* We can set the CSS custom property for the web component all the way at the root if we want to and it will inherit down */
--card-color: green;
}
/* we can select only all/some instances of the :host element to override the CSS Custom property */
some-comp {
--card-color: red;
}
</style>
</head>
<body>
<!-- we can override the root assignment with an inline style if we want to -->
<some-comp style="--card-color: blue"></some-comp>
<!-- 1. Defining the Web Component -->
<script>
class UserCard extends HTMLElement {
constructor() {
super();
// Attach an open Shadow DOM to isolate styles and markup
const shadow = this.attachShadow({ mode: 'open' });
// Define the internal HTML structure and scoped CSS
shadow.innerHTML = `
<style>
.card p {
/* if --card-color is defined, use it, otherwise use rebeccapurple, but don't set --card-color to a value in shadow root styles */
color: var(--card-color, rebeccapurple);
}
</style>
<div class="card">
<p>Some text in a card</p>
<button id="alert-btn">Follow</button>
</div>
`;
}
}
customElements.define('user-card', UserCard);
</script>
</body>
</html>
If you want more details about how to correctly establish CSS Custom properties for your web components, I have another article that describes the ins and outs.
Closing#
Turns out Inheritance is a super power of web components in CSS and JS. I also wrote an article about the JS version.
If you are creating reusable components, having CSS custom properties that override your defaults will be amazing for your consumers, whether or not you are building web components or framework components. Inheritance is the mechanism that provides web components that configurability. Shadow DOM encapsulation is awesome still, but does not apply to inheritance, and that’s why CSS Custom properties can still get into the shadow root. And wanting to have component-specific CSS properties everywhere for component config but not being able to do it without a fight is one of the reasons that Tailwind sucks.
The End