High-performance text highlighting for the modern web. A production-grade alternative to mark.js — built for Angular, React, Next.js, and vanilla JavaScript.
- Fast — Designed to rival Chrome's native Find in Page. Uses
TreeWalkerfor O(n) traversal, binary search for match resolution, and zero-layout-cost rendering via the CSS Custom Highlight API. - Framework-safe — Never uses
innerHTML. Preserves Angular bindings, React reconciliation, event listeners, and component trees. - Three rendering engines — CSS Highlight API (zero DOM mutations), DOM wrapping (text node splitting, preserves original node for bindings), and overlay positioning. Auto-detects the best engine for your browser.
- Batched rendering — For very large DOMs (50K+ nodes), split rendering across animation frames with
batchSizeto keep the UI responsive. - Full-featured — Regex, multiple keywords, case sensitivity, diacritics, synonyms, wildcards, accuracy modes, exclude selectors, across-elements matching, and more.
- Tiny & tree-shakeable — ESM and CJS builds with
sideEffects: false.
| Package | Description | Version |
|---|---|---|
| @markitjs/core | Framework-agnostic highlighting engine | |
| @markitjs/react | React hook (useHighlight) and <Highlighter> component |
|
| @markitjs/angular | Angular directive (markitHighlight) and MarkitService |
# npm
npm install @markitjs/core
# bun
bun add @markitjs/core
# pnpm
pnpm add @markitjs/coreimport { markit } from '@markitjs/core';
const instance = markit(document.getElementById('content'));
instance.mark('search term', { renderer: 'auto' });
// Later...
instance.destroy();# npm
npm install @markitjs/react
# bun
bun add @markitjs/react
# pnpm
pnpm add @markitjs/react
# yarn (install @markitjs/core explicitly)
yarn add @markitjs/react @markitjs/coreWith Yarn, install peer dependency @markitjs/core explicitly (see commands above).
'use client';
import { useHighlight } from '@markitjs/react';
function SearchResults({ query }: { query: string }) {
const ref = useHighlight(query, { caseSensitive: false });
return (
<div ref={ref}>
<p>Content to search...</p>
</div>
);
}Next.js App Router: add "use client" at the top of any file that uses the hook or <Highlighter>. SSR-safe — no hydration mismatch. For dynamic content, pass contentKey so highlights re-apply when content changes (see Framework lifecycles).
# npm
npm install @markitjs/angular
# bun
bun add @markitjs/angular
# pnpm
pnpm add @markitjs/angular
# yarn (install @markitjs/core explicitly)
yarn add @markitjs/angular @markitjs/coreWith Yarn, install peer dependency @markitjs/core explicitly (see commands above).
import { MarkitHighlightDirective } from '@markitjs/angular';
@Component({
standalone: true,
imports: [MarkitHighlightDirective],
template: `
<div [markitHighlight]="searchTerm" [markitOptions]="{ renderer: 'dom' }">
<p>Content to search...</p>
</div>
`,
})
export class SearchComponent {
searchTerm = '';
}Runs outside NgZone. Compatible with OnPush, Signals, and zoneless apps. For dynamic content, pass [markitContentKey] so the directive re-applies highlights after content updates (see Framework lifecycles).
| Engine | DOM Mutations | Reflows | Best For |
|---|---|---|---|
CSS Highlight API (highlight-api) |
0 | 0 | Default. Fastest. Framework-safe. |
DOM Wrapping (dom) |
Per match | Batched | When you need click handlers on highlights |
Overlay (overlay) |
Container only | On scroll/resize | Maximum framework isolation |
Auto (auto) |
— | — | Feature-detects Highlight API, falls back to DOM |
| Option | Type | Default | Description |
|---|---|---|---|
renderer |
'auto', 'highlight-api', 'dom', 'overlay' |
'auto' |
Rendering strategy |
caseSensitive |
boolean |
false |
Case-sensitive matching |
ignoreDiacritics |
boolean |
false |
Strip diacritics (café → cafe) |
accuracy |
'partially', 'exactly', 'startsWith', 'complementary' |
'partially' |
Match accuracy mode |
separateWordSearch |
boolean |
false |
Split term into individual words |
acrossElements |
boolean |
false |
Match across element boundaries |
synonyms |
SynonymMap |
— | Synonym expansion ({ "JS": ["JavaScript"] }) |
wildcards |
'disabled', 'enabled', 'withSpaces' |
'disabled' |
Wildcard ? and * support |
exclude |
string[] |
— | CSS selectors to skip |
batchSize |
number |
0 |
Async batch rendering (0 = synchronous) |
debounce |
number |
0 |
Debounce delay in ms for live search |
debug |
boolean |
false |
Log timing to console |
See the full API reference for all options and callbacks.
markit/
├── packages/
│ ├── core/ @markitjs/core — highlighting engine
│ ├── react/ @markitjs/react — React hook & component
│ └── angular/ @markitjs/angular — Angular directive & service
├── apps/
│ ├── docs/ VitePress documentation site + playground
│ └── e2e-bench/ Playwright real-browser performance benchmarks
├── turbo.json Turborepo task configuration
├── tsconfig.base.json Shared TypeScript config
└── package.json Root workspace config (Bun)
git clone https://github.com/saurabhiam/markit.git && cd markit
bun install| Command | Description |
|---|---|
bun run build |
Build all packages |
bun run test |
Run all unit/integration tests (Vitest) |
bun run typecheck |
TypeScript type checking |
bun run dev |
Dev mode with watch |
bun run docs:dev |
Start documentation dev server |
bun run docs:build |
Build documentation site |
bun run e2e |
Run Playwright smoke tests (1K nodes; CI) |
bun run bench |
Run full Playwright performance benchmarks |
bun run clean |
Clean all build artifacts |
Documentation: Framework lifecycles — how React and Angular bindings run and re-apply highlights over time.
Unit & integration tests (127 tests across core, angular, react):
bun run testReal-browser performance benchmarks (Playwright + Chromium):
- Smoke tests (1K nodes, used in CI):
bun run e2e— runs for all three renderers (highlight-api, dom, overlay) - Full suite (1K–100K nodes):
bun run bench
bun run benchThe core engine follows a pipeline:
- Text Index —
TreeWalkerbuilds a flat array of text nodes mapped to a virtual concatenated string - Matcher — Compiles search terms into an optimized regex, finds matches against the virtual string
- Resolver — Binary search maps virtual string offsets back to DOM text nodes and character offsets
- Renderer — Applies highlights using the selected engine (Highlight API / DOM / Overlay)
Plugin hooks (beforeSearch, afterMatch, beforeRender, afterRender) allow interception at each stage. Multiple MarkIt instances can coexist on the same page; with the CSS Highlight API (or auto), instances share one Highlight per highlightName, so they do not overwrite each other.
Typical real-browser results (M1 MacBook, Chrome):
| Scenario | 1K nodes | 10K nodes | 50K nodes |
|---|---|---|---|
| Single keyword | < 5ms | < 30ms | < 150ms |
| 5 keywords | < 10ms | < 60ms | < 300ms |
| Regex | < 10ms | < 50ms | < 250ms |
| Unmark | < 2ms | < 15ms | < 80ms |
MarkIt uses Changesets with GitHub Actions for automated releases. Packages use independent versioning (each has its own version). Releases are keyed by a release tag (e.g. release-2025-03-10-01); one GitHub Release per release with package tarballs and source zip.
See RELEASING.md for the full release guide.
Made with ♥ in India