rough-notation

A small JavaScript library that draws and animates hand-sketched annotations — underlines, circles, boxes, highlights — over any DOM element.

Library
npm
v0.5.1
9,685stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
37/100Needs Attention
Development Activity0
Maintenance0
Community48
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
65/100Good
Architecture68
Code Quality45
Innovation62
Learning Curve85

Rough Notation lets you call annotations into existence on a web page the way a person would mark up a printed page with a pen: sketchy underlines, circles, boxes, highlights, strike-throughs, crossed-off marks, and brackets, all rendered as hand-drawn SVG paths rather than static CSS decoration. Under the hood it delegates the actual sketchy-line generation to RoughJS, then wraps that output in a small stateful class that inserts an SVG sibling next to the annotated element, tracks the element’s bounding rect, and re-renders on resize.

Because each annotation is an independent object with show(), hide(), and remove() methods, and because groups of annotations can be sequenced with annotationGroup(), the library is commonly reached for in documentation sites, product tours, and marketing pages that want to draw the reader’s eye to specific text or UI elements without shipping a full canvas-drawing or animation framework. At 3.83kb gzipped with zero runtime dependencies beyond RoughJS, it’s designed to be dropped into an existing page rather than adopted as infrastructure.

What You Get

  • Seven built-in annotation types (underline, box, circle, highlight, strike-through, crossed-off, bracket) covering the common ways to visually call out text or elements
  • An annotate(element, config) function returning a stateful annotation object with show(), hide(), remove(), and live-updatable style properties
  • annotationGroup() for sequencing multiple annotations’ entrance animations one after another instead of all at once
  • Automatic re-rendering on window resize and element size changes via a ResizeObserver, so annotations stay aligned with reflowed content
  • Configurable stroke animation (duration, iteration count, RTL direction) or a static, non-animated render mode
  • Multiline mode that draws a separate annotation per line box for inline text that wraps across multiple lines
  • Distributed as ESM, CJS, and an IIFE global build, so it can be pulled in via a bundler, a native <script type=module>, or a plain <script> tag from a CDN

Common Use Cases

  • Documentation and marketing sites drawing attention to a specific word or phrase as the reader scrolls past it
  • Product tours and onboarding flows that circle or underline a UI element to direct a new user’s focus
  • Presentation-style web pages and slide decks that want a hand-drawn, sketchy visual style instead of flat design
  • Interactive tutorials that sequence a series of annotations across a page using annotationGroup() to guide reading order
  • Framework wrapper libraries (React, Vue, Svelte, Angular, Web Components) that expose Rough Notation as a declarative component

Under The Hood

Architecture The library is organized as four small, single-purpose modules rather than one monolithic file: model.ts defines the shared types and config interfaces, render.ts contains the pure per-annotation-type drawing logic (switch statement over type, producing RoughJS op-sets that get converted to SVG path strings), keyframes.ts lazily injects a single shared CSS @keyframes rule into the document head, and rough-notation.ts holds the RoughAnnotationImpl class that owns the DOM lifecycle — inserting an SVG sibling element, wiring a ResizeObserver and window resize listener, tracking last-known element rects to decide whether a redraw is needed, and exposing show/hide/remove as the public state machine (unattachednot-showingshowing). annotationGroup() is a thin wrapper that just staggers each annotation’s internal _animationDelay before calling show() on each in turn. If the core RoughAnnotationConfig shape changed, both render.ts’s switch statement and the public RoughAnnotation interface would need to change in lockstep, since there’s no abstraction layer between configuration and rendering.

Tech Stack Written in TypeScript and compiled with tsc plus Rollup (via rollup-plugin-node-resolve and rollup-plugin-terser) into three distinct output formats — an ES module, a CommonJS build, and an IIFE global bundle — declared via main/module/types fields in package.json. The only runtime dependency is RoughJS, from which it imports the low-level line, rectangle, ellipse, and linearPath renderer primitives directly (not RoughJS’s higher-level canvas API), plus its randomSeed helper for deterministic-looking randomness per annotation instance. There is no framework dependency — the library targets the raw DOM and browser SVG/ResizeObserver APIs.

Code Quality No test suite exists — the test script in package.json is a stub that prints an error and exits non-zero, and no test files are present anywhere in the repository. Linting is configured via tslint.json (now a deprecated tool, superseded industry-wide by ESLint + typescript-eslint), and TypeScript’s compiler is run in a fairly permissive configuration. Naming is consistent and the code favors small private methods with single responsibilities, but the near-total absence of automated tests means correctness relies entirely on manual verification and the project’s now-infrequent maintenance cadence.

What Makes It Unique Rough Notation’s specific niche is treating annotation as a first-class animated DOM object rather than a CSS effect: because each stroke is an actual SVG path with a real geometric length, the library can animate stroke-dashoffset proportionally across multiple paths and multiple annotations, producing a continuous hand-drawn-in-real-time effect that plain text-decoration or box-shadow tricks cannot replicate. Delegating shape generation to RoughJS gives it a distinctive sketchy, imperfect line quality that differentiates it visually from typical UI highlight/underline treatments, and its annotation-group sequencing primitive is a small but genuinely useful abstraction most comparable libraries don’t offer.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search