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:
::selection
controlling the appearance of selected content::spelling-error
controlling the appearance of misspelled word markers::grammar-error
controlling how grammar errors are marked::target-text
controlling the appearance of the string matching a target-text URL::highlight
defining 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-text
for controlling how URL text-fragments targets are rendered::spelling-error
for modifying the appearance of browser detected spelling errors::grammar-error
for modifying browser detected grammar errors::highlight
for 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.