Isolating CSS Inheritance
I was recently looking into CSS isolation between multiple applications running on the same web page and it turned into a deep dive of the CSS initial and revert keywords which I wanted to share in a short post on the subject (short for my blog anyways).
The Problem
I was tasked with running multiple web applications on the same page at the same time. These applications are decoupled from one another, developed by different teams, deployed independently at different times, and built from different commits. Effectively a microfrontend deployment.
Given this setup, how can we ensure that internal CSS styling from one app does not unexpectedly leak into the other? To make this even more complicated, this particular use case allows one application to be rendered inside another, meaning CSS properties may inherit across applications.
The team building the "parent" application might write:
<div id="parent-root" style="color: red;">
<div>I'm red, just like I expected!</div>
${renderChildApp()}
</div>
And then the team maintaining the "child" application might write:
<div id="child-root">
<div>I expect to default to black.</div>
<div>
In isolation, both of these look perfectly reasonable. But when rendered together, we can see:
<!-- Parent application -->
<div id="parent-root" style="color: red;">
<div>I'm red, just like I expected!</div>
<!-- Child application -->
<div id="child-root">
<div>I'm red? I expected to default to black.</div>
</div>
</div>
Because these apps are independently built and deployed, they should be generally isolated from each other, and arbitrary internal changes in one app (such as text color) should not affect the other.
However in this case, the parent's color: red; style is inherited into the child app and colors its text. This style is effectively leaked across applications.
Now there are numerous ways styles can leak from application to another, all of which require unique solutions. But for this post, we'll just focus on CSS inheritance. So what can we do to isolate these two apps from each other?
Also if your first thought was to reach for shadow DOM or @scope, I'm afraid neither are viable alternatives for this particular problem due to the focus on CSS inheritance specifically (explanation of shadow DOM and @scope insufficiencies).
Overrides
The immediately obvious answer is to override color: black; on the child app root.
color.<div id="parent-root" style="color: red;">
<div id="child-root" style="color: black;">
<div>I'm black!</div>
</div>
</div>
But listing out every CSS property and its default value is quite complicated and tedious.
all
Fortunately, CSS has a useful property for this very problem. all applies a single value to all CSS properties (with some minor exceptions) for a particular element.
Each CSS property has a different type, meaning there is no specific value you can provide which is meaningful to every property. But there a few CSS base keywords you can use which do apply to every property.
So which of these would be appropriate in this scenario? A few can be immediately excluded.
inherit explicitly opts in to the inheritance behavior we don't want.
unset is equivalent to inherit when applied to inherited properties, and equivalent to initial for non-inherited properties. Again, this explicitly opts in to inheritance we're trying to avoid. And if the initial behavior is what we want, we should just use that directly.
revert-layer lets you roll back styles from an @layer. This is actually closer to workable than you might initially think, but ultimately does not fit this use case.
So that leaves us with initial and revert as potential candidates. Let's look at each.
initial
The
initialCSS keyword applies the initial (or default) value of a property to an element.
Every CSS property has a default value and setting it to initial uses the default value for its associated property.
Let's look at display as an example. The default value of display according to the CSS spec is inline. So setting display: initial; is equivalent to display: inline;.
You might be surprised to hear that inline is the default and not block, however that's likely because you're thinking of div, which is an exception we'll get to shortly. Create an element with an arbitrary tag name like my-random-element and check DevTools. You can confirm the default is indeed display: inline;. This is the reason so many components include :host { display: block; } in their styles.
In the context of all: initial;, we're using the default value for every property as specified by the CSS standard.
revert
So what about revert?
The
revertCSS keyword reverts the cascaded value of the property from its current value to the value the property would have had if no changes had been made by the current style origin to the current element. Thus, it resets the property either to user agent set value, to user set value, to its inherited value (if it is inheritable), or to initial value.
And that's a little more involved... Let me try and break this down a bit.
This description mentions a "user agent set value", and to understand that, we need to first discuss the user-agent stylesheet.
Style Origins
The first definition to understand here is "style origin". This refers to the different locations CSS can come from.
if no changes had been made by the current style origin to the current element
This is talking about styles originating from the current document, basically all the CSS you think of as your application's styles, anything in a <style> or <link rel="stylesheet"> you've inserted into the page. revert pretends none of these application styles exist and instead reverts to...
The User-Agent Stylesheet
Every browser includes a "user-agent stylesheet" (UA stylesheet), this is an additional set of CSS styles applied to all web pages with some browser-specific styling.
This stylesheet is why a plain <button> element with no CSS looks just a little different between browsers in order to match the system UI users expect on their particular platform. For example, consider the visual design of a default button on desktop Windows vs mobile iOS. Those differences are managed by the UA stylesheet.
These UA styles are literally defined as a stylesheet, and you can observe it in DevTools or by just looking at browser source code. This is also how the display exception for div exception gets handled. Browsers include in their user-agent stylesheet:
div {
display: block;
}
The UA stylesheet is why div elements always default to block, even though the initial value of display is actually inline.
There is also a "user" style origin where browsers and extensions allow you to apply your own stylesheet to every page you visit.
In building our microfrontend web application, we want to respect the user's intent and configured settings. If they have configured their browser with something like h1 { color: magenta; } because they like their headers to be really bright and colorful, then more power to them, and our website should follow that preference.
Stepping back to revert, recall that it resets to the "user agent set value", meaning it resets a CSS property to its default after taking the user-agent stylesheet into account.
We can demonstrate the difference with initial using the div example:
display: initial; vs display: revert;. <!-- Two elements stack horizontally because they're `inline`. -->
<div style="display: initial;">I'm <code>inline</code>!</div>
<div style="display: initial;">I'm to the side!</div>
<br><br>
<!-- Two elements stack vertically because they're `block`. -->
<div style="display: revert;">I'm <code>block</code>!</div>
<div style="display: revert;">I'm underneath!</div>
inline!block!Based on this description and our desire to respect the user-agent stylesheet, it seems like the obvious answer to isolating CSS inheritance is all: revert; rather than all: initial;, right?