-
-
Notifications
You must be signed in to change notification settings - Fork 3
Tips and Edges
Every mark @highlighters draws is one band primitive: a parallelogram whose two long sides are threaded through a wavy grid and whose four corners are rounded arcs. Two option groups shape that primitive. tip is the nib: the slant of the chisel, how far the stroke runs past the text, and the per-end variance that keeps repeated marks from looking stamped. edge is the fray: how wavy and rough the long sides are, how the ends are capped, and how tight the corners round.
This page covers both groups in full. For ink texture (flow, dryout, streakiness) see Ink-and-Optics; for how the overshoot interacts with boundary snapping see Snapping-and-Overshoot.
highlight("#intro", {
tip: { type: "chisel", angle: 35, overshoot: 4 },
edge: { waviness: 1.5, frequency: 18, roughness: 0.3, cap: "round" },
});A real highlighter has a felt nib with a broad face and a narrow edge. Drag it along a line and the broad face leaves a band whose top edge leads or trails the bottom edge by a constant slant. That slant, and the ink that pools where you press down past the start and end of a word, is what tip reproduces.
| Value | Shape |
|---|---|
"chisel" (default) |
Slanted parallelogram. The top edge leads the bottom by a slant derived from angle. The classic broad-tip marker. |
"bullet" |
Square (no slant) with fully rounded caps. The corner radius is forced to the maximum the box allows, so the ends read as half-circles regardless of edge.radius. |
"fine" |
Square (no slant) with tight corners. The radius is capped at a quarter of the band height, for a crisp pen-like stroke. |
Only chisel produces a slant. bullet and fine are upright bands; they differ in how their corners round (see edge.radius below, which bullet/fine partly override).
highlight(".a", { tip: { type: "chisel" } }); // broad slanted marker
highlight(".b", { tip: { type: "bullet" } }); // rounded-end marker
highlight(".c", { tip: { type: "fine" } }); // crisp penangle is the nib angle relative to the stroke, in degrees. The renderer turns it into an absolute-px horizontal shift of the top edge over the bottom edge: larger angle, more lean. The shift is computed from the angle and the band height and capped at half the band width so the parallelogram can never invert. width and thickness on tip exist in the type but are reserved and not consumed by the renderer; the lean comes from angle and the band geometry alone, so there is no nib-width control.
angle only affects chisel. It is ignored by bullet and fine.
highlight(".steep", { tip: { type: "chisel", angle: 55 } });
highlight(".flat", { tip: { type: "chisel", angle: 10 } });overshoot is the signed number of pixels each outer end of a run extends past the text edge. Press a real marker down and it bleeds a little before and after the word; positive overshoot reproduces that. A negative value pulls the mark in short of the text. Inner edges of a wrapped run always overlap regardless, so a multi-line highlight stays continuous.
overshootJitter (>= 0) is the per-end deterministic variance on overshoot: each end gets overshoot + jitter * noise, seeded by the mark seed so the same content always lands the same way but adjacent marks do not look stamped from one template.
highlight(".loose", { tip: { overshoot: 6, overshootJitter: 3 } });
highlight(".tight", { tip: { overshoot: -2, overshootJitter: 0 } });Overshoot and snapping are coupled. See Snapping-and-Overshoot for how snap decides where the text edge sits before overshoot is applied.
angleJitter (>= 0, degrees) adds deterministic per-line variance to the chisel angle, so a paragraph that wraps over several lines does not draw every line at exactly the same lean. It is 0 by default and only meaningful for chisel.
| Field | Type | Default | Range | Meaning |
|---|---|---|---|---|
type |
TipType |
"chisel" |
"chisel" | "bullet" | "fine" |
Nib shape. |
width |
number |
16 |
px | Reserved. Not consumed by the renderer. |
thickness |
number |
4 |
px | Reserved. Not consumed by the renderer. |
angle |
number |
35 |
degrees | Chisel slant. Ignored by bullet/fine. |
overshoot |
number |
2 |
signed px | Px each outer end runs past the text edge (negative pulls in short). |
overshootJitter |
number |
1 |
px, >= 0
|
Per-end deterministic variance of overshoot. |
angleJitter |
number |
0 |
degrees, >= 0
|
Per-line deterministic variance on the chisel angle. |
TipTypeis"chisel" | "bullet" | "fine". There is no nib-width control:widthandthicknessare schema-only.
The long sides of the band are not straight lines. Each side is threaded through a row of vertices on a fixed spatial grid, displaced up and down to read like ink soaking unevenly into paper. Set waviness and roughness to 0 and the edge collapses to clean geometric lines; raise them and the band frays.
The grid is anchored in absolute pixels and seeded by grid index, so the wobble is width-independent: growing a mark (during the draw-on entrance, or as a live selection extends) appends fresh vertices without shifting the ones already drawn. The edge never swims.
waviness is the peak wave displacement from the baseline, in absolute px. It is the headline frayed-edge dial: 0 gives a ruler-straight side, larger values give a more hand-drawn wobble. Paper absorbency amplifies it; a wetter paper (see Ink-and-Optics) grows the amplitude so the edge softens as it wicks.
frequency is the px spacing (segmentLength) between adjacent grid vertices, i.e. the wavelength. Smaller frequency means denser, busier waves; larger means longer, lazier swells. It is in pixels, not cycles, and is width-independent: the same frequency produces the same wavelength on a short word and a full line.
highlight(".busy", { edge: { waviness: 2, frequency: 10 } }); // tight chop
highlight(".lazy", { edge: { waviness: 2, frequency: 40 } }); // long swellsroughness (0-1) layers high-frequency micro-jitter on top of the base wave, up to roughly 30% of the wave amplitude. At 0 the edge is a smooth wobble; near 1 it reads frayed and fibrous rather than sinusoidal. It scales with waviness, so a waviness of 0 has no roughness to add to.
cap is the end-cap style of the band's leading and trailing edges.
| Value | End shape |
|---|---|
"round" (default) |
Rounded ends, using edge.radius. |
"flat" |
Square ends. Forces the corner radius to 0.
|
"square" |
Square ends. Forces the corner radius to 0.
|
Both "flat" and "square" zero out the corner radius, so a flat/square cap is a hard-cornered band no matter what radius you pass.
radius is the corner radius in absolute px, automatically clamped against short marks so the four corner arcs always fit (it can never exceed half the usable width or half the height). How it is used depends on tip.type and cap:
-
cap: "flat"orcap: "square"— radius is forced to0(hard corners). -
tip.type === "bullet"— radius is forced to the maximum the box allows (fully rounded ends), ignoring the value you pass. -
tip.type === "fine"— radius ismin(your radius, the geometric ceiling, height * 0.25): tight corners. -
tip.type === "chisel"(round cap) — radius is your value, clamped to the geometric ceiling.
highlight(".soft", { tip: { type: "chisel" }, edge: { cap: "round", radius: 8 } });
highlight(".hard", { edge: { cap: "flat" } }); // radius ignored, square corners| Field | Type | Default | Range | Meaning |
|---|---|---|---|---|
waviness |
number |
1 |
absolute px | Peak wavy-edge displacement. 0 is straight. |
frequency |
number |
22 |
px | Wavelength (segmentLength) between grid vertices. Smaller is denser. Width-independent. |
roughness |
number |
0.2 |
0-1
|
High-frequency micro-jitter on the base wave. |
cap |
EdgeCap |
"round" |
"flat" | "round" | "square" |
End-cap style. flat/square force radius 0. |
radius |
number |
5 |
absolute px (clamped) | Corner radius. Overridden by bullet (max) and fine (tight). |
EdgeCapis"flat" | "round" | "square".
The tip and edge groups are plain option objects, so they read identically across every binding. They merge field-wise under update() and across mergeOptions, so you can patch a single field without restating the group. See Options-Reference for the full merge semantics.
import { highlight } from "@highlighters/core";
const h = highlight("#headline", {
tip: { type: "chisel", angle: 40, overshoot: 5, overshootJitter: 2 },
edge: { waviness: 1.8, frequency: 16, roughness: 0.4, cap: "round", radius: 6 },
});
// Field-wise patch: only the slant changes, everything else is preserved.
h.update({ tip: { angle: 20 } });import { Highlight } from "@highlighters/react";
function Headline() {
return (
<Highlight
as="h1"
options={{
tip: { type: "bullet", overshoot: 4 },
edge: { waviness: 2, frequency: 12, cap: "round" },
}}
>
Continuous deployment
</Highlight>
);
}import { useRef } from "react";
import { useHighlight } from "@highlighters/react";
function Term() {
const ref = useRef<HTMLElement>(null);
useHighlight(ref, {
tip: { type: "fine" },
edge: { waviness: 0, roughness: 0, cap: "flat" }, // crisp, ruler-straight
});
return <em ref={ref}>idempotent</em>;
}<script setup lang="ts">
import { Highlight } from "@highlighters/vue";
</script>
<template>
<Highlight
as="h2"
:options="{
tip: { type: 'chisel', angle: 35, angleJitter: 6 },
edge: { waviness: 1.5, frequency: 20, roughness: 0.3 },
}"
>
Per-line slant variance
</Highlight>
</template><script lang="ts">
import { highlight } from "@highlighters/svelte";
</script>
<p use:highlight={{
tip: { type: "chisel", overshoot: 6, overshootJitter: 3 },
edge: { waviness: 2, frequency: 14, cap: "round", radius: 7 },
}}>
A loose, bleeding marker stroke.
</p>Crisp pen, no fray. Square corners and zero wobble for a precise, technical look.
highlight(".code-term", {
tip: { type: "fine" },
edge: { waviness: 0, roughness: 0, cap: "flat" },
});Rounded marker. Soft half-circle ends, gentle wave.
highlight(".callout", {
tip: { type: "bullet", overshoot: 4 },
edge: { waviness: 1.2, frequency: 22, cap: "round" },
});Loose hand-drawn chisel. Steep slant, generous overshoot, busy frayed edge.
highlight(".scribble", {
tip: { type: "chisel", angle: 50, overshoot: 7, overshootJitter: 4, angleJitter: 8 },
edge: { waviness: 2.4, frequency: 12, roughness: 0.6, cap: "round", radius: 6 },
});For more end-to-end patterns combining tip, edge, ink, and animation, see Recipes.
-
tip.width/tip.thicknessdo nothing. They are part of the type for completeness but the renderer ignores them. The nib lean comes fromangleand band geometry. Do not build a UI control around them. -
flatandsquarecaps ignoreradius. If you want a corner radius, usecap: "round". -
bulletandfineoverrideedge.radius.bulletforces maximum rounding;fineclamps to a tight corner. Only achiselwith a round cap usesradiusas-given. -
Everything is deterministic.
overshootJitter,angleJitter, and the edge wobble all derive from the mark seed, never wall-clock. Same content plus same options produces byte-identical geometry on server and client. See SSR-Support and How-It-Works. -
Geometry survives degrade. Dropping to a lower renderer tier simplifies edge organicness and texture, but never moves or recolours a mark. A
flat-capped, straight-edged mark looks the same on every tier. See Performance.
- Ink-and-Optics — flow, viscosity, feathering, dryout, and how paper absorbency amplifies the edge.
-
Snapping-and-Overshoot — where the text edge sits before
overshootextends past it. - Mark-Types-and-Shapes — highlight, underline, overline, strike-through, and how the band is positioned.
- Options-Reference — the full option table and field-wise merge rules.
- Animation — the draw-on entrance that grows the band by appending edge vertices.
- How-It-Works — the band primitive, the absolute-px grid, and determinism.
Getting Started
The Mark
- Mark-Types-and-Shapes
- Tips-and-Edges
- Ink-and-Optics
- Color-and-Palettes
- Snapping-and-Overshoot
- Animation
Targeting
Reference
Production