Create and animate hand-drawn annotations on any HTML element the way you'd mark up paper.
Notatio uses RoughJS to underline, box, circle, highlight, strike through, cross off, bracket or squiggle anything already on the page. Annotations follow the text, in horizontal and vertical writing modes alike. It has no runtime dependencies and ships as an ES module with TypeScript types.
| Type | Draws |
|---|---|
underline |
A sketchy line alongside the element. |
box |
A box around the element. |
circle |
A circle around the element. |
highlight |
A highlighter effect behind the element. |
strikethrough |
A line through the middle of the element. |
crossed-off |
An X across the element. |
bracket |
A bracket beside the element, usually a paragraph. |
wavy |
An underline along a sine wave, for a spellchecker look. |
zigzag |
Wavy, but with sharp corners instead of curves. |
npm install notatio
pnpm add notatio
yarn add notatio
bun add notatio
deno add npm:notatioOr load the ES module straight from a CDN:
<script type="module">
import { annotate } from 'https://unpkg.com/notatio';
</script>Pass the element to annotate and a config describing the style, then call show().
import { annotate } from 'notatio';
const element = document.querySelector('#myElement');
const annotation = annotate(element, {
type: 'underline',
color: '#e11d48',
strokeWidth: 3,
iterations: 3,
padding: [2, 0],
animationDuration: 1200,
animate: { onHide: true },
});
annotation.show();Certain options can be changed afterwards, causing the visible annotation to redraw itself.
annotation.color = 'seagreen';Annotations can also draw themselves the first time the element is scrolled into the view.
annotate(element, {
type: 'highlight',
color: '#1e293b',
showOnVisible: { threshold: 0.5, repeat: true },
});Pass a Range (or a StaticRange, or a Selection) instead of an element to mark up a part of text in place, without
wrapping it in an element.
const paragraph = document.querySelector('#note').firstChild;
const range = new Range();
range.setStart(paragraph, 0);
range.setEnd(paragraph, 12);
annotate(range, { type: 'circle', color: 'rebeccapurple' }).show();See the reference for the trade-offs against an element target.
Annotations adapt to the element's writing-mode. In this example, the underline runs down the column, on its left
side.
<p id="tategaki" style="writing-mode: vertical-rl">縦書きのテキスト</p>annotate(document.querySelector('#tategaki'), { type: 'underline' }).show();You can group annotations to animate them one after another, in the given order.
import { annotate, annotationGroup } from 'notatio';
const heading = annotate(document.querySelector('#heading'), {
type: 'circle',
color: 'rebeccapurple',
padding: 12,
});
const note = annotate(document.querySelector('#note'), {
type: 'bracket',
brackets: ['left', 'right'],
strokeWidth: 2,
});
const typo = annotate(document.querySelector('#typo'), {
type: 'wavy',
color: 'red',
amplitude: 4,
frequency: 8,
});
await annotationGroup([heading, note, typo]).show();Notatio is framework agnostic: it takes an element and draws beside it. You only need to call remove() when the
component umounts.
import { useEffect, useRef } from 'react';
import { annotate } from 'notatio';
function Highlighted({ children }) {
const ref = useRef(null);
useEffect(() => {
const annotation = annotate(ref.current, { type: 'highlight', color: '#fde68a' });
annotation.show();
return () => annotation.remove();
}, []);
return <span ref={ref}>{children}</span>;
}An annotation is an absolutely positioned <svg> inserted next to the element, carrying aria-hidden="true" and
pointer-events: none, so it is out of the accessibility tree and never intercepts clicks, hover or selection. The
annotated element itself is left alone, except by highlight, which sets position: relative on it when it is
otherwise static and puts it back on remove(). Animations are disabled whenever prefers-reduced-motion: reduce
is set.
You have two responsibilities for accessibility:
- Put the meaning somewhere a screen reader can read it. A strikethrough that means "completed" or a wavy underline
that means "misspelled" is decoration to assistive technology, so the meaning has to live in the text or in an
aria-labelon the element. - Ensure the WCAG contrast of a highlight. It paints behind the text, so a dark highlight under dark text fails the WCAG contrast guideline.
Notatio targets ES2024 and uses ResizeObserver, MutationObserver, IntersectionObserver,
Element.getAnimations() and structuredClone().
The reference covers every annotation type, all configuration options, writing modes, the annotation and group objects, accessibility and styling hooks.
MIT.
Notatio is a fork of rough-notation by Preet Shihn, who also wrote RoughJS, which does the drawing. A couple of open issues and pull requests from the source repository were also applied for new features, improvements and optimizations.