Skip to content
👉All tips here

CSS Highlights API for Syntax Highlighting

CSS highlighting API syntax highlighter

Traditional syntax highlighters wrap every keyword, string, and operator in separate <span> elements. A 20-line code snippet can easily generate 200+ DOM nodes, slowing down your page. The CSS Highlights API changes this. Instead of wrapping text in elements, it styles text ranges directly—keeping everything as a single text node.

Real impact: A documentation site with 50 code blocks can have 10,000+ spans in the DOM. With CSS Highlights API, that drops to just 50 text nodes resulting in 2-3x faster initial render and significantly lower memory usage.

There’s a better way!

The Problem with DOM-Heavy Highlighting

Popular libraries like Prism.js, Highlight.js, and Shiki all use the same approach: they wrap each token in <span> elements. Even modern tools like Shiki (used by VS Code’s website) generate heavy DOM structures.

Here’s what they produce:

<span class="keyword">const</span>
<span class="identifier">name</span>
<span class="operator">=</span>
<span class="string">"Alice"</span>

Each <span> adds overhead: more parsing, more layout calculations, more memory. On documentation sites with dozens of code blocks, this becomes a real bottleneck.

The Solution: CSS Highlights API

Instead of wrapping text in elements, this API lets you style text ranges directly. The text stays as a single text node, the browser handles the styling.

Browser support: Chrome 105+, Firefox 140+, Safari 17.2+

CSS Highlights API Implementation

1. Define your styles

::highlight(keyword) {
  color: #0000ff;
  font-weight: bold;
}

::highlight(string) {
  color: #a31515;
}

::highlight(comment) {
  color: #008000;
  font-style: italic;
}

2. Apply Highlighting

function highlightCode(element, code) {
  if (!CSS.highlights) return () => {};

  const textNode = element.firstChild;
  if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
    return () => {};
  }

  // Tokenize your code
  // Tokenize your code (simple regex example)
  // In production, use a proper lexer for your language
  const tokens = tokenize(code);
  
  // Create ranges for each token type
  const rangesByType = new Map();
  
  tokens.forEach(token => {
    const range = new Range();
    range.setStart(textNode, token.start);
    range.setEnd(textNode, token.end);
    
    if (!rangesByType.has(token.type)) {
      rangesByType.set(token.type, []);
    }
    rangesByType.get(token.type).push(range);
  });

  // Register highlights
  for (const [type, ranges] of rangesByType) {
    const highlight = new Highlight(...ranges);
    CSS.highlights.set(type, highlight);
  }

  // Return cleanup function
  return () => {
    for (const type of rangesByType.keys()) {
      CSS.highlights.delete(type);
    }
  };
}

3. Use it

Vanilla JavaScript

const pre = document.querySelector('pre');
const cleanup = highlightCode(pre, pre.textContent);

or React:

function CodeBlock({ code }) {
  const ref = useRef(null);
  
  useEffect(() => {
    if (!ref.current) return;
    return highlightCode(ref.current, code);
  }, [code]);
  
  return <pre ref={ref}>{code}</pre>;
}

Live Demo

See the difference in action, both approaches look identical, but check the DOM node count:

Why is faster?

  • No DOM manipulation: Text remains a single node
  • Less memory: Ranges are lightweight
  • Browser-optimized: Native rendering pipeline
  • Clean HTML: Inspect your code blocks—just text!

When NOT to Use This

Skip the CSS Highlights API if:

  • You need older browser support (pre-2023 browsers like Chrome 104 or Safari 17.1)
  • You’re already using a mature library (Prism, Highlight.js, Shiki) and don’t have performance issues
  • Your code blocks have interactive inline elements (copy buttons, tooltips within the syntax)
  • You need contenteditable code with live highlighting as users type

CSS Highlights API Accessibility Benefits

Beyond performance, the CSS Highlights API is better for accessibility:

Screen readers: With traditional highlighters, screen readers announce every <span> element, creating verbose output like “keyword const, identifier name, operator equals, string Alice”. With the Highlights API, screen readers read the code naturally as plain text: “const name equals Alice”.

Keyboard navigation: Fewer DOM nodes mean simpler tab navigation and fewer tab stops when users navigate through the page.

Copy/paste: Users get clean, unformatted text when copying code. No need to strip out wrapper elements or worry about accidentally copying HTML structure.

The text remains semantically pure, it’s just styled visually without altering its structure or meaning.

For more web accessibility tips check my post here.

The Bottom Line

If you’re building a docs site, code playground, or any app with lots of syntax highlighting, the CSS Custom Highlight API is a no-brainer. It’s faster, uses less memory, and keeps your DOM clean.

Feature detection is simple:

if (CSS.highlights) {
  // Use highlights API
} else {
  // Fallback to traditional approach
}

No more <span> soup. Just fast, clean highlighting.

Worth noting: Most syntax highlighting libraries (Prism, Highlight.js, Shiki) haven’t adopted this API yet. There’s an opportunity here, either build your own lightweight highlighter of course depending on the needs for your project, or wait for these tools to catch up. The performance gains are too good to ignore.