Back to blog

High-Performance Syntax Highlighting with CSS Highlights API

Pavitra Golchha • Published on  Oct 24, 2025

Tags

The Performance Problem with Traditional Syntax Highlighting

Most syntax highlighters work by wrapping each token (keywords, strings, operators, etc.) in individual <span> elements with CSS classes. For a typical code snippet, this can mean creating hundreds or even thousands of DOM nodes:

<span class="keyword">const</span>
<span class="identifier">greeting</span>
<span class="operator">=</span>
<span class="string">"Hello World"</span>

Each of these nodes adds overhead to the browser’s rendering pipeline—more nodes to parse, more layout calculations, more paint operations, and more memory consumption. For documentation sites or code-heavy applications, this can significantly impact performance.

Enter the CSS Custom Highlight API

The CSS Custom Highlight API provides a way to style arbitrary text ranges without modifying the DOM structure. Instead of creating wrapper elements, you define Range objects that point to specific character positions in text nodes, group them by style type, and register them with the browser’s highlight registry.

Why is it faster?

  • No DOM manipulation: Text remains in a single text node
  • Minimal memory overhead: Ranges are lightweight objects
  • Browser-optimized: The browser handles the painting directly
  • Clean separation: Styling is done purely in CSS

Browser Support

The CSS Custom Highlight API is supported in all modern browsers:

  • Chrome/Edge 105+
  • Firefox 140+
  • Safari 17.2+
  • Opera 91+

While support is excellent, it’s still good practice to include a feature check: if (!CSS.highlights) { /* fallback */ }

Implementation

Here’s a complete implementation of syntax highlighting using the CSS Highlights API:

Step 1: Define CSS Highlight Styles

First, define styles for each token type using the ::highlight() pseudo-element:

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

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

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

::highlight(number) {
  color: #098658;
}

::highlight(operator) {
  color: #000000;
}

::highlight(function) {
  color: #795e26;
}

Step 2: Implement the Highlighting Logic

Here’s the core logic that applies highlighting to a code element:

function applyHighlighting(element: HTMLElement, code: string): () => void {
  // Check for browser support
  if (!CSS.highlights) {
    console.warn('CSS Custom Highlight API not supported')
    return () => {}
  }

  // Get the text node (must be a single text node)
  const textNode = element.firstChild
  if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
    return () => {}
  }

  // Tokenize the code (using your lexer of choice)
  const tokens = lexTypeScript(code)
  
  // Create ranges for each token
  const tokenRanges = tokens.map(token => {
    const range = new Range()
    range.setStart(textNode, token.start)
    range.setEnd(textNode, token.end)
    return { type: token.type, range }
  })
  
  // Group ranges by token type
  const highlightsByType = Map.groupBy(
    tokenRanges, 
    (item: { type: string; range: Range }) => item.type
  )

  // Create highlights and register them
  const createdHighlights = new Map<string, Highlight>()
  
  for (const [type, items] of highlightsByType) {
    const ranges = items.map((item: { type: string; range: Range }) => item.range)
    const highlight = new Highlight(...ranges)
    createdHighlights.set(type, highlight)
    
    // Register with global CSS highlights registry
    const existing = CSS.highlights.get(type)
    if (existing) {
      ranges.forEach(range => existing.add(range))
    } else {
      CSS.highlights.set(type, highlight)
    }
  }

  // Return cleanup function
  return () => {
    for (const [type, highlight] of createdHighlights) {
      const globalHighlight = CSS.highlights.get(type)
      if (globalHighlight) {
        highlight.forEach(range => globalHighlight.delete(range))
        if (globalHighlight.size === 0) {
          CSS.highlights.delete(type)
        }
      }
    }
  }
}

Step 3: Using It in Your Application

Here’s a simple vanilla JavaScript example:

