notation
hand-drawn annotations for the web
Underlines, boxes, circles, highlights, strike-throughs, cross-offs, brackets and arrows, or a shape you write yourself. Each one is an animated SVG stroke over any element, re-measured when the text reflows. It takes a DOM element rather than a component, so it works in any framework, or none. Zero dependencies.
Installation
npm install @shardsui/notation Coding agents can install a SKILL.md that teaches them the API, and then take instructions
like /notation underline the page title.
npx skills add abdrizik/notation Usage
annotate() returns an Annotation. Nothing is drawn until you call show().
import { annotate } from '@shardsui/notation'
const a = annotate('#title', {
type: 'circle',
color: 'crimson',
padding: [6, 10]
})
a.show() The target is an element or a selector string. A selector matches every element it finds, so
annotate('.term', { type: 'underline' }) underlines all of them. A selector that matches nothing
warns and returns an annotation whose methods do nothing.
show(), hide() and update() return the annotation, so calls chain. finished settles when the
stroke lands.
Types
type is the only required option. Eight are built in.
annotate('#title', {
type: 'underline'
}) Options
Each option below shows the call that draws it.
annotate('#title', {
type: 'underline',
color: 'crimson',
strokeWidth: 5
}) const a = annotate('#el', {
type: 'underline',
color: 'var(--color-ink)',
})
a.show() Multiline
Wrapped text gets one mark per line box. Resize the window and the strokes follow the new wrap.
multiline and every line box in this wrapped paragraph gets a fresh path of its own, re-measured whenever
the text reflows around it rather than one stroke stretched across the whole block.annotate('#paragraph', {
type: 'underline',
multiline: true
}).show() Hiding
hide() draws the stroke backwards until nothing is left. Read showing to pick the direction.
A second show() during the retraction picks the pen up where it left off.
const flag = annotate('#clause', { type: 'box' })
control.onclick = () => (flag.showing ? flag.hide() : flag.show()) Custom shapes
type also takes a function. It receives the measured box and returns an array of strokes, each
one a list of points or a Spine. The built-in types use the same contract.
Build coordinates from rect.x and rect.y; the box is not at the origin.
annotate('#title', {
type: underline
})
function underline(rect) {
const y = rect.y + rect.h
const line = [
[rect.x, y],
[rect.x + rect.w, y]
]
return [line]
} annotate('#el', { type: squiggle }).show()
function squiggle(rect: Rect, { padding }: ShapeOptions): Point[][] {
const y = rect.y + rect.h + padding[2]
const waves = Math.round(rect.w / 26)
const points: Point[] = []
for (let i = 0; i <= waves * 8; i++) {
const t = i / (waves * 8)
points.push([rect.x + rect.w * t, y + Math.sin(t * waves * Math.PI * 2) * 3])
}
return [points]
} multiline is false for custom shapes, so your function gets the whole bounding box. Set it to
true to run once per line. The second argument carries the rest of the options, with padding
resolved to four numbers. To give a shape parameters, return it from a function.
Multiple targets
One call can cover several targets. Pass [target, config] pairs; the strokes draw in the order
you list them, each waiting for the one before it. hide() retracts them together.
  .show()
target  → circled   type → banded   call → underlined
const a = annotate([
['#target', { type: 'circle', padding: [2, 6] }],
['#type', { type: 'highlight' }],
['#call', { type: 'underline' }],
['#snippet', { type: 'box' }]
])
a.show() Frameworks
Draw when the element mounts, and call remove() when it unmounts.
import { annotate } from '@shardsui/notation'
export function Title() {
return (
<h1
ref={(node) => {
const a = annotate(node, { type: 'underline' })
a.show()
return () => a.remove()
}}
>
Hand-drawn annotations
</h1>
)
} API reference
AnnotationOptions
The second argument to annotate().
Annotation
One handle, however many elements the call matched. Its methods drive all of them together.
Spine
A stroke can be a bare points array. Use a Spine when it needs one of the other keys.
Browser support
Chrome and Edge 113, Firefox 115, and Safari 17.4 or later.