-
-
Notifications
You must be signed in to change notification settings - Fork 3
Limitations
This page documents what @highlighters does not support, what happens at the boundary, and why. Every constraint below is derived from the source, not aspiration. If you hit something here, the workaround (where one exists) is given.
For the full surface that is supported, see API-Reference and Options-Reference. For tier behaviour see How-It-Works and Performance.
The framework READMEs and some JSDoc examples reference fields that do not exist in the API. Treat them as illustrative only. The real options are documented in Options-Reference.
What: Snippets like options={{ preset: "wet" }}, use:highlight={{ preset: "mild" }}, and handle.update({ preset: "premium" }) appear in framework docstrings and the README. HighlightOptions has no preset field. Passing one is silently ignored: it survives mergeOptions as an unknown key but is dropped by resolveOptions, so it never reaches a renderer.
Why: Color coordination is done through palette families, not named presets. The default family is mild and the default color is mild → yellow (#f5e6a8). Reach for a curated swatch with the real API.
import { highlight } from "@highlighters/core";
// Real: pull from a curated family.
highlight(".key-point", { color: { palette: "fluorescent", swatch: "pink" } });
// Real: a family's default swatch.
highlight(".note", { palette: "vintage" });See Color-and-Palettes for the families and swatches.
What: color: "pink" is not a palette lookup. color is a CSS color string or a { palette, swatch } object, so "pink" is passed straight to CSS as the named color pink (a bright magenta), not the mild palette's muted pink swatch (#eec2cf).
Why: Letting any CSS color through (hex, rgb(), hsl(), named, currentColor, var(...)) is the whole point of the string form. Disambiguating "is this a CSS name or a swatch name" would make color lossy. Use the object form for swatches.
// CSS named color "pink" (magenta), NOT the mild swatch.
highlight(".x", { color: "pink" });
// The mild "pink" swatch.
highlight(".x", { color: { palette: "mild", swatch: "pink" } });What: The README mentions a single colorant dye-to-pigment knob. No such field exists in HighlightOptions or anywhere in core. Ink character is tuned through the ink group (flow, viscosity, feathering, streakiness, dryout, startEndBuildup, flowFade). See Ink-and-Optics.
These fields are valid TypeScript and survive resolution, but the renderer never reads them. They are inert today.
What: TipOptions.width (default 16) and TipOptions.thickness (default 4) are resolved and merged, but no geometry or renderer consumes them. Setting them changes nothing on screen.
Why: The renderer derives the chisel lean from tip.angle and the measured band width, not from an explicit nib width. Only tip.type, tip.angle, tip.overshoot, tip.overshootJitter, and tip.angleJitter affect output. See Tips-and-Edges.
What: contrastBackground is documented as driving "the dev-time WCAG-contrast warning." In the current code it is resolved to a field on ResolvedOptions and otherwise unused: there is no contrast computation and no console.warn/console.error anywhere in core. Setting it is a no-op. It is never rendered (it does not composite the mark against that background).
Why: The field reserves the surface for a future dev-time check. Until that lands, do not rely on it to catch low-contrast marks. For legibility, note that the default mix-blend-mode: multiply keeps dark text readable through the ink regardless of this field. See Accessibility-and-Reflow.
Three tiers sit behind one API and degrade is fidelity-only: a lower tier never moves or recolors a mark, only simplifies edges and texture. The cost is that the lower tiers ignore some options. See How-It-Works and Performance.
What: The CSS tier paints a linear-gradient band. It honors color, opacity, blendMode, gradient, and edge.radius. It does not render edge.waviness, edge.roughness, the ink texture layers (streakiness, feathering, dryout, noise), or glow. The band has straight edges.
Why: Wavy edges and texture require SVG masks and an feTurbulence filter, which is exactly the Tier A capability the CSS tier exists to do without. Color, opacity, blend, and band position still match Tier A, so the mark stays in the same place with the same hue.
What: The highlight-api tier paints via a generated ::highlight() rule over registered Ranges, with no overlay DOM. It supports only color and opacity (folded into the fill via color-mix). It drops blendMode, gradient, glow, all edge and ink shaping, and the draw-on animation (there is no band element to animate, so bandFor() returns null). For safety, the color is run through CSS.supports and falls back to transparent if it is not a bare color, so a var(...) or computed color may not paint here.
Why: ::highlight() exposes no opacity, blend, or filter hooks, so opacity is folded into the fill and everything else is unavailable by construction. This is the maximally-safe tier: native multiline, find-in-page and selection unaffected, text nodes untouched. It is only auto-chosen when it is the highest supported tier, or when you pin renderer: "highlight-api". See Animation for how draw-on behaves per tier.
What: Under renderer: "auto", SVG degrades to CSS when prefers-reduced-motion: reduce, prefers-reduced-data: reduce, or more than 50 simultaneously visible marks (DEFAULT_DEGRADE_THRESHOLD). The CSS tier is the floor of auto-degrade. The threshold is not configurable through HighlightOptions: it is a constant. To force a tier regardless of these signals, pin renderer to "svg", "css", or "highlight-api".
Why: The threshold protects the frame rate when many SVG filters are live. Pinning is the escape hatch; a pinned tier steps down only if unsupported. See Performance.
See Selection-Highlighting for the full feature set.
What: On coarse pointers ((pointer: coarse) or (hover: none)), highlightSelection() returns an inert handle and paints nothing, deferring to the OS native selection UI.
Why: Native selection (with its grab handles) is the better UX on touch, and a custom overlay would fight it. This is a deliberate fallback, not a failure: the returned handle is safe to call.
What: The speed option group engages only inside highlightSelection, only during a primary-button fine-pointer (mouse or pen) drag, and only when speed.enabled is true. It is a byte-identical no-op for highlight(), highlightAll(), programmatic or keyboard selections, and is suppressed under prefers-reduced-motion. The look that a fast swipe leaves persists after release because nothing repaints until the next gesture.
Why: Speed-aware deposit is sampled from focus-caret velocity during an active drag; there is no velocity to read for a static, keyboard, or programmatic selection. The feature is marked Beta. See Selection-Highlighting.
What: When the selection empties, the overlay fades over 200ms (gated by fadeOnClear, instant under reduced motion). This duration is a constant, not an option.
Why: It is a small interaction detail, not a tunable. fadeOnClear: false removes the fade entirely if you want an instant clear.
See Snapping-and-Overshoot for snap modes and Page-Highlighting for page scans.
What: A RegExp text query is scanned globally over a concatenated copy of the text; zero-width matches are skipped (the scanner nudges lastIndex to avoid an infinite loop). A sticky (y) flag is stripped before scanning.
Why: A zero-width match has no characters to wrap in a Range, and matching at a single position would loop forever. Anchor your pattern to consume at least one character.
What: highlightAll() marks any element matching [data-highlight]. Only the attribute's presence is read. There is no documented way to encode per-element color, shape, or any other option through the attribute value: every matched element gets the single options you passed to highlightAll().
Why: The declarative path is one handle covering all matches with one config. For per-element control, call highlight() per element with its own options, or drive the framework components. See Page-Highlighting.
What: Outside a DOM (SSR, workers), every entry point returns an inert no-op handle whose tier reads "css" and whose methods do nothing. The same inert handle is returned when a target resolves to zero ranges.
Why: The library is SSR-safe by returning a handle you can call without branching. Resolution is deterministic, so server and client agree. For the DOM-free pure subset (geometry, RNG, config), import @highlighters/core/path. See SSR-Support.
What: handle.update(opts) re-resolves options and re-renders without re-seeding the stable per-mark geometry. Retuning color, opacity, ink, or edges leaves the mark exactly where it is. To get fresh organic geometry you must remove() and re-highlight() (or pass a new seed).
Why: This is intentional: a mark that is already down should not jump or reshape as you tune it. Determinism means the same seed always yields the same geometry. See API-Reference.
What: On the live-selection handle, update(opts) composes additively across calls via mergeOptions: each call layers over the accumulated options rather than replacing them. You cannot "unset" a previously-set field by omitting it; you must overwrite it with a new value.
Why: Re-spreading the construction-time options on every selectionchange would revert prior update() calls. Accumulation keeps user retuning sticky across the stream of selection events.
See the per-framework quick starts in Getting-Started and Which-API-Should-I-Use.
What: <Highlight> (React and Vue) renders one element (default <span>) and marks its text content. It is not a way to target an arbitrary external node from markup. To mark something you do not render, use the hook/composable with a ref or a core Target, or call highlight() directly.
Why: The component is a thin convenience over "render this element, then highlight it." Cross-tree targeting is the hook's job.
What: @highlighters/svelte exports the highlight action only. There is no <Highlight> component wrapper.
Why: A Svelte use: action is the idiomatic primitive and covers the element-content case directly. Apply it with <p use:highlight={options}>. See Getting-Started.
What: In React, option changes are pushed via handle.update() keyed by JSON.stringify(options). Non-serializable option values (functions, anything that does not survive JSON.stringify) will not reliably trigger an update.
Why: The string key is a cheap change detector across renders. All real HighlightOptions fields are serializable (colors, numbers, enums, plain config objects), so this only bites if you smuggle a non-plain value through.
The marks are non-interactive overlays (or, on Tier C, native ::highlight() paint). A few constraints fall out of the browser, not the library.
What: Marks reposition on reflow and web-font load via a reflow observer. If the targeted text is moved into a display: none subtree, removed, or otherwise made unmeasurable, the mark has nothing to track and the overlay will not follow it until the text is measurable again.
Why: Geometry is built from the live rects of the text's Ranges. No box, no band. This is the same constraint any overlay measuring getBoundingClientRect() lives with. See Accessibility-and-Reflow.
What: On the SVG and CSS tiers the overlay mounts into the host (default document.body). Inside a transformed, scrolling, or stacked container you must pass that positioned element as the host argument so the overlay tracks the text; a static host is promoted to position: relative.
Why: An absolutely positioned overlay needs a positioned ancestor in the right coordinate space. Tier C sidesteps this entirely (it paints natively with no overlay DOM). See Getting-Started and How-It-Works.
| Option group | SVG (Tier A) | CSS (Tier B) | Highlight API (Tier C) |
|---|---|---|---|
color, opacity
|
yes | yes | yes (opacity folded into fill) |
blendMode |
yes | yes | no |
gradient |
yes | yes | no |
edge.radius |
yes | yes | no |
edge.waviness / roughness / cap
|
yes | no | no |
ink (streak, feather, dryout, ...) |
yes | no | no |
glow |
yes | no | no |
animation (draw-on) |
yes | yes | no |
tip.width / tip.thickness
|
no (schema-only) | no | no |
contrastBackground |
no (unused) | no | no |
See Options-Reference for every field and default, and FAQ for common questions.
Getting Started
The Mark
- Mark-Types-and-Shapes
- Tips-and-Edges
- Ink-and-Optics
- Color-and-Palettes
- Snapping-and-Overshoot
- Animation
Targeting
Reference
Production