Why highlight pseudo-elements matter now

For years, styling selected or found text in the browser was a black box. You could ::selection and that was basically it. Everything else — search matches, spell-check squiggles, target-text jumps — was locked behind vendor internals.

The CSS Custom Highlight API changed that. Combined with pseudo-elements like ::search-text, ::highlight(), ::target-text, ::spelling-error, and ::grammar-error, you can now target text ranges that were never wrapped in an element, and style them with plain CSS.

This shifts a whole class of UI problems — search result highlighting, reader modes, annotation tools, collaborative editors — from "hack with <span> injection" to "register a Range, style a pseudo-element". If you've ever written a mark.js wrapper just to color a search hit, this is the moment to reconsider your stack. For a broader look at how the platform is evolving around text rendering, see this deep dive into CSS highlight pseudo-elements.

Developer inspecting CSS Custom Highlight API pseudo-elements in browser DevTools Programming Illustration

The pseudo-elements you need to know

Here's the current landscape, from most stable to most experimental:

Pseudo-elementPurposeSupport
::selectionUser-selected textUniversal
::target-textText scrolled to via URL fragmentChrome, Safari
::search-textBrowser's find-in-page matchesChromium
::spelling-error / ::grammar-errorNative spell/grammar markersChromium
::highlight(name)Custom ranges registered via JSChrome 105+, Safari 17.2+

Styling find-in-page results

::search-text is the one people have wanted for a decade. Browser find-in-page highlights used to be untouched by CSS. Now:

/* Default search highlight */
::search-text {
  background: #ffe066;
  color: #1a1a1a;
}

/* The currently focused match (the one Enter jumps to) */
::search-text:current {
  background: #ff7043;
  color: #fff;
  outline: 2px solid #ff7043;
}

Custom Highlight API in practice

The real power move is registering your own ranges. No DOM mutation, no <mark> tags, no reflow storms on large documents.

// 1. Find the range you want to highlight
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
const ranges = [];
let node;
while ((node = walker.nextNode())) {
  const idx = node.textContent.indexOf('pseudo-element');
  if (idx !== -1) {
    const range = new Range();
    range.setStart(node, idx);
    range.setEnd(node, idx + 'pseudo-element'.length);
    ranges.push(range);
  }
}

// 2. Register them under a named highlight
const highlight = new Highlight(...ranges);
CSS.highlights.set('search-term', highlight);
/* 3. Style the named highlight */
::highlight(search-term) {
  background-color: #fff3a0;
  text-decoration: underline wavy #e67e22;
}

That's it. No wrapper elements, no cleanup when the search term changes — just CSS.highlights.clear() and re-register.

The MicroLighter pattern

A neat trick that's been circulating: build a syntax highlighter entirely on top of the Custom Highlight API. Instead of emitting <span class="keyword"> for every token, you tokenize the text, build Range objects, and register them under highlights like keyword, string, comment. The CSS then does the coloring. This keeps the DOM tiny and makes re-highlighting on theme switch basically free.

Frontend engineer styling search-text highlight with CSS pseudo-elements on a code editor Development Concept Image

Caveats, limits, and what to watch out for

A few things nobody mentions in the happy-path demos:

  • Only a subset of CSS applies. Highlight pseudo-elements are limited to a small set of properties: color, background-color, text-decoration and its longhands, text-shadow, and a few others. You cannot set padding, border, font-size, or display. If you need a box around a match, this is not the tool.
  • No ::before / ::after. You can't inject content into a highlight. It's styling only.
  • Ranges must be live. If the DOM changes, your ranges may become invalid and silently stop rendering. Re-register on mutation if your content is dynamic.
  • ::search-text is Chromium-only right now. Firefox and Safari haven't shipped it. Feature-detect with CSS.supports('selector(::search-text)') and fall back gracefully.
  • Accessibility: highlights are visual only. Screen readers won't announce them. If the highlight conveys meaning (e.g., search matches), replicate it in ARIA live regions or the accessible name.

Progressive enhancement recipe

/* Baseline: works everywhere */
mark.search-hit {
  background: #ffe066;
}

/* Upgrade: no DOM wrappers needed */
@supports selector(::highlight(search-term)) {
  ::highlight(search-term) {
    background: #ffe066;
  }
}

Ship the <mark> fallback, layer the highlight on top when supported, and delete the fallback when Baseline catches up. For a parallel story of how a platform-level API reshaped an entire stack, the WhatsApp Rust scaling case study is worth a read — same pattern of "new primitive unlocks a simpler architecture".

Where to go next

  1. Read the CSS Custom Highlight API spec — it's short and readable.
  2. Try ::target-text for deep-link highlighting in documentation sites.
  3. Build a small annotation tool: select text with window.getSelection(), convert to a Range, register a highlight. You'll learn the whole API in an afternoon.
  4. Watch the ::search-text and ::spelling-error specs — as they land in more engines, the "wrap in a span" era officially ends.

Web developer reviewing MicroLighter syntax highlighter built with CSS Custom Highlight API Software Concept Art

The bottom line

CSS highlight pseudo-elements are one of those quiet platform additions that remove entire categories of JavaScript. If your codebase still injects <mark> tags for search results, or ships a 40KB highlighting library just to color tokens, the Custom Highlight API is worth a serious look this quarter.

Start with ::highlight() for your own ranges, layer ::search-text and ::target-text where supported, and keep a <mark> fallback for the long tail. The DOM you don't create is the DOM you don't have to clean up.

This content was drafted using AI tools based on reliable sources, and has been reviewed by our editorial team before publication. It is not intended to replace professional advice.