High-Performance Syntax Highlighting with CSS Highlights API
Pavitra Golchha • Published on Oct 24, 2025
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
- Tokenization: Your lexer scans the source code and produces tokens with type information and character positions
- Range Creation: For each token, a
Rangeobject is created that marks its exact position in the text node - Grouping: Ranges are grouped by token type (keyword, string, comment, etc.)
- Highlight Registration: Each group is wrapped in a
Highlightobject and registered withCSS.highlightsusing the token type as the key - CSS Styling: The browser applies the styles defined in
::highlight(token-type)rules - 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
| Aspect | Traditional (DOM spans) | CSS Highlights API |
|---|---|---|
| DOM Nodes | Hundreds/thousands | 1 text node |
| Memory Usage | High | Low |
| Initial Render | Slower | Faster |
| Re-render | Slower | Faster |
| HTML Structure | Complex | Simple |
| Browser Support | Universal | Modern 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! 🎨✨