The CSS Highlight Inheritance Model
NOTE: The information here regarding CSS Custom Properties and Highlight Pseudos is out of date. See Custom Properties for Highlight Pseudos
The CSS highlight inheritance model describes the process for inheriting the CSS properties of the various highlight pseudo elements:
::selectioncontrolling the appearance of selected content::spelling-errorcontrolling the appearance of misspelled word markers::grammar-errorcontrolling how grammar errors are marked::target-textcontrolling the appearance of the string matching a target-text URL::highlightdefining the appearance of a named highlight, accessed via the CSS highlight API
The inheritance model described here was proposed and agreed upon in 2022, and is part of the
CSS Pseudo-Elements Module Level 4 Working Draft Specification, but ::selection was implemented and widely used long before this, with browsers
differing in how they implemented inheritance for selection styles.
When this model is implemented and released to users, the inheritance of
CSS properties for the existing pseudo elements, particularly ::selection will change
in a way that may break sites.
The model is implemented behind a flag in Chrome 118 and higher, as is expected to be enabled by default as early as Chrome 123 in February 2024. To see the effects right now, enable “Experimental Web Platform features” via chrome://flags.
::selection, the Original highlight Pseudo #
Historically, ::selection, or even -moz-selection, was the only pseudo element
available to control the highlighting of text, in this case the appearance of selected content.
Sites use ::selection when they want to modify the default browser selection
painting, most often adjusting the color to improve contrast or convey additional
meaning to users. Sites also use the feature to work around selection painting problems in
browsers, with the style .broken::selection { background-color: transparent; } and
a .broken class on elements with broken selection painting. We’ll use this last
situation as the example as we demonstrate the changes to property inheritance.
The ::selection pseudo element as implemented in most browsers uses originating element inheritance, meaning that
the selection inherited properties from the style of the nearest element to which the selection is being
applied. The inheritance behavior of ::selection has never been fully compatible among browsers, and has failed
to comply with the CSS specification even as the spec has been updated over time. As a result,
workarounds are required when a page-wide selection style is inadequate, as demonstrated with
this example (works when
highlight inheritance is not enabled):
<style>
::selection /* = *::selection (universal) */ {
background-color: lightgreen;
}
.broken::selection {
background-color: transparent;
}
</style>
<p>Some <em>not broken</em> text</p>
<p class="broken">Some <em>broken</em> text</p>
<script>
range = new Range();
range.setStart(document.body, 0);
range.setEnd(document.body, 3)
document.getSelection().addRange(range);
</script>
The intent of the page author is probably to have everything inside .broken be invisible for
selection, but that does not happen. The problem here is that ::selection applies to all elements, including the
<em> element. While the .broken element uses its own ::selection, that only applies to the element itself when selected, not its descendants.
You could always add more specific ::selection selectors, such as
.broken >em::selection, but that is brittle to DOM changes and
leads to style bloat.
You could also use a CSS custom property for the page-wide selection color, and just make it transparent for the broken elements, like this (works when highlight inheritance is not enabled):
<style>
:root {
--selection-color: lightgrey;
}
::selection {
background-color: var(--selection-color);
}
.broken {
--selection-color: transparent;
}
</style>
<p>Some <em>not broken</em> text</p>
<p class="broken">Some <em>broken</em> text</p>
<script>
range = new Range();
range.setStart(document.body, 0);
range.setEnd(document.body, 3)
document.getSelection().addRange(range);
</script>
In this case, we define a page-wide selection color as a custom
property referenced in a ::selection rule that matches all elements. The more specific
.broken rule re-defines the custom property to make the selection transparent.
With originating inheritance, the ::selection for the word “broken” inherits the custom property value from the <em> element (it’s originating element),
which in turn inherits the custom property value from the
.broken element.
This custom property workaround is how sites such as GitHub have historically worked around the
problems with originating inheritance for ::selection.
New highlighting Features Change the Trade-Offs #
The question facing the CSS Working Group, the people who set the standards for CSS, was whether to make the specification match the implementations using originating inheritance, or require browsers to change to use the specified highlight inheritance model. The question sat on the back-burner for a while because web developers were not complaining very loudly (there was a workaround once custom properties were implemented) and there was uncertainty around whether the change would break sites. Some new features in CSS changed the calculus.
First came a set of additional CSS highlight pseudo elements that pushed for a
resolution to the question of how they should inherit. Given that the new pseudos were to be implemented according to the spec, while ::selection was not, there was the possibility for ::selection inheritance behavior to be inconsistent with that of the new highlight pseudos:
::target-textfor controlling how URL text-fragments targets are rendered::spelling-errorfor modifying the appearance of browser detected spelling errors::grammar-errorfor modifying browser detected grammar errors::highlightfor defining persistent highlights on a page
In addition, the set of properties allowed in ::selection and other highlight
pseudos was expanded to allow for text decorations and control of fill and stroke colors
on text, and maybe text-shadow. More properties makes the CSS custom property workaround
for originating inheritance unwieldy in practice because it expands the set of variables
needed.
The decision was made to change browsers to match the spec, thus changing the way that
::selection inheritance works in browsers. ::selection was the only widely used highlight pseudo and authors expected it to use originating inheritance, so
any change to the inheritance behavior was likely to cause problems for some sites.
What is highlight inheritance? #
The CSS highlight inheritance model starts with defining a highlight inheritance tree
for each type of highlight pseudo. These trees are parallel to the DOM element tree with the same branching stucture, except the nodes in the tree now represent the highlight styles for each element. The highlight pseudos inherit their styles through the ancestor chain in their highlight inheritance tree, with body::selection inheriting from html::selection and so on.
Under this model, the original example behaves the same because the ::selection rule still matches all elements:
<style>
::selection {
background-color: lightgreen;
}
.broken::selection {
background-color: transparent;
}
</style>
<p>Some <em>not broken</em> text</p>
<p class="broken">Some <em>broken</em> text</p>
However, with highlight inheritance is is recommended that page-wide selection styles be set in :root::selection
or body::selection.
<style>
:root::selection {
background-color: lightgreen;
}
.broken::selection {
background-color: transparent;
}
</style>
<p>Some <em>not broken</em> text</p>
<p class="broken">Some <em>broken</em> text</p>
<script>
range = new Range();
range.setStart(document.body, 0);
range.setEnd(document.body, 3)
document.getSelection().addRange(range);
</script>
The :root::selection is at the root of the highlight inheritance tree, because it is the selection pseudo for the document root element. All selection pseudos in the document now inherit from their parent in the highlight tree, and do not need to match a universal ::selection rule. Furthermore, a change in ::selection matching a particular element will also be inherited by all its descendants.
In this example, there are no rules matching the <em> elements, so they look at
their parent selection pseudo. The “not broken” text will be highlighted in lightgreen inherited from the root via it’s parent chain. The “broken” text receives a transparent background inherited from its parent <p> that matches its own rule.
The Custom Property Workaround Now Fails #
The most significant breakage caused by the change to highlight inheritance is due to sites employing the custom property workaround for originating inheritance. Consider our former example with that workaround:
<style>
:root {
--selection-color: lightgrey;
}
:root::selection {
background-color: var(--selection-color);
}
.broken {
--selection-color: transparent;
}
</style>
<p>Some <em>not broken</em> text</p>
<p class="broken">Some <em>broken</em> text</p>
According to the original specification, and the initial implementation in chromium,
the default selection color is used everywhere in this example because the
custom --selection-color: lightgrey; property is defined on :root
which is the root of the DOM tree but not the highlight tree. The latter is
rooted at :root::selection which does not define the property.
Many sites use :root as the location for CSS custom properties. Upvoted
answers on Stack Overflow explicitly recommend doing so for selection colors.
Unsurprisingly, many sites broke when chromium initially enabled highlight inheritance,
including GitHub. To fix this, the specification was changed to require that
:root::selection inherit custom properties from :root.
But all is not well. The <em> element’s ::selection has its background set to the custom property value, which is now inherited from :root via :root::selection. But because
the inheritance chain does not involve .broken at all,
the custom property is evaluated against the :root value. To make this work, the
custom property must be overridden in a .broken::selection rule:
:root {
--selection-color: lightgrey;
}
:root::selection {
background-color: var(--selection-color);
}
.broken::selection {
--selection-color: transparent;
background-color: var(--selection-color);
}
Note that there’s really no point using custom properties overrides for highlights,
unless you desire different ::selection styles in different parts of the document and want them to use a common set of property values that you would define on the root.
The Current Situation (at the time of writing) #
While writing this, highlight inheritance is used for all highlight pseudos except ::selection
and ::target-text in chromium-based browsers (other browsers are still implementing the additional
pseudo elements). Highlight inheritance is also enabled for ::selection and ::target-text for
users that have “Experimental Web Platform features” enabled in chrome::flags in Chrome.
Chrome is trying to enable highlight inheritance for ::selection. One attempt to enable for all
users was rolled back, and another attempt will be made in an upcoming release. The current plan is to
iterate until the change sticks. That is, until site authors notice that things have changed and make suitable
site changes.
If inertia is too strong, additional changes to the spec may be needed to support, in particular, the continued use of the existing custom property workaround.
Thanks #
The implementation of highlight inheritance in chromium was undertaken by Igalia S.L. funded by Bloomberg L.P. Delan Azabani did most of the work, with contributions from myself.