-
-
Notifications
You must be signed in to change notification settings - Fork 3
Ink and Optics
The mark you see is ink laid on paper, then composited against the page. This page covers the two halves of that model: the ink group (flow, viscosity, feathering, streakiness, dryout, flowFade, startEndBuildup), the paper it soaks into, and the optics that decide how the result blends with the text underneath (blendMode, opacity, glow).
Everything here is part of Options-Reference and resolves through resolveOptions. Ink fields are deterministic: the same seed (or the same target, when seed is omitted) produces byte-identical texture on server and client. See How-It-Works for the rendering pipeline and Color-and-Palettes for the colorant itself.
All ink fields are tier-dependent. The full texture (feathering, streaks, dryout gaps, end pooling) renders on the SVG tier. The CSS and highlight-api tiers honour
color,opacity, andblendModebut flatten the organic texture to a clean band. Degrade is fidelity-only: a lower tier never moves or recolours a mark. See Performance.
ink is a namespaced option group. Pass only the fields you want to change; the rest fall back to the defaults below (they merge field-wise, not all-or-nothing).
| Field | Type | Default | Range | Effect |
|---|---|---|---|---|
flow |
number |
0.45 |
0–1
|
Deposit amount. Raises band width and softens edges. |
viscosity |
number |
0.5 |
0–1
|
Inverse of flow. Raises edge sharpness and skip frequency. |
feathering |
number |
0.2 |
0–1
|
Capillary lateral spread at the edges. |
streakiness |
number |
0.25 |
0–1
|
Lengthwise lighter/darker lanes within a stroke. |
dryout |
number |
0.1 |
0–1
|
Probabilistic alpha gaps (skipping). Coupled to viscosity. |
startEndBuildup |
number |
0.1 |
-1–1
|
Deposit variation at stroke ends. Positive pools ink, negative suppresses end darkening. |
flowFade |
number |
0.5 |
0–1
|
Directional dry-out along each line: saturated at touchdown, drier toward the end. |
All fields are normalized 0–1 except startEndBuildup, which is signed (-1–1). Non-finite values fall back to the default.
These are the two primary knobs and they pull against each other. flow is how wet the nib is: more ink, wider band, softer edges. viscosity is the opposite tendency: thicker ink sits where it lands, so edges sharpen and the stroke skips more often. A wet marker is high flow / low viscosity; a near-dry one is the reverse.
import { highlight } from "@highlighters/core";
// Wet, generous marker
highlight(".lede", { ink: { flow: 0.85, viscosity: 0.2 } });
// Dry, scratchy marker
highlight(".aside", { ink: { flow: 0.2, viscosity: 0.85, dryout: 0.4 } });Lateral capillary spread at the long edges of the band. At 0 the edge stays where the geometry puts it; higher values let ink wick sideways, softening and widening the boundary. Feathering also responds to the paper (paper.absorbency below): a thirstier sheet wicks more for the same feathering value.
Lengthwise lanes of lighter and darker ink running along the stroke, the way a real chisel nib lays uneven coverage. At 0 the fill is even.
Probabilistic gaps where the nib skips the paper and leaves bare patches. Coupled to viscosity: thicker ink skips more, so raising viscosity raises the effective skipping even at a fixed dryout.
Deposit variation at the two ends of each stroke, where a real pen pauses on touchdown and lift-off.
- Positive pools ink at the ends (darker caps), like a pen pressing in and out.
-
0is neutral. - Negative suppresses end darkening, for a stroke that fades cleanly off each edge.
Directional dry-out along the line: the stroke is wettest at touchdown (the leading edge) and dries toward the trailing end. It follows stroke direction, so a backward drag in Selection-Highlighting fades from the right edge instead.
| Look | flow |
viscosity |
feathering |
streakiness |
dryout |
flowFade |
|---|---|---|---|---|---|---|
| Default | 0.45 |
0.5 |
0.2 |
0.25 |
0.1 |
0.5 |
| Wet fountain pen | 0.85 |
0.2 |
0.35 |
0.1 |
0.0 |
0.3 |
| Dry chalk marker | 0.25 |
0.8 |
0.1 |
0.5 |
0.45 |
0.7 |
| Clean, flat band | 0.5 |
0.5 |
0.0 |
0.0 |
0.0 |
0.0 |
| Heavy end-pooling | 0.6 |
0.4 |
0.2 |
0.2 |
0.1 |
0.4 + startEndBuildup: 0.8
|
// Dry chalk marker
highlight("h2", {
ink: { flow: 0.25, viscosity: 0.8, feathering: 0.1, streakiness: 0.5, dryout: 0.45, flowFade: 0.7 },
});
// Clean, flat band (texture off; edges still come from the `edge` group)
highlight("h2", {
ink: { flow: 0.5, viscosity: 0.5, feathering: 0, streakiness: 0, dryout: 0, flowFade: 0 },
});For the related edge geometry (waviness, roughness, caps, corner radius) see Tips-and-Edges. Ink controls coverage; edge controls the shape of the band boundary.
The surface the ink soaks into. One field today.
| Field | Type | Default | Range | Effect |
|---|---|---|---|---|
absorbency |
number |
0.3 |
0–1
|
Higher wicks more, growing feather and softening edges. |
absorbency is a multiplier on the wicking behaviour: a thirsty sheet (high absorbency) makes the same ink.feathering spread further and the edges read softer. Glossy stock (low absorbency) keeps ink tight to the geometry.
// Soft, blotter-paper edges
highlight("blockquote", {
ink: { feathering: 0.4 },
paper: { absorbency: 0.8 },
});
// Crisp, coated-stock edges
highlight("code", {
paper: { absorbency: 0.05 },
});Once ink is laid down, three options decide how it interacts with the text and background beneath it: blendMode, opacity, and the additive glow.
How the ink composites against whatever is behind it. This is the single most important optics knob, because it decides whether a yellow mark over black text reads as a real highlighter (text stays legible) or paints over it.
BlendMode |
Optics | Behaviour over text |
|---|---|---|
"multiply" (default) |
Subtractive | Darkens. Ink tints the page; dark text shows through. This is how a real highlighter works. |
"darken" |
Per-channel min | Keeps the darker of ink and backdrop per channel. Text that is darker than the ink survives. |
"color-burn" |
Aggressive darken | Deeper, more saturated darkening than multiply. |
"overlay" |
Contrast | Multiply in shadows, screen in highlights. Punchier, can shift hue. |
"screen" |
Additive lighten | Lightens the backdrop. For glow-like / dark-mode marks, not classic highlighting. |
"normal" |
Opaque source-over | Paints the ink straight on top. With high opacity this hides the text; pair with low opacity. |
// Classic marker over light text (default)
highlight("p", { color: "#fff14d", blendMode: "multiply" });
// Dark-mode glow band: lighten instead of darken
highlight("p", { color: "#5ad7ff", blendMode: "screen", opacity: 0.4 });With multiply (and the other subtractive modes), overlapping marks darken where they cross, exactly like passing a highlighter over the same words twice. This is a property of the compositing model, not a separate option. Two marks of the same colour over the same run will read noticeably deeper at the overlap.
Within a single wrapped run the renderer already overlaps the inner edges of adjacent line-bands (see tip.overshoot in Tips-and-Edges), so multi-line marks stay continuous around the wrap without a seam.
If you want stacked marks not to darken, switch to "normal" and lower opacity, or use "darken" (which clamps to the per-channel minimum rather than accumulating).
| Option | Type | Default | Range | Effect |
|---|---|---|---|---|
opacity |
number |
0.55 |
0–1 (clamped) |
Overall ink alpha. |
Top-level alpha for the whole mark, clamped to 0–1. The default 0.55 keeps text legible under a multiply mark. Raise it for a denser tint; lower it when using "normal" so the ink does not bury the text.
Per-stop alpha is separate: a gradient stop carries its own opacity that multiplies against this top-level value. See Color-and-Palettes.
The ink colour itself is set by color / palette / gradient, documented in full in Color-and-Palettes. In brief:
highlight("p", { color: "#f5e6a8" }); // CSS color string
highlight("p", { color: { palette: "mild", swatch: "pink" } }); // palette swatch
highlight("p", { palette: "fluorescent" }); // family default swatchcolor accepts any CSS color string (hex, rgb(), hsl(), named, currentColor, var(...)) or a { palette, swatch } reference. An empty or whitespace-only string is treated as unset and falls back to the palette default (mild -> yellow = #f5e6a8).
Not a real option: some framework JSDoc snippets show
preset(e.g.{ preset: "wet" }) or a bare named colour like"pink"as a palette shortcut. Neither exists. There is nopresetfield anywhere in the API, and a bare"pink"is passed straight through as a CSS color (the literal named colourpink), not a palette lookup. To pick a palette swatch use the object form{ palette, swatch }. Build "presets" yourself by spreading an options object, see the recipe at the end.
Additive emission painted over the multiply ink, for a fluorescent-marker bloom. Off by default.
| Field | Type | Default | Range | Effect |
|---|---|---|---|---|
enabled |
boolean |
false |
— | Master enable. |
intensity |
number |
0.5 |
0–1
|
Additive emission strength. |
spread |
number |
4 |
px | Bloom spread radius. |
color |
ColorValue |
"" |
CSS color | Emission hue. Empty string resolves to a brightened form of the ink colour at render time. |
Leaving glow.color empty ("") lets the renderer derive a brightened version of the mark's ink colour, so the bloom matches the highlighter automatically. Set it explicitly for a contrasting halo.
// Fluorescent marker with an auto-matched bloom
highlight(".key-term", {
palette: "fluorescent",
glow: { enabled: true, intensity: 0.7, spread: 6 },
});
// Cyan ink, custom magenta halo
highlight(".key-term", {
color: "#5ad7ff",
glow: { enabled: true, intensity: 0.6, color: "#ff6fae" },
});Glow is part of the SVG-tier texture: on the CSS and highlight-api tiers the bloom is dropped along with the rest of the organic detail. Like all motion-adjacent richness, plan for it to be absent under degrade. See Performance and Accessibility-and-Reflow.
The ink and optics options are plain HighlightOptions fields, so they pass through every binding identically.
import { Highlight } from "@highlighters/react";
export function Lede({ children }: { children: React.ReactNode }) {
return (
<Highlight
options={{
color: { palette: "fluorescent", swatch: "yellow" },
blendMode: "multiply",
opacity: 0.6,
ink: { flow: 0.85, viscosity: 0.2, feathering: 0.35 },
paper: { absorbency: 0.6 },
glow: { enabled: true, intensity: 0.7 },
}}
>
{children}
</Highlight>
);
}import { useRef } from "react";
import { useHighlight } from "@highlighters/react";
function KeyTerm() {
const ref = useRef<HTMLSpanElement>(null);
useHighlight(ref, {
color: "#5ad7ff",
blendMode: "screen",
ink: { dryout: 0.3, streakiness: 0.4 },
});
return <span ref={ref}>capillary action</span>;
}<script setup>
import { Highlight } from "@highlighters/vue";
const options = {
palette: "fluorescent",
blendMode: "multiply",
opacity: 0.6,
ink: { flow: 0.85, viscosity: 0.2 },
paper: { absorbency: 0.6 },
glow: { enabled: true, intensity: 0.7 },
};
</script>
<template>
<Highlight :options="options">market-defining release</Highlight>
</template><script>
import { highlight } from "@highlighters/svelte";
</script>
<p use:highlight={{
color: "#f5e6a8",
blendMode: "multiply",
ink: { flow: 0.6, startEndBuildup: 0.8, flowFade: 0.4 },
paper: { absorbency: 0.8 },
}}>
Ink pools at the ends of this stroke.
</p>import { highlight } from "@highlighters/core";
highlight(".aside", {
blendMode: "darken",
opacity: 0.5,
ink: { flow: 0.2, viscosity: 0.85, dryout: 0.4 },
paper: { absorbency: 0.05 },
});The speed group modulates ink wetness from drag velocity: a faster swipe deposits less ink, a deceleration into a line end pools it there. It engages only under highlightSelection during a primary-button fine-pointer (mouse/pen) drag, is suppressed under prefers-reduced-motion, and is a byte-identical no-op for programmatic, keyboard, touch, and static highlights. It is off by default (speed.enabled: false). Full field table and behaviour live in Selection-Highlighting and Options-Reference.
There is no preset option, but a preset is just an options object you spread. Define them once and reuse:
import { highlight, type HighlightOptions } from "@highlighters/core";
const WET: HighlightOptions = {
blendMode: "multiply",
opacity: 0.6,
ink: { flow: 0.85, viscosity: 0.2, feathering: 0.35, dryout: 0 },
paper: { absorbency: 0.7 },
};
const DRY: HighlightOptions = {
blendMode: "multiply",
opacity: 0.5,
ink: { flow: 0.2, viscosity: 0.85, streakiness: 0.5, dryout: 0.45 },
paper: { absorbency: 0.05 },
};
highlight(".lede", { ...WET, color: { palette: "fluorescent", swatch: "yellow" } });
highlight(".aside", { ...DRY, color: { palette: "vintage", swatch: "rust" } });Because the namespaced groups (ink, paper, glow, ...) merge field-wise via mergeOptions, you can layer a base preset and override a single field:
highlight(".lede", { ...WET, ink: { ...WET.ink, dryout: 0.2 } });More patterns in Recipes.
- Color-and-Palettes: the colorant, palettes, swatches, and gradients
- Tips-and-Edges: nib shape and the edge geometry that bounds the ink
- Selection-Highlighting: where speed-aware ink engages
- How-It-Works: the render pipeline and how ink texture is built
- Performance: tier degrade and which ink fields survive it
- Options-Reference: every field, default, and range in one place
Getting Started
The Mark
- Mark-Types-and-Shapes
- Tips-and-Edges
- Ink-and-Optics
- Color-and-Palettes
- Snapping-and-Overshoot
- Animation
Targeting
Reference
Production