-
-
Notifications
You must be signed in to change notification settings - Fork 3
Recipes
Common integration patterns for @highlighters, implemented across every supported framework. Each recipe is a self-contained snippet you can paste into a project; adapt the markup, colours, and spacing to suit your own design system.
The Vanilla tabs call @highlighters/core directly. The framework tabs use the bindings (@highlighters/react, @highlighters/vue, @highlighters/svelte), which handle setup, option updates, and teardown for you. For most web work, prefer the framework packages. See Which-API-Should-I-Use for the trade-offs.
Every recipe uses real HighlightOptions fields only. The full set of options lives in Options-Reference; the function and type signatures in API-Reference.
Mark every occurrence of a string (or RegExp) within a root, including matches that span inline boundaries. Pass a TextTarget ({ text, root? }) to highlight. The default snap for a text query is "word", so matches clamp to word bounds rather than overshooting into whitespace. See Snapping-and-Overshoot.
import { highlight } from "@highlighters/core";
// Every match of the term inside the article.
const handle = highlight({
text: "deterministic",
root: document.querySelector("article") ?? document.body,
});
// A RegExp works too (use a global flag for all matches).
highlight({ text: /\bink\b/gi });
// Clearing the search removes all its marks and restores the DOM.
handle.remove();One MarkHandle covers all matches of that query. Re-run on input to drive a live find-as-you-type box; remove the previous handle before creating the next.
let current = highlight({ text: "" });
searchInput.addEventListener("input", () => {
current.remove();
current = highlight({ text: searchInput.value, root: article });
});The hook accepts a core Target, so pass a TextTarget object straight through:
import { useHighlight } from "@highlighters/react";
function SearchHighlighter({ term }: { term: string }) {
// Recreates the mark whenever `term` changes identity.
useHighlight({ text: term, root: document.body }, { snap: "word" });
return null;
}<script setup lang="ts">
import { computed } from "vue";
import { useHighlight } from "@highlighters/vue";
const props = defineProps<{ term: string }>();
const target = computed(() => ({ text: props.term }));
useHighlight(target, { snap: "word" });
</script>
<template><span /></template>The Svelte action highlights the element it is attached to, so for arbitrary text queries reach for core directly:
<script lang="ts">
import { highlight } from "@highlighters/core";
export let term: string;
let handle = highlight({ text: term });
$: {
handle.remove();
handle = highlight({ text: term });
}
</script>Highlight a whole element's text. The framework bindings are the natural fit: wrap the run in a Highlight component (or attach the action), and the binding marks its text content without altering it. The default snap for an element/selector target is "line".
import { highlight } from "@highlighters/core";
// Pass an element, or a CSS selector string.
highlight("#thesis", { markType: "highlight", color: { palette: "mild", swatch: "yellow" } });import { Highlight } from "@highlighters/react";
function Article() {
return (
<p>
The part that matters most is{" "}
<Highlight options={{ color: { palette: "fluorescent", swatch: "yellow" } }}>
this exact sentence
</Highlight>
.
</p>
);
}Highlight renders a <span> by default. Use as for a different tag; extra HTML attributes pass through:
<Highlight as="strong" className="lead" options={{ markType: "underline" }}>
the whole opening clause
</Highlight><script setup lang="ts">
import { Highlight } from "@highlighters/vue";
</script>
<template>
<p>
The part that matters most is
<Highlight :options="{ color: { palette: 'fluorescent', swatch: 'yellow' } }">
this exact sentence
</Highlight>.
</p>
</template><script lang="ts">
import { highlight } from "@highlighters/svelte";
</script>
<p use:highlight={{ color: { palette: "fluorescent", swatch: "yellow" } }}>
Highlight this whole paragraph.
</p>The Svelte action takes the options object directly as its parameter (not nested). Pass new options to re-render through the live handle without re-seeding geometry.
Set animation.trigger to "in-view" to arm an IntersectionObserver so the draw-on sweep fires when the mark scrolls into view. Tune threshold and rootMargin for when it arms, and repeat: true to re-animate every entry. All animation is suppressed automatically under prefers-reduced-motion: reduce. Full details in Animation.
import { highlight } from "@highlighters/core";
highlight("#callout", {
animation: {
trigger: "in-view",
threshold: 0.4, // 40% of the mark visible before it draws
rootMargin: "0px 0px -10% 0px",
duration: 500,
easing: "ease-out",
direction: "left-to-right",
repeat: false, // one-shot
},
});import { Highlight } from "@highlighters/react";
function Callout({ children }: { children: React.ReactNode }) {
return (
<Highlight options={{ animation: { trigger: "in-view", threshold: 0.4 } }}>
{children}
</Highlight>
);
}<template>
<Highlight :options="{ animation: { trigger: 'in-view', threshold: 0.4 } }">
{{ text }}
</Highlight>
</template>
<script setup lang="ts">
import { Highlight } from "@highlighters/vue";
defineProps<{ text: string }>();
</script><script lang="ts">
import { highlight } from "@highlighters/svelte";
</script>
<p use:highlight={{ animation: { trigger: "in-view", threshold: 0.4 } }}>
This draws on when it scrolls into view.
</p>For a sequence that draws one mark after another like a pen down the page, bundle handles with group and call show() (members reveal in array order). This is a core-only API:
import { highlight, group } from "@highlighters/core";
const marks = group([
highlight("#point-1"),
highlight("#point-2"),
highlight("#point-3"),
]);
marks.hide();
marks.show(); // staggered draw-on in array orderThere are three render entry points, each with a different lifecycle.
| Entry point | What it marks | Default snap
|
Lifecycle |
|---|---|---|---|
highlight(target, options?, host?) |
one target (element, selector, Range, Selection, text query, or page) |
derived from target | persistent until remove()
|
highlightAll(options?) |
every [data-highlight] element plus a page scan, kept in sync by a MutationObserver
|
"line" |
persistent; one handle covers all matches |
highlightSelection(options?) |
the user's live selection, driven by selectionchange
|
"word" |
live; fades out on clear |
Persistent (declarative). Mark content already in the page and keep it marked as the DOM mutates. highlightAll collects everything carrying the data-highlight attribute (selected as [data-highlight]; presence is all that matters, there are no documented attribute values), scans the page, and attaches a debounced MutationObserver so added nodes get marked and removed nodes drop their marks. See Page-Highlighting.
<p data-highlight>This paragraph is highlighted declaratively.</p>
<aside data-highlight>So is this one.</aside>
<script type="module">
import { highlightAll } from "@highlighters/core";
// One handle for the whole page. Disconnecting the watcher folds into remove().
const all = highlightAll({ color: { palette: "mild", swatch: "green" } });
</script>Live (selection). Pour ink under the user's mouse/pen selection in real time. On coarse pointers (touch) this returns an inert handle and defers to native selection UI, so there is no overlay. When the selection empties, the overlay fades over 200ms (gated by fadeOnClear; instant under reduced-motion). See Selection-Highlighting.
import { highlightSelection } from "@highlighters/core";
const live = highlightSelection({
color: { palette: "fluorescent", swatch: "pink" },
fadeOnClear: true,
});
// update() composes additively across calls.
live.update({ markType: "underline" });
// Detach the selectionchange + pointer listeners.
live.remove();Speed-aware ink (the speed option group, Beta) engages only here, and only during a primary-button fine-pointer drag: a faster swipe deposits less ink. It is a no-op for programmatic, keyboard, or static selections, and under reduced motion. Enable it with speed: { enabled: true }.
In React/Vue/Svelte, drive highlightSelection / highlightAll from an effect or lifecycle hook and store the handle so you can remove it on teardown:
import { useEffect } from "react";
import { highlightSelection } from "@highlighters/core";
function LiveSelection() {
useEffect(() => {
const handle = highlightSelection({ speed: { enabled: true } });
return () => handle.remove();
}, []);
return null;
}There are no presets. Colour coordination is via the five palette families: fluorescent, mild (the library default), vintage, neutral, and calm. Each family's swatches map is ordered, and that order is the colour-coding cycle. See Color-and-Palettes for the full swatch tables.
Three ways to set colour, in order of specificity:
import { highlight } from "@highlighters/core";
// 1. A specific swatch in a family.
highlight("#a", { color: { palette: "vintage", swatch: "teal" } });
// 2. A family only -> uses that family's default swatch (mild->yellow, vintage->mustard, etc.).
highlight("#b", { palette: "calm" });
// 3. Any CSS color string (hex, rgb(), hsl(), named, currentColor, var(...)).
highlight("#c", { color: "var(--brand-ink)" });To colour-code a set of annotations, cycle the swatch names in a family. The swatch order is the intended cycle:
const cycle = ["yellow", "green", "blue", "pink", "orange", "purple"] as const;
document.querySelectorAll<HTMLElement>("[data-note]").forEach((el, i) => {
highlight(el, { color: { palette: "mild", swatch: cycle[i % cycle.length] } });
});A palette reference works identically through the framework bindings, since color is a plain option:
// React
<Highlight options={{ color: { palette: "calm", swatch: "lavender" } }}>note</Highlight><!-- Vue -->
<Highlight :options="{ color: { palette: 'calm', swatch: 'lavender' } }">note</Highlight><!-- Svelte -->
<span use:highlight={{ color: { palette: "calm", swatch: "lavender" } }}>note</span>For a multi-stop look, set gradient (it overrides color when present). Two or more stops are needed for a visible gradient:
highlight("#banner", {
gradient: {
type: "linear",
angle: 90,
stops: [
{ offset: 0, color: "#fff14d" },
{ offset: 1, color: "#ff6fae" },
],
},
});For brighter-than-page emission over the multiply ink, enable glow:
highlight("#neon", {
color: { palette: "fluorescent", swatch: "green" },
glow: { enabled: true, intensity: 0.7, spread: 6 },
});See Ink-and-Optics for blend modes, opacity, and the ink physics knobs.
The default blendMode is "multiply", so overlapping marks darken where they cross and dark text stays legible underneath. This is the intended optic: lay marks over the same text and they compound naturally, the way two passes of a real highlighter do.
import { highlight } from "@highlighters/core";
const sentence = "#claim";
// Two overlapping passes deepen the shared region under multiply.
highlight(sentence, { color: { palette: "fluorescent", swatch: "yellow" } });
highlight(sentence, { color: { palette: "fluorescent", swatch: "pink" }, opacity: 0.4 });Each call returns its own independent MarkHandle, so you can show, hide, update, or remove the layers separately:
const base = highlight(sentence, { color: "#fff14d" });
const accent = highlight(sentence, { color: "#ff6fae", opacity: 0.35 });
accent.hide(); // back to a single pass
accent.show(); // overlap again
accent.remove(); // drop just the accent layerIf you want overlaps to composite differently, change blendMode ("normal", "darken", "screen", "overlay", "color-burn"). Lowering opacity on the upper layer keeps both readable.
Performance note: marks count toward the auto-degrade threshold. Above 50 simultaneously visible marks, the "auto" renderer steps the SVG tier down to the lighter CSS tier (fidelity only, never position or colour). Heavy overlap multiplies the count, so budget accordingly. See Performance and How-It-Works.
highlight (and the React/Vue bindings) accept a host: a positioned element to mount the overlay inside, instead of the document body. Use it for marks inside transformed, scrolling, or stacked containers so the overlay tracks the content. A static host is promoted to position: relative.
import { highlight } from "@highlighters/core";
const panel = document.querySelector<HTMLElement>(".scroll-panel");
highlight("#row-3 .label", { snap: "line" }, panel);import { useRef } from "react";
import { Highlight } from "@highlighters/react";
function Panel() {
const host = useRef<HTMLDivElement>(null);
return (
<div ref={host} style={{ position: "relative", overflow: "auto" }}>
<Highlight host={host.current} options={{ snap: "line" }}>
scrolls with its container
</Highlight>
</div>
);
}(Vue's useHighlight composable and <Highlight> component do not take a host; mount inside the body or use core directly when you need one.)
- Getting-Started: installation and first mark in each framework
- Options-Reference: every option, default, and range
- API-Reference: full function and type signatures
- Mark-Types-and-Shapes: highlight, underline, overline, strike-through
- Color-and-Palettes: the five families and their swatch tables
-
Animation: draw-on, in-view triggers, stagger, and
group - Page-Highlighting and Selection-Highlighting: the declarative and live entry points
- SSR-Support and Accessibility-and-Reflow: rendering off the DOM, and how marks behave on reflow
Getting Started
The Mark
- Mark-Types-and-Shapes
- Tips-and-Edges
- Ink-and-Optics
- Color-and-Palettes
- Snapping-and-Overshoot
- Animation
Targeting
Reference
Production