Regex match highlighter
Split text into plain and matched segments for any regular expression, and render the matches as highlighted marks in React.
React / Next.jsUtilitiesregexhighlightreacttext
Live preview
Code
export type Part = { text: string; hit: boolean; groups?: string[] };
export function segments(pattern: string, flags: string, text: string, maxMatches = 500) {
let re: RegExp;
try {
re = new RegExp(pattern, flags.includes('g') ? flags : flags + 'g'); // matchAll requires g
} catch (e) {
return { parts: [{ text, hit: false }] as Part[], error: (e as Error).message, count: 0 };
}
const parts: Part[] = [];
let cursor = 0;
let count = 0;
for (const m of text.matchAll(re)) {
if (m[0] === '' ) continue; // skip empty matches such as /a*/ or /^/
if (++count > maxMatches) break;
const start = m.index ?? 0;
if (start > cursor) parts.push({ text: text.slice(cursor, start), hit: false });
parts.push({ text: m[0], hit: true, groups: m.slice(1).map((g) => g ?? '') });
cursor = start + m[0].length;
}
if (cursor < text.length) parts.push({ text: text.slice(cursor), hit: false });
return { parts, error: '', count };
}
export function Highlight({ pattern, flags = '', text }: { pattern: string; flags?: string; text: string }) {
const { parts, error, count } = segments(pattern, flags, text);
let n = 0;
return (
<div>
{error && <p role="alert" className="mb-2 text-sm text-red-600">{error}</p>}
<p className="whitespace-pre-wrap break-words rounded border p-3 font-mono text-sm">
{parts.map((p, i) =>
p.hit ? (
<mark key={i} className={n++ % 2 ? 'bg-amber-200' : 'bg-sky-200'} title={p.groups?.join(' | ')}>
{p.text}
</mark>
) : (
<span key={i}>{p.text}</span>
),
)}
</p>
<p className="mt-1 text-xs text-slate-500">{count} match{count === 1 ? '' : 'es'}</p>
</div>
);
}
How it works
What it solves
A regex tester must show where a pattern matches inside a block of text, survive invalid patterns while the user is still typing and never freeze on empty matches. This helper turns the pattern, flags and text into an array of segments that any renderer can display.
How it works
segments compiles the pattern with new RegExp, always adding the global flag so matchAll can iterate. Compilation errors are caught and returned as an error string, so the UI can show the message beside the input rather than crashing. Each match contributes the unmatched text before it and then the matched text flagged as a hit, with its capture groups attached; a final tail segment covers the rest.
Zero-length matches such as a* or ^ are a classic infinite-loop trap in manual exec loops. matchAll advances automatically, and the code skips empty hits when building segments so they do not produce blank marks. A cap on the number of matches protects the page from catastrophic patterns that produce huge result sets.
The component maps hit segments to <mark> elements with alternating background colours so adjacent matches are distinguishable, and preserves whitespace using whitespace-pre-wrap.
Adapting it
- Render group captures in a table using the
groupsarray. - Run the matcher in a worker with a timeout to guard against slow patterns.
- Remove the forced
gflag when you want only the first match.
Pitfalls
- Backtracking-heavy patterns can still freeze the main thread.
- Escape user text with React rather than injecting HTML.
How to use it
- Save as
components/Highlight.tsx. const { parts, error } = segments(pattern, flags, text)gives data only; no UI needed.- Or render
<Highlight pattern="\\d+" flags="i" text={sample} />to see marks directly. - Show
errornext to the pattern input.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.