Highlight the same word in three different ways. At the same time.

A TypeScript-first library for substring highlighting with first-class multi-state per-match styling. Every match gets a base style. Specific matches — by index, range, list, or your own predicate — get layered styles on top. Bring precomputed ranges when your own matcher already found them. ~2.7 KB brotlied. Two micro-dependencies. SSR-safe. React, React Native, or no framework at all.

Rendered output
time time time time time
#0base + preview #1base + preview #2base + active #3base + bookmarked #4base + bookmarked
Highlight.tsx
// Every "time" gets hl-base. Then layer states on top.
import { Highlight } from 'one-more-highlight';

<Highlight
  text="time time time time time"
  searchWords={['time']}
  highlightClassName="hl-base"
  states={[
    { name: 'active',     index: 2,        className: 'hl-active' },
    { name: 'preview',    range: [0, 1],   className: 'hl-preview' },
    { name: 'bookmarked', indices: [3, 4], className: 'hl-bookmark' },
  ]}
/>

TypeScript-first

Discriminated union narrows on the selector field — index, range, or indices.

Tiny

~2.7 KB brotlied for the core (ESM); ~3.2 KB for the CDN bundle. Two micro-deps (clsx, escape-string-regexp), both MIT, ~400 B combined. /css, /vanilla, /native, /a11y and /navigation are opt-in — you only pay for what you import.

React, native, or none

<Highlight> on the web, <HighlightText> from /native, or /vanilla with no framework at all. The selectors stay the same.

SSR-safe

No window, no Date.now, no Math.random. Hydration matches.

Get started

$ pnpm add one-more-highlight

Peer dependency: react ≥ 18. react-native ≥ 0.70 is optional and used only by the /native entry point.