-
-
Notifications
You must be signed in to change notification settings - Fork 3
Page Highlighting
This guide covers marking whole pages, regions, and text queries with @highlighters: highlightAll for declarative/page-wide marking, include/exclude selectors for scoping, text-query targeting for matching specific strings, and surgical exclusions.
For live, user-driven selection marking see Selection-Highlighting. For a tour of every entry point see Which-API-Should-I-Use.
highlightAll(options?) scans document.body, marks every visible text run it finds, and also marks every element carrying a data-highlight attribute. One call, one handle, the whole page.
import { highlightAll } from "@highlighters/core";
const handle = highlightAll();
// ...later
handle.remove(); // restore the original DOMIt returns a single API-Reference covering all matches. The default snap is "line" (whole-line bands rather than per-word). To recolour or restyle, pass options exactly as you would to highlight:
const handle = highlightAll({
color: { palette: "fluorescent", swatch: "yellow" },
opacity: 0.4,
});highlightAll attaches a debounced MutationObserver to the root. Nodes added later get marked; nodes removed drop their marks, with no full rescan. The watcher's teardown folds into the handle, so handle.remove() disconnects it.
This means highlightAll is safe to call before content has loaded. If there are no matches yet, it still wires the watcher and returns a live handle, so a later DOM change produces marks.
Outside a DOM (SSR), it returns an inert no-op handle. See SSR-Support.
Performance: a page-wide mark can produce a large mark count. Above the auto-degrade threshold (50 simultaneous marks) the SVG tier steps down to the CSS tier. See Performance and How-It-Works.
Page-wide marking does not have to mean the entire body. Pass a PageTarget to highlight() to scan a sub-tree with include/exclude selectors:
interface PageTarget {
root?: Element | Document; // Root to scan. Defaults to document.body.
include?: string[]; // Selectors whose subtrees are additionally included.
exclude?: string[]; // Selectors whose subtrees are excluded.
}import { highlight } from "@highlighters/core";
highlight({
root: document.querySelector("article")!,
exclude: ["pre", "code", "figure"],
});A few rules worth knowing:
-
No
includemeans everything. Whenincludeis empty (or omitted), the wholerootis in scope. Add selectors to narrow scope to only those subtrees. -
Exclusion always wins. A node is dropped the moment any ancestor matches an
excludeselector, even if it also matches aninclude. Exclude has precedence at every level. -
Invalid selectors are inert. A malformed selector in
includeorexcludematches nothing rather than throwing. -
Non-rendered text is skipped automatically. Text inside
<script>,<style>,<head>, and similar non-rendered subtrees is never marked, so you do not need to exclude them yourself. - Whitespace is trimmed. Each accepted text node is marked over its first-to-last non-whitespace span, so bands hug the visible glyphs.
highlight({
root: document.body,
include: [".prose", "[data-mark]"],
exclude: [".prose blockquote"],
});Here only text inside .prose or [data-mark] is in scope, and any blockquote within .prose is carved back out.
There are two ways to keep a region from being marked, and both apply to highlightAll and to any PageTarget scan.
Selector-based, defined at the call site:
highlight({ root: document.body, exclude: ["nav", "footer", ".no-highlight"] });Declarative, defined in the markup. Any element carrying data-highlight-exclude has its entire subtree skipped by highlightAll, PageTarget scans, and the live highlightSelection marker, with no call-site configuration:
<article>
<p>This paragraph is highlighted.</p>
<aside data-highlight-exclude>
<p>Nothing in here gets marked.</p>
</aside>
</article>This is the inverse of the data-highlight opt-in used by highlightAll. Use data-highlight to force a single element to be marked; use data-highlight-exclude to carve a subtree out of any page scan or the live selection marker.
data-highlight-excludeis honoured by both the page-scan walker and the live-selection marker;data-highlight(the opt-in) applies to page scans only. Both are presence-based: there are no meaningful values, they are matched as[data-highlight]and[data-highlight-exclude].
To mark every occurrence of a string or pattern, pass a TextTarget to highlight():
interface TextTarget {
text: string | RegExp; // Every match within root.
root?: Element | Document; // Defaults to document.body.
}import { highlight } from "@highlighters/core";
// Literal string: case-sensitive, non-overlapping.
highlight({ text: "TypeScript" });
// RegExp: scanned globally; case follows the `i` flag.
highlight({ text: /\b(perf|performance)\b/i, root: document.querySelector("main")! });Matching behaviour, taken from source:
- String queries are literal, case-sensitive, and non-overlapping. The scan advances past each match.
-
RegExp queries are scanned globally over the concatenated text. Case follows the
iflag. Zero-width matches are skipped (no infinite loops). A stickyyflag is stripped so the scan does not stop at the first gap. -
Matches can span inline boundaries. The walk concatenates text nodes into one flat string, so a query matches across inline wrappers like
foo<em>bar</em>baz. - Text inside non-rendered subtrees (
<script>,<style>, etc.) is never matched.
The default snap for a text query is "word". See Snapping-and-Overshoot for how snap rounds ranges to word, line, or glyph boundaries.
A plain Element or a CSS selector string also resolves through the same pipeline. A selector marks every matching element's text content:
highlight(document.querySelector("h1")!); // one element
highlight("h2, h3"); // every matching elementFor elements and selectors the default snap is "line".
If you need the resolved Ranges without rendering (for measuring, custom rendering, or your own observers), the same functions that power the entry points are exported from @highlighters/core:
import { findTextRanges, collectPageRanges } from "@highlighters/core";
// Range per match of a string or RegExp under a root.
const ranges = findTextRanges(document.body, "TypeScript");
// Range per visible text run under a root, honouring include/exclude.
const pageRanges = collectPageRanges({
root: document.body,
exclude: [".sidebar"],
});Both are DOM-touching, so they live in the full @highlighters/core entry, not the SSR-safe @highlighters/core/path subset. Both never throw and return [] when there is no DOM or no match. See API-Reference for the complete export list.
The framework bindings are built around marking a single element via a ref, so they map most naturally to data-highlight / data-highlight-exclude declarative scanning. For page-wide and text-query marking, call the core highlightAll / highlight functions from an effect and tear the handle down on cleanup.
import { useEffect } from "react";
import { highlightAll } from "@highlighters/core";
function usePageHighlights() {
useEffect(() => {
const handle = highlightAll({ color: { palette: "mild", swatch: "yellow" } });
return () => handle.remove();
}, []);
}Text-query marking scoped to a container:
import { useEffect, useRef } from "react";
import { highlight } from "@highlighters/core";
function SearchHits({ query }: { query: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current || !query) return;
const handle = highlight({ text: query, root: ref.current });
return () => handle.remove();
}, [query]);
return <div ref={ref}>/* content */</div>;
}For marking a single element by ref, prefer the useHighlight hook or <Highlight> component, documented in API-Reference.
<script setup lang="ts">
import { onMounted, onBeforeUnmount } from "vue";
import { highlightAll, type MarkHandle } from "@highlighters/core";
let handle: MarkHandle | null = null;
onMounted(() => {
handle = highlightAll({ opacity: 0.4 });
});
onBeforeUnmount(() => handle?.remove());
</script>Text query scoped to a template ref:
<script setup lang="ts">
import { ref, watch, onBeforeUnmount } from "vue";
import { highlight, type MarkHandle } from "@highlighters/core";
const root = ref<HTMLElement | null>(null);
const props = defineProps<{ query: string }>();
let handle: MarkHandle | null = null;
watch(
() => props.query,
(q) => {
handle?.remove();
if (root.value && q) handle = highlight({ text: q, root: root.value });
},
);
onBeforeUnmount(() => handle?.remove());
</script>
<template>
<div ref="root"><!-- content --></div>
</template><script lang="ts">
import { onMount } from "svelte";
import { highlightAll } from "@highlighters/core";
onMount(() => {
const handle = highlightAll({ color: { palette: "fluorescent", swatch: "green" } });
return () => handle.remove();
});
</script>The highlight Svelte action (@highlighters/svelte) is for marking a single element's text in place via use:highlight. For page-wide or text-query marking, call core highlightAll / highlight as above.
| Marker | Effect | Matched as |
|---|---|---|
data-highlight |
Force this element to be marked by highlightAll
|
[data-highlight] |
data-highlight-exclude |
Skip this element's whole subtree in any page scan or the live selection | [data-highlight-exclude] |
<main>
<h1 data-highlight>Marked because of the attribute</h1>
<p>Marked by the page scan.</p>
<pre data-highlight-exclude><code>not.marked();</code></pre>
</main>highlightAll(); // picks up the page scan, the data-highlight, and skips the excluded <pre>- Selection-Highlighting — live, user-driven selection marking.
-
Snapping-and-Overshoot — how
snaprounds ranges to word/line/glyph boundaries. - Mark-Types-and-Shapes — highlight, underline, overline, strike-through.
- Color-and-Palettes — palettes, swatches, gradients.
-
Animation — draw-on entrance, stagger,
in-viewtriggers. - Performance — mark count, tier degrade, large pages.
- Recipes — copy-paste patterns.
- API-Reference / Options-Reference — the full surface.
Getting Started
The Mark
- Mark-Types-and-Shapes
- Tips-and-Edges
- Ink-and-Optics
- Color-and-Palettes
- Snapping-and-Overshoot
- Animation
Targeting
Reference
Production