Modern, zero-dependency component for highlighting text using CSS Custom Highlight API
- Vanilla JS Demo - Pure JavaScript implementation (no framework)
- React Storybook - Interactive examples with all features
- Codesandbox demo - basic use cases
- 🚀 Blazing Fast - No DOM mutiations! Uses
TreeWalkerfor efficient DOM traversal (500× faster than naive approaches) - 🎯 Non-Invasive - Zero impact on your DOM structure or React component tree. The DOM is not mutated.
- ⚡ Non-Blocking - Uses
requestIdleCallbackto prevent UI freezes during search operations - 🎨 Fully Customizable - Control highlights colors with simple CSS variables
- 🔄 Multi-Term Support - Highlight multiple search terms simultaneously with different styles
- 📦 Zero Dependencies - Pure React + Modern Browser APIs
- 🧩 Multiple Usage Patterns - React (ref-based/wrapper/hook) or vanilla JS (framework-agnostic)
- 🌐 TypeScript First - Full type safety with extensive JSDoc documentation
- 🔌 Framework Agnostic - Use with React, Vue, Svelte, Angular, or vanilla JavaScript
- 🏗️ Clean Architecture - React hook is a thin wrapper around framework-agnostic core
- Installation
- Quick Start
- Usage Patterns
- API Reference
- Styling
- Performance
- Browser Support
- Advanced Examples
- Best Practices
- Troubleshooting
- Contributing
Install via npm:
npm install react-css-highlightOr using pnpm:
pnpm add react-css-highlightOr using yarn:
yarn add react-css-highlightimport { useRef } from "react";
import Highlight from "react-css-highlight";
function SearchResults() {
const contentRef = useRef<HTMLDivElement>(null);
return (
<>
<Highlight search="React" targetRef={contentRef} />
<div ref={contentRef}>
<p>React is a JavaScript library for building user interfaces.</p>
<p>React makes it painless to create interactive UIs.</p>
</div>
</>
);
}Result: All instances of "React" will be highlighted with a yellow background.
This library can be used in React or vanilla JavaScript (Vue, Svelte, Angular, etc.).
There are three ways to use this library in React, each suited for different scenarios:
Use when:
- Multiple highlights on the same content
- Working with portals or complex layouts
- Need to highlight existing components
- Want zero performance overhead
import { useRef } from "react";
import Highlight from "react-css-highlight";
function AdvancedSearch() {
const contentRef = useRef<HTMLDivElement>(null);
return (
<>
{/* Multiple highlights with different styles */}
<Highlight
search="error"
targetRef={contentRef}
highlightName="highlight-error"
/>
<Highlight
search="warning"
targetRef={contentRef}
highlightName="highlight-warning"
/>
<div ref={contentRef}>
<p>Error: Connection failed</p>
<p>Warning: High memory usage</p>
</div>
</>
);
}Use when:
- Simple, single highlight needed
- Content is self-contained
- Want cleaner, simpler code
⚠️ Important: The child element must be a single React element that accepts arefprop. DOM elements (div, section, article, etc.) and most React components support this natively.
import { HighlightWrapper } from "@/components/general/Highlight";
function SimpleSearch() {
return (
<HighlightWrapper search="important">
<div>
<p>This is an important message about important topics.</p>
</div>
</HighlightWrapper>
);
}Valid children:
// ✅ DOM elements with ref support
<HighlightWrapper search="term"><div>Content</div></HighlightWrapper>
<HighlightWrapper search="term"><article>Content</article></HighlightWrapper>
<HighlightWrapper search="term"><section>Content</section></HighlightWrapper>
// ✅ Custom components with forwardRef (or ref prop in React 19)
const MyComponent = forwardRef((props, ref) => <div ref={ref} {...props} />);
<HighlightWrapper search="term"><MyComponent>Content</MyComponent></HighlightWrapper>
// ❌ Multiple elements
<HighlightWrapper search="term">
<div>First</div>
<div>Second</div> {/* Error: must be single element */}
</HighlightWrapper>
// ❌ Non-element children
<HighlightWrapper search="term">
Just plain text {/* Error: not a React element */}
</HighlightWrapper>When these requirements aren't met, use the Component (Ref-Based) pattern instead.
Use when:
- Building custom components or abstractions
- Need direct access to match count, error state, or browser support
- Want to control the entire render logic
- Integrating with complex state management
The useHighlight hook provides the same functionality as the Highlight component, but gives you direct access to the highlight state.
⚠️ Important: When using the hook directly, you must import the CSS file somewhere in your project (typically in your main entry file or root component):import "react-css-highlight/dist/Highlight.css";This only needs to be imported once per project, not in every file that uses the hook.
import { useRef } from "react";
import { useHighlight } from "react-css-highlight";
// Note: CSS should be imported once in your app's entry point, not here
function CustomHighlightComponent() {
const contentRef = useRef<HTMLDivElement>(null);
const { matchCount, isSupported, error } = useHighlight({
search: "React",
targetRef: contentRef,
highlightName: "highlight",
caseSensitive: false,
wholeWord: false,
maxHighlights: 1000,
debounce: 100,
onHighlightChange: (count) => console.log(`Found ${count} matches`),
onError: (err) => console.error("Highlight error:", err),
});
return (
<div>
{!isSupported && (
<div className="warning">
Your browser doesn't support CSS Custom Highlight API
</div>
)}
{error && (
<div className="error">
Error: {error.message}
</div>
)}
<div className="match-count">
Found {matchCount} matches
</div>
<div ref={contentRef}>
<p>React is a JavaScript library for building user interfaces.</p>
<p>React makes it painless to create interactive UIs.</p>
</div>
</div>
);
}Hook Return Value:
| Property | Type | Description |
|---|---|---|
matchCount |
number |
Number of highlighted matches found |
isSupported |
boolean |
Whether the browser supports CSS Custom Highlight API |
error |
Error | null |
Error object if highlighting failed, null otherwise |
refresh |
`(search?: string | string[]) => void` |
When to use the hook vs component:
- Use the component when you just need highlighting without additional UI logic
- Use the hook when you need to:
- Display match counts in your UI
- Show error messages to users
- Conditionally render UI based on browser support
- Build complex components that need highlight state
- Integrate with form state or other React state management
- Manually control re-highlighting for dynamic content (virtualized lists, infinite scroll, etc.)
This library also provides a framework-agnostic API for use with Vue, Svelte, Angular, or vanilla JavaScript. Import from react-css-highlight/vanilla to use the createHighlight() function and utility APIs without React dependencies.
Requirements: This package uses ES modules and requires a bundler (Vite, Webpack, Esbuild) or module system. It does not support plain
<script>tags without a build tool.
📖 See Vanilla JS Documentation →
The useHighlight hook accepts the same options as the Highlight component and returns highlight state.
Note: When using the hook directly, you must import the CSS file once in your project:
// In your main.tsx, App.tsx, or _app.tsx import "react-css-highlight/dist/Highlight.css";This is not needed when using the
HighlightorHighlightWrappercomponents, as they import it automatically.
Parameters: Same as Highlight Component Props
Returns: UseHighlightResult
import { useHighlight } from "react-css-highlight";
// CSS already imported in main entry file
const { matchCount, isSupported, error } = useHighlight({
search: "term",
targetRef: contentRef,
highlightName: "highlight",
caseSensitive: false,
wholeWord: false,
maxHighlights: 1000,
debounce: 100,
onHighlightChange: (count) => {},
onError: (err) => {},
});| Property | Type | Description |
|---|---|---|
matchCount |
number |
Number of matches currently highlighted (updates synchronously) |
isSupported |
boolean |
Whether browser supports CSS Custom Highlight API |
error |
Error | null |
Error object if highlighting failed, null otherwise |
refresh |
(search?: string | string[]) => void |
Manually trigger re-highlighting. Optionally pass search term(s) to temporarily highlight different content without updating component state |
| Prop | Type | Default | Description |
|---|---|---|---|
search |
string | string[] |
required | Text to highlight (supports multiple terms). If array is passed, make sure it is memoed |
targetRef |
RefObject<HTMLElement | null> |
required | Ref to the element to search within |
highlightName |
string |
"highlight" |
CSS highlight name (use predefined styles from Highlight.css) |
caseSensitive |
boolean |
false |
Case-sensitive search |
wholeWord |
boolean |
false |
Match whole words only |
maxHighlights |
number |
1000 |
Maximum highlights (performance limit) |
debounce |
number |
100 |
Debounce delay in ms before updating highlights |
ignoredTags |
string[] |
undefeind |
HTML tags names whose text content should not be highlighted. These are merged with the default list of contentless ignored tags which is defined within the constants file |
onHighlightChange |
(count: number) => void |
undefined |
Callback when highlights update |
onError |
(error: Error) => void |
undefined |
Error handler |
All Highlight props except targetRef, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
children |
ReactNode |
required | Single React element that accepts a ref prop |
import {
DEFAULT_MAX_HIGHLIGHTS, // 1000
IGNORED_TAG_NAMES, // ["SCRIPT", "STYLE", "NOSCRIPT", "IFRAME", "TEXTAREA"]
SLOW_SEARCH_THRESHOLD_MS // 100
} from "react-css-highlight";import {
useHighlight, // Main highlight hook
useDebounce // Utility debounce hook
} from "react-css-highlight";The component comes with pre-defined highlight styles that use CSS custom properties:
::highlight(highlight) {
background-color: var(--highlight-primary, #fef3c7);
color: inherit;
}All highlight colors can be customized using CSS custom properties. Override these variables in your global stylesheet or component styles:
:root {
/* Primary highlight (default) */
--highlight-primary: #fef3c7; /* Light yellow */
/* Secondary highlight */
--highlight-secondary: #cffafe; /* Sky blue */
/* Success highlight */
--highlight-success: #dcfce7; /* Light green */
/* Warning highlight */
--highlight-warning: #fde68a; /* Orange-yellow */
/* Error highlight */
--highlight-error: #ffccbc; /* Light red */
/* Active/focused highlight */
--highlight-active: #fcd34d; /* Dark yellow */
}Example: Customize colors to match your theme:
:root {
--highlight-primary: #e0f2fe; /* Light blue */
--highlight-success: #d1fae5; /* Mint green */
--highlight-error: #fee2e2; /* Light pink */
}The component includes several pre-defined highlight styles:
// Available variants
highlightName="highlight" // Primary (default)
highlightName="highlight-primary" // Yellow (#fef3c7)
highlightName="highlight-secondary" // Sky blue (#cffafe)
highlightName="highlight-success" // Light green (#dcfce7)
highlightName="highlight-warning" // Orange-yellow (#fde68a)
highlightName="highlight-error" // Light red (#ffccbc)
highlightName="highlight-active" // Dark yellow (#fcd34d), bold textCreate custom highlight styles by providing a highlightName:
<Highlight
search="error"
targetRef={ref}
highlightName="my-custom-highlight"
/>::highlight(my-custom-highlight) {
background-color: #ff0000;
color: white;
text-decoration: underline wavy;
font-weight: bold;
}- Pre-compiled Regex - Patterns compiled once per search (500× faster)
- TreeWalker - Native browser API for efficient DOM traversal
- Early Exit - Stops at
maxHighlightslimit - Empty Node Skipping - Ignores whitespace-only text nodes
- requestIdleCallback - Non-blocking highlight styling to prevent UI freezes
- Sync Match Count - Match counts calculated synchronously, styling applied asynchronously
- Performance Monitoring - Dev-mode warnings for slow searches (>100ms)
The highlighting system uses a two-phase approach for optimal performance:
-
Synchronous Phase (immediate):
- Calculates match count
- Updates state
- Calls
onHighlightChangecallback - Returns immediately to keep UI responsive
-
Asynchronous Phase (deferred):
- Applies visual highlighting using CSS Custom Highlight API
- Scheduled via
requestIdleCallbackduring browser idle time - Prevents blocking user interactions
This means matchCount is always up-to-date immediately, while visual highlights appear shortly after without blocking the main thread.
// ✅ Good - Single highlight with reasonable limit
<Highlight search="term" targetRef={ref} maxHighlights={500} />
// ✅ Good - Pre-filter search terms and memo the result
const toHighlight = useMemo(() => terms.filter(t => t.length > 2), [terms])
<Highlight
search={toHighlight}
targetRef={ref}
/>
// ⚠️ Caution - Many terms on huge documents and the array is not memoed
<Highlight
search={[...100terms]}
targetRef={ref}
maxHighlights={5000} // Consider lowering
/>| Browser | Version | Status | Notes |
|---|---|---|---|
| Chrome | 105+ | ✅ Full support | |
| Chrome Android | 105+ | ✅ Full support | |
| Edge | 105+ | ✅ Full support | |
| Firefox | 140+ | Cannot use with text-decoration or text-shadow |
|
| Firefox Android | 140+ | Same limitations as desktop | |
| Safari | 17.2+ | Style ignored when combined with user-select: none (WebKit bug 278455) |
|
| Safari iOS | 17.2+ | Same limitation as desktop | |
| Opera | 91+ | ✅ Full support | |
| Opera Android | 73+ | ✅ Full support | |
| Samsung Internet | 20+ | ✅ Full support | |
| WebView Android | 105+ | ✅ Full support |
- ❌ Cannot use
text-decoration(underline, overline, line-through) - ❌ Cannot use
text-shadow - ✅ Other styling properties work (background-color, color, font-weight, etc.)
/* ❌ Won't work in Firefox */
::highlight(my-highlight) {
text-decoration: underline;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
/* ✅ Works in Firefox */
::highlight(my-highlight) {
background-color: yellow;
color: black;
font-weight: bold;
}⚠️ Highlight style is ignored when the target element hasuser-select: none- Workaround: Remove
user-select: nonefrom highlighted content
/* ❌ Highlight won't appear in Safari */
.content {
user-select: none;
}
/* ✅ Highlight works */
.content {
user-select: auto; /* or remove the property */
}The component automatically detects browser support:
import { isHighlightAPISupported } from "react-css-highlight";
if (!isHighlightAPISupported()) {
console.warn("Browser doesn't support CSS Custom Highlight API");
}In development mode, the component logs warnings when the API is unsupported.
For browsers without support, consider:
-
Feature Detection + Graceful Degradation
const isSupported = isHighlightAPISupported(); return isSupported ? ( <Highlight search="term" targetRef={ref} /> ) : ( <TraditionalMarkHighlight search="term"> {content} </TraditionalMarkHighlight> );
-
User Notification
{!isHighlightAPISupported() && ( <div className="warning"> Your browser doesn't support text highlighting. Please upgrade to Chrome 105+, Safari 17.2+, or Firefox 140+. </div> )}
When testing your implementation:
- Chrome/Edge 105+ - Test full functionality
- Safari 17.2+ - Verify no
user-select: noneconflicts - Firefox 140+ - Avoid
text-decorationandtext-shadow - Mobile Safari - Test touch interactions with highlights
- Chrome Android - Verify performance on mobile devices
// Note: Import CSS once in your app entry point (main.tsx, App.tsx, or _app.tsx):
// import "react-css-highlight/dist/Highlight.css";
import { useState, useRef } from "react";
import { useHighlight } from "react-css-highlight";
function SearchWithStats() {
const [searchTerm, setSearchTerm] = useState("");
const contentRef = useRef<HTMLDivElement>(null);
const { matchCount, isSupported, error, refresh } = useHighlight({
search: searchTerm,
targetRef: contentRef,
debounce: 300,
});
if (!isSupported) {
return (
<div className="alert">
Your browser doesn't support text highlighting.
Please upgrade to a modern browser.
</div>
);
}
return (
<div>
<div className="search-header">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
className="search-input"
/>
<div className="search-stats">
{error ? (
<span className="error">Error: {error.message}</span>
) : (
<span className="match-count">
{searchTerm && `${matchCount} ${matchCount === 1 ? 'match' : 'matches'}`}
</span>
)}
</div>
</div>
<div ref={contentRef} className="content">
{/* Your content here */}
</div>
</div>
);
}For virtualized lists or dynamically changing content, use the refresh() callback:
import { useEffect, useRef, useState } from "react";
import { useHighlight } from "react-css-highlight";
import VirtualList from "react-virtual-list"; // or any virtualization library
function VirtualizedSearchList() {
const [searchTerm, setSearchTerm] = useState("");
const [visibleRows, setVisibleRows] = useState([]);
const listRef = useRef<HTMLDivElement>(null);
const { matchCount, refresh } = useHighlight({
search: searchTerm,
targetRef: listRef,
debounce: 300,
});
// Re-highlight when visible rows change (virtualization updates DOM)
useEffect(() => {
refresh();
}, [visibleRows, refresh]);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search in list..."
/>
<p>Found {matchCount} matches</p>
<div ref={listRef}>
<VirtualList
items={items}
onVisibleRowsChange={setVisibleRows}
>
{(item) => <div>{item.content}</div>}
</VirtualList>
</div>
</div>
);
}function InteractiveSearch() {
const [searchTerm, setSearchTerm] = useState("");
const [caseSensitive, setCaseSensitive] = useState(false);
const [matchCount, setMatchCount] = useState(0);
const contentRef = useRef<HTMLDivElement>(null);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<label>
<input
type="checkbox"
checked={caseSensitive}
onChange={(e) => setCaseSensitive(e.target.checked)}
/>
Case sensitive
</label>
<p>Found {matchCount} matches</p>
{/* Debounce prevents excessive updates while typing */}
<Highlight
search={searchTerm}
targetRef={contentRef}
caseSensitive={caseSensitive}
debounce={300} // Wait 300ms after user stops typing
onHighlightChange={setMatchCount}
/>
<div ref={contentRef}>
{/* Your content here */}
</div>
</div>
);
}function CustomDebounceExample() {
const [searchTerm, setSearchTerm] = useState("");
const contentRef = useRef<HTMLDivElement>(null);
return (
<>
{/* No debounce - immediate updates */}
<Highlight
search={searchTerm}
targetRef={contentRef}
debounce={0}
/>
{/* Long debounce for expensive operations */}
<Highlight
search={searchTerm}
targetRef={largeContentRef}
debounce={500}
maxHighlights={500}
/>
{/* Alternative: Use the exported useDebounce hook */}
<SearchWithCustomDebounce />
</>
);
}
// You can also use the exported useDebounce hook directly
import { useDebounce } from "@/components/general/Highlight";
function SearchWithCustomDebounce() {
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearch = useDebounce(searchTerm, 300);
const contentRef = useRef<HTMLDivElement>(null);
return (
<>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<Highlight
search={debouncedSearch}
targetRef={contentRef}
debounce={0} // Already debounced manually
/>
<div ref={contentRef}>{content}</div>
</>
);
}function ColorCodedSearch() {
const contentRef = useRef<HTMLDivElement>(null);
return (
<>
<Highlight
search={["TODO", "FIXME"]}
targetRef={contentRef}
highlightName="highlight-warning"
/>
<Highlight
search={["DONE", "FIXED"]}
targetRef={contentRef}
highlightName="highlight-success"
/>
<Highlight
search={["BUG", "ERROR"]}
targetRef={contentRef}
highlightName="highlight-error"
/>
<pre ref={contentRef}>
{codeContent}
</pre>
</>
);
}import { createPortal } from "react-dom";
function ModalWithHighlight() {
const modalRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
<Highlight search="important" targetRef={modalRef} />
{isOpen && createPortal(
<div ref={modalRef} className="modal">
<p>This is important information in a portal.</p>
</div>,
document.body
)}
</>
);
}function RobustSearch() {
const [error, setError] = useState<Error | null>(null);
const contentRef = useRef<HTMLDivElement>(null);
return (
<>
<Highlight
search={userInput}
targetRef={contentRef}
onError={(err) => {
console.error("Highlight error:", err);
setError(err);
}}
/>
{error && (
<div className="error">
Failed to highlight: {error.message}
</div>
)}
<div ref={contentRef}>{content}</div>
</>
);
}// ✅ Filter empty/short terms before passing
const validTerms = terms.filter(t => t.trim().length > 0);
<Highlight search={validTerms} targetRef={ref} />
// ✅ Use reasonable maxHighlights for large documents
<Highlight search="term" targetRef={ref} maxHighlights={500} />
// ✅ Memoize search terms if they're derived from props
const searchTerms = useMemo(() =>
extractTerms(props.query),
[props.query]
);
// ✅ Use wholeWord for precise matching
<Highlight search="cat" targetRef={ref} wholeWord />
// Only matches "cat", not "category" or "scatter"
// ✅ Provide meaningful highlightName for multiple highlights
<Highlight search="error" highlightName="log-error" />
<Highlight search="warning" highlightName="log-warning" />// ❌ Don't create highlights on every render
{items.map(item =>
<Highlight search={item.term} targetRef={ref} key={item.id} />
)}
// This creates N highlights! Use array instead:
<Highlight search={items.map(i => i.term)} targetRef={ref} />
// ❌ Don't use extremely high maxHighlights
<Highlight search="a" maxHighlights={999999} /> // Will freeze browser!
// ❌ Don't highlight on input change without debounce
<input onChange={(e) => setSearch(e.target.value)} />
<Highlight search={search} targetRef={ref} debounce={0} /> // Will update on every keystroke!
// ✅ Use the built-in debounce prop (recommended)
<Highlight search={search} targetRef={ref} debounce={300} />
// ✅ Or debounce manually using the exported hook
const debouncedSearch = useDebounce(search, 300);
<Highlight search={debouncedSearch} targetRef={ref} />
// ❌ Don't pass empty strings
<Highlight search={["", "term", ""]} /> // Filter first!
// ❌ Don't use wrapper pattern for complex scenarios
<HighlightWrapper>
<HighlightWrapper> // Nested = bad
<Content />
</HighlightWrapper>
</HighlightWrapper>Check:
- Browser supports CSS Custom Highlight API (Chrome 105+, Safari 17.2+)
targetRef.currentis not null (component is mounted)- Search terms are not empty strings
- Content actually contains the search terms
- Check browser console for errors
// Debug helper
<Highlight
search="term"
targetRef={ref}
onHighlightChange={(count) => console.log(`Found ${count} matches`)}
onError={(err) => console.error(err)}
/>Solutions:
- Use the built-in
debounceprop (default is 100ms) - Reduce
maxHighlights(default is 1000) - Filter out short/common terms
- Break large documents into smaller sections
// Use built-in debounce (recommended)
<Highlight
search={searchTerm}
targetRef={ref}
debounce={300} // Wait 300ms after changes
maxHighlights={300} // Lower limit
/>
// Or debounce manually
const debouncedSearch = useDebounce(searchTerm, 300);
<Highlight
search={debouncedSearch}
targetRef={ref}
debounce={0} // Already debounced
maxHighlights={300}
/>Solution: For dynamic content (virtualized lists, infinite scroll, etc.), use the refresh() callback:
// Using the hook with refresh
const { refresh } = useHighlight({
search: "term",
targetRef: contentRef
});
// Re-highlight after content changes
useEffect(() => {
refresh();
}, [contentVersion, refresh]);Alternative: Force re-render the Highlight component with a key:
// Works but less efficient
<Highlight
key={contentVersion}
search="term"
targetRef={ref}
/>The component automatically skips:
<script><style><noscript><iframe><textarea>
For additional exclusions, wrap excluded content in a container and don't pass its ref.
// ❌ Wrong
const ref = useRef<HTMLDivElement>();
// ✅ Correct
const ref = useRef<HTMLDivElement>(null);┌───────────────────────────────────────────────────────────┐
│ React Layer │
│ useHighlight Hook (React-specific concerns) │
│ - State management (matchCount, error, isSupported) │
│ - Effect lifecycle & cleanup │
│ - Debouncing user input │
│ - Callback stability (useEffectEvent) │
└─────────────────────┬─────────────────────────────────────┘
│ Delegates to
┌─────────────────────▼─────────────────────────────────────┐
│ Vanilla Core │
│ createHighlight (Framework-agnostic) │
│ - Input validation & normalization │
│ - DOM traversal & text matching │
│ - CSS Custom Highlight API integration │
│ - Async scheduling (requestIdleCallback) │
│ - Used by React, Vue, Svelte, Angular, etc. │
└───────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 1. User provides search terms + targetRef │
└──────────────────┬──────────────────────────────────┘
│
┌──────────────────▼──────────────────────────────────┐
│ 2. Normalize and validate input │
│ - Trim whitespace, filter empty strings │
│ - Pre-compile regex patterns (once) │
│ - Escape special characters │
└──────────────────┬──────────────────────────────────┘
│
┌──────────────────▼──────────────────────────────────┐
│ 3. TreeWalker traverses DOM text nodes [SYNC] │
│ - Skip SCRIPT, STYLE, empty nodes │
│ - Process only TEXT_NODE types │
│ - Calculate match count immediately │
└──────────────────┬──────────────────────────────────┘
│
┌──────────────────▼──────────────────────────────────┐
│ 4. Create Range objects for matches [SYNC] │
│ - Calculate start/end offsets │
│ - Store ranges in array │
│ - Update matchCount & call onChange │
└──────────────────┬──────────────────────────────────┘
│
┌──────────────────▼──────────────────────────────────┐
│ 5. Schedule visual update [ASYNC] │
│ - requestIdleCallback queues update │
│ - Waits for browser idle time │
│ - Non-blocking, cancellable │
└──────────────────┬──────────────────────────────────┘
│ (when browser is idle)
┌──────────────────▼──────────────────────────────────┐
│ 6. Register with CSS.highlights API [ASYNC] │
│ - Create Highlight(...ranges) │
│ - CSS.highlights.set(name, highlight) │
└──────────────────┬──────────────────────────────────┘
│
┌──────────────────▼──────────────────────────────────┐
│ 7. Browser applies ::highlight() CSS styles │
│ - Non-invasive (no DOM mutation) │
│ - Hardware accelerated │
└─────────────────────────────────────────────────────┘