// Create a code viewer element
function createCodeViewer(code, language = 'javascript') {
  const container = document.createElement('div')
  container.style.cssText = `
    position: relative;
    background: #f5f5f5;
    padding: 15px;
    border-radius: 4px;
    font-family: monospace;
    font-size: 14px;
    overflow-x: auto;
    border: 1px solid #e0e0e0;
    white-space: pre;
    line-height: 1.5;
  `
  
  const codeElement = document.createElement('div')
  codeElement.textContent = code
  container.appendChild(codeElement)
  
  // Apply highlighting
  const cleanup = applyHighlighting(codeElement, code)
  
  // Store cleanup function for later
  container._cleanup = cleanup
  
  return container
}

// Usage
const viewer = createCodeViewer(`
const greeting = "Hello, World!"
function sayHello() {
  console.log(greeting)
}
`)

document.body.appendChild(viewer)

// Clean up when removing
// viewer._cleanup()
// viewer.remove()

Or with React:

import { useEffect, useRef } from 'react'

function CodeViewer({ code, language = 'javascript' }) {
  const codeRef = useRef<HTMLDivElement>(null)
  
  useEffect(() => {
    if (!codeRef.current) return
    
    const cleanup = applyHighlighting(codeRef.current, code)
    return cleanup
  }, [code])
  
  return (
    <div style={{
      position: 'relative',
      background: '#f5f5f5',
      padding: '15px',
      borderRadius: '4px',
      fontFamily: 'monospace',
      fontSize: '14px',
      overflowX: 'auto',
      border: '1px solid #e0e0e0',
      whiteSpace: 'pre',
      lineHeight: '1.5'
    }}>
      <div ref={codeRef}>{code}</div>
    </div>
  )
}

Interactive Demo

Try it yourself! Edit the code and customize the syntax highlighting styles in real-time. The demo below has two editors side-by-side: one for your JavaScript code and one for the CSS highlight styles. Watch as your changes update the preview in real-time!

🎨 Meta CSS Editor

Edit CSS highlight styles - this editor styles itself in real-time!

How It Works

  1. Tokenization: Your lexer scans the source code and produces tokens with type information and character positions
  2. Range Creation: For each token, a Range object is created that marks its exact position in the text node
  3. Grouping: Ranges are grouped by token type (keyword, string, comment, etc.)
  4. Highlight Registration: Each group is wrapped in a Highlight object and registered with CSS.highlights using the token type as the key
  5. CSS Styling: The browser applies the styles defined in ::highlight(token-type) rules
  6. Cleanup: When the component unmounts, all ranges are removed from the registry

Key Advantages

  • ⚡ Performance: No DOM mutations mean faster initial render and re-renders
  • 💾 Memory Efficient: Ranges use minimal memory compared to wrapper elements
  • 🧹 Clean HTML: The text remains as a single text node in the DOM
  • 🎨 Pure CSS Styling: All styling is done declaratively in CSS
  • ♻️ Easy Cleanup: Ranges can be added/removed without touching the DOM

Limitations

  • Text Nodes Only: Only works with plain text content
  • Single Text Node: The highlighted element must contain a single text node
  • Static Ranges: Ranges don’t automatically update if text content changes
  • Older Browsers: Requires fallback for browsers older than Chrome 105, Firefox 140, or Safari 17.2

Comparison with Traditional Approach

AspectTraditional (DOM spans)CSS Highlights API
DOM NodesHundreds/thousands1 text node
Memory UsageHighLow
Initial RenderSlowerFaster
Re-renderSlowerFaster
HTML StructureComplexSimple
Browser SupportUniversalModern browsers

Conclusion

The CSS Custom Highlight API is a game-changer for implementing syntax highlighting and other text styling features. By eliminating the need for wrapper DOM elements, it delivers superior performance while keeping your code cleaner and more maintainable.

With excellent browser support across all major browsers, this API is production-ready for most modern web applications. For legacy browser support, you can gracefully fall back to traditional DOM-based highlighting.

The future of text highlighting is here—and it doesn’t need a single <span> tag! 🎨✨