Modern, zero-dependency component for highlighting text using CSS Custom Highlight API
demo.mp4
- π 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.
- π¨ 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
- π§© Two Usage Patterns - Ref-based (power users) or wrapper (convenience)
- π TypeScript First - Full type safety with extensive JSDoc documentation
- π Search Results - Highlight search terms in documentation, articles, or search results
- π Code Editors - Syntax highlighting and search in code blocks
- π Data Tables - Highlight matching values in large datasets
- π Learning Tools - Emphasize key terms in educational content
- π Security Audit - Highlight sensitive data patterns in logs
- π§ Email Clients - Highlight mentions, keywords, or search matches
- 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 "@/components/general/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.
There are three ways to use this library, 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 |
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
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 |
isSupported |
boolean |
Whether browser supports CSS Custom Highlight API |
error |
Error | null |
Error object if highlighting failed, null otherwise |
| Prop | Type | Default | Description |
|---|---|---|---|
search |
string | string[] |
required | Text to highlight (supports multiple terms) |
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 search execution
- Performance Monitoring - Dev-mode warnings for slow searches (>100ms)
// β
Good - Single highlight with reasonable limit
<Highlight search="term" targetRef={ref} maxHighlights={500} />
// β
Good - Pre-filter search terms
<Highlight
search={terms.filter(t => t.length > 2)}
targetRef={ref}
/>
// β οΈ Caution - Many terms on huge documents
<Highlight
search={[...100terms]}
targetRef={ref}
maxHighlights={5000} // Consider lowering
/>| Content Size | Search Terms | Time | Highlights |
|---|---|---|---|
| 1,000 nodes | 1 term | ~5ms | ~50 |
| 1,000 nodes | 5 terms | ~15ms | ~250 |
| 10,000 nodes | 1 term | ~40ms | ~500 |
| 10,000 nodes | 10 terms | ~120ms | 1000 (max) |
Tested on MacBook Pro M1, Chrome 120
| 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 "@/components/general/Highlight/utils";
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 } = 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>
);
}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: Content is assumed to be static. If content changes, re-render the Highlight component:
// Force re-render with key
<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);βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. User provides search terms + targetRef β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββ
β 2. Pre-compile regex patterns (once) β
β - Escape special characters β
β - Add word boundaries if needed β
β - Validate patterns β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββ
β 3. TreeWalker traverses DOM text nodes β
β - Skip SCRIPT, STYLE, empty nodes β
β - Process only TEXT_NODE types β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββ
β 4. Create Range objects for each match β
β - Calculate start/end offsets β
β - Handle multi-node matches β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββ
β 5. Register with CSS.highlights API β
β - Create Highlight(...ranges) β
β - CSS.highlights.set(name, highlight) β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββ
β 6. Browser applies ::highlight() CSS styles β
β - Non-invasive (no DOM mutation) β
β - Hardware accelerated β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