We all have a few skeletons in our CSS closets. There’s probably that one-off !important where you can now manage that more effectively with cascade layers. Or maybe a dated Checkbox Hack that :has() has solved. Perhaps it’s been a long while since your last site redesign and it’s chock-full of vendor-prefixed properties from 2012. Thar be demons!
Stu Robson’s ReliCSS (clever name!) tool can excavate outdated CSS in your codebase that have modern CSS solutions.
Each relic is assigned a level of severity. As Stu explains it:
High Severity: True “fossils”. Hacks for (now) unsupported browsers (IE6/7) or “dangerous” techniques. High-risk, obsolete, should be first targets for removal.
Medium Severity: The middle ground. Hacks for older unsupported browsers (IE8-10). They work but they’re fragile. Hacks to review to see if they’re still relevant for your actual users.
Low Severity: Modern artifacts. Usually vendor prefixes (-webkit-, -moz-). Safe mostly, but better handled by automated tools like Autoprefixer. They’re an opportunity to improve your build process.
It’s been a little while since my personal site got an overhaul. Not to toot my own horn, but heyyyyyy!
Seriously, though. I know there are things in there I’m embarrassed to admit.
But what if we do archeological dig on CSS-Tricks? I mean, it’s been at least five years since this place has gotten the love it deserves. I’m almost afraid to look. Here goes…
🫣
OK, not as bad as I imagined. It’s largely vendor prefixing, which I’m sure comes courtesy of an older Autoprefixer configuration.
Chrome 144 recently shipped ::search-text, which is now one of several highlight-related pseudo-elements. This one selects find-in-page text, which is the text that gets highlighted when you do a Ctrl/Command + F-type search for something on a page and matches are found.
By default, ::search-text matches are yellow while the current target (::search-text:current) is orange, but ::search-text enables us to change that.
I’ll admit, I hadn’t really been following these highlight pseudo-elements. Up until now, I didn’t even know that there was a name for them, but I’m glad there is because that makes it easier to round them all up and compare them, which is exactly what I’m going to do here today, as it’s not super obvious what they do based on the name of the pseudo-element. I’ll also explain why we’re able to customize them, and suggest how.
Text fragments allow for programmatic highlighting using URL parameters. If you’re referred to a website by a search engine, it might use text fragments, which is why ::target-text is easily confused with ::search-text.
And let’s not forget about the <mark> HTML element either, which is what I’m using in the demos below.
What should highlight pseudo-elements look like?
The question is, if they all (besides ::highlight()) have default styling, why would we need to select them with pseudo-elements? The reason is accessibility (color contrast, specifically) and usability (emphasis). For example, if the default yellow background of ::search-text doesn’t contrast well enough with the text color, or if it doesn’t stand out against the background of the container, then you’ll want to change that.
I’m sure there are many ways to solve this (I want to hear “challenge accepted” in the comments), but the best solution that I’ve come up with uses relative color syntax. I took wrong turns with both background-clip: text and backdrop-filter: invert(1) before realizing that many CSS properties are off-limits when it comes to highlight pseudo-elements:
body {
--background: #38003c;
background: var(--background);
mark,
::selection,
::target-text,
::search-text {
/* Match color to background */
color: var(--background);
/* Convert to RGB then subtract channel value from channel maximum (255) */
background: rgb(from var(--background) calc(255 - r) calc(255 - g) calc(255 - b));
}
}
Your browser might not support that yet, so here’s a video that shows how the highlighted text adapts to background color changes.
What’s happening here is that I’m converting the container’s background color to RGB format and then subtracting the value of each channel (r, g, and b) from the maximum channel value of 255, inverting each channel and the overall color. This color is then set as the background color of the highlighting, ensuring that it stands out no matter what, and thanks to the new CodePen slideVars, you can mess around with the demo to see this in action. You might be able to do this with color formats besides RGB, but RGB is the easiest.
So that covers the usability, but what about the accessibility?
Well, the highlighting’s text color is the same as the container’s background color because we know that it’s the inverse of the highlighting’s background color. While this doesn’t mean that the two colors will have accessible contrast, it seems as though they will most of the time (you should always check color contrast using color contrast tools, regardless).
If you don’t like the randomness of inverting colors, that’s understandable. You can totally pick colors and write conditional CSS for them manually instead, but finding accessible colors that stand out against the different backdrops of your design for all of the different types of highlight pseudo-elements, while accounting for alternative viewing modes such as dark mode, is a headache. Besides, I think certain UI elements (e.g., highlights, errors, focus indicators) should be ugly. They should stand out in a brutalist sort of way and feel disconnected from the design’s color palette. They should demand maximum attention by intentionally not fitting in.
Keep in mind that the different types of highlight pseudo-elements should be visually distinctive too, for obvious reasons, but also in case two different types overlap each other (e.g., the user selects text currently matched by find-in-page). Therefore, in the amended code snippet below, mark, ::selection, ::target-text, and ::search-text all have slightly different backgrounds.
I’ve left mark unchanged, the r value of ::selection as it was, the g value of ::target-text as it was, and the b value of ::search-text as it was, so those last three only have two channels inverted instead of all three. They’re varied in color now (but still look inverted), and with the addition of an alpha value at 70% (100% for ::search-text:current), they also blend into each other so that we can see where each highlight begins and ends:
body {
--background: #38003c;
background: var(--background);
mark,
::selection,
::target-text,
::search-text {
color: var(--background);
}
mark {
/* Invert all channels */
background: rgb(from var(--background) calc(255 - r) calc(255 - g) calc(255 - b) / 70%);
}
::selection {
/* Invert all channels but R */
background: rgb(from var(--background) r calc(255 - g) calc(255 - b) / 70%);
}
::target-text {
/* Invert all channels but G */
background: rgb(from var(--background) calc(255 - r) g calc(255 - b) / 70%);
}
::search-text {
/* Invert all channels but B */
background: rgb(from var(--background) calc(255 - r) calc(255 - g) b / 70%);
&:current {
/* Invert all channels but B, but without transparency */
background: rgb(from var(--background) calc(255 - r) calc(255 - g) b / 100%);
}
}
}
::spelling-error and ::grammar-error are excluded from all this because they have their own visual affordances (red underlines and green underlines respectively, typically contrasted against the neutral background of an editable element such as <textarea>).
But mark, ::selection, ::target-text, and new-to-Chrome ::search-text? Well, they can appear anywhere (even on top of each other), so I think it’s important that they’re visually distinctive from each other while being accessible at all times. Again though, even fully-inverted colors can be inaccessible. In fact, the inverse of #808080 is #808080, so test, test, test! Although, maybe contrast-color() could come to the rescue once the CSS Color Module Level 5 version of it ships.
In the meantime, please, no more highlight-y elements!
I was building a Modal component that uses the <dialog> element’s showModal method. While testing the component, I discovered I could tab out of the <dialog> (in modal mode) and onto the address bar.
And I was surprised — accessibility advice around modals have commonly taught us to trap focus within the modal. So this seems wrong to me.
Upon further research, it seems like we no longer need to trap focus within the <dialog> (even in modal mode). So, the focus-trapping is deprecated advice if you use <dialog>.
Some notes for you
Instead of asking you to read through the entire GitHub Issue detailing the discussion, I summarized a couple of key points from notable people below.
Here are some comments from Scott O’Hara that tells us about the history and context of the focus-trapping advice:
WCAG is not normatively stating focus must be trapped within a dialog. Rather, the normative WCAG spec makes zero mention of requirements for focus behavior in a dialog.
The informative 2.4.3 focus order understanding docdoes talk about limiting focus behavior within a dialog – but again, this is in the context of a scripted custom dialog and was written long before inert or <dialog> were widely available.
The purpose of the APG is to demonstrate how to use ARIA. And, without using native HTML features like <dialog> or inert, it is far easier to trap focus within the custom dialog than it is to achieve the behavior that the <dialog> element has.
Both the APG modal dialog and the WCAG understanding doc were written long before the inert attribute or the <dialog> element were widely supported. And, the alternative to instructing developers to trap focus in the dialog would have been to tell them that they needed to ensure that all focusable elements in the web page, outside of the modal dialog, received a tabindex=-1.
In the page context you can choose to Tab out of the bottom and around the browser chrome, you can use a keyboard command to move straight to the address bar or open a particular menu, you can close the tab, and so on. This gives people a choice about how, why, and what they do to escape out of the context.
It seems logical (to me at least) for the same options to be available to people when in a dialog context instead of a page context.
Finally, Matatk shared the conclusion from the W3C’s Accessible Platform Architectures (APA) Working Group that okay-ed the notion that <dialog>‘s showModal method doesn’t need to trap focus.
We addressed this question in the course of several APA meetings and came to the conclusion that the current behavior of the native dialog element should be kept as it is. So, that you can tab from the dialog to the browser functionalities.
We see especially the benefit that keyboard users can, for example, open a new tab to look something important up or to change a browser setting this way. At the same time, the dialog element thus provides an additional natural escape mechanism (i.e. moving to the address bar) in, for example, kiosk situations where the user cannot use other standard keyboard shortcuts.
From what I’m reading, it sounds like we don’t have to worry about focus trapping if we’re properly using the Dialog API’s showModal method!
Hope this news make it easier for you to build components. 😉