npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

gritty-notation

v0.2.0

Published

A modern, performant library for creating and animating hand-drawn annotations on web pages inspired by rough notation

Readme

Gritty Notation

npm version License: MIT TypeScript

A modern, performant library for creating and animating hand-drawn annotations on web pages.

Inspired by rough-notation but rebuilt from the ground up with modern tooling, enhanced features, and improved developer experience.

🎨 Interactive Documentation & Examples

Explore our comprehensive Storybook documentation with live examples, interactive controls, and ready-to-use recipes.

✨ Features

  • 12 Annotation Types: Underline, box, circle, highlight, strike-through, crossed-off, bracket, wavy, scallop, zigzag, checkmark, and x-mark
  • Dual Position Support: Apply decorative annotations to multiple positions simultaneously (top, bottom, left, right)
  • Inverted Patterns: Mirror wave patterns for creative effects
  • Multiline Support: Annotations adapt to text that wraps across multiple lines
  • Vertical Text Support: Automatic detection and proper rendering for vertical writing modes
  • Annotation Groups: Coordinate multiple annotations with sequential animations
  • Smooth Animations: CSS-based animations with customizable duration and easing
  • Event Callbacks: Lifecycle hooks (onShow, onHide, onRemove, onError)
  • Annotation Presets: Built-in + custom presets for common use cases
  • Scroll-Triggered: IntersectionObserver support for show-on-scroll
  • Interactive Animations: Hover interactions, loop/pulse effects, and playback controls
  • Flexible Spacing: Independent paddingX and paddingY controls for precise positioning
  • Comprehensive Documentation: Interactive Storybook with live examples and recipes
  • TypeScript First: Full type safety and excellent IntelliSense support
  • SSR Compatible: Works seamlessly with server-side rendering (Next.js, SvelteKit, etc.)
  • Lightweight: ~42KB minified, zero dependencies except roughjs
  • Framework Agnostic: Works with vanilla JavaScript, React, Vue, Svelte, and more
  • Accessible: Built with accessibility in mind
  • Modern Build: ESM and CJS support, optimized for modern bundlers

📦 Installation

npm install gritty-notation
# or
yarn add gritty-notation

🚀 Quick Start

import { annotate } from 'gritty-notation';

const element = document.querySelector('.highlight-me');
const annotation = annotate(element, {
  type: 'underline',
  color: '#ff0000',
  animate: true,
});

annotation.show();

📖 API

annotate(element, config)

Creates an annotation for the specified element.

Parameters:

  • element: HTMLElement - The DOM element to annotate
  • config: AnnotationConfig - Configuration options

Returns: Annotation

Configuration Options

interface AnnotationConfig {
  // Either type OR preset is required
  type?: 'underline' | 'box' | 'circle' | 'highlight' | 'strike-through' |
         'crossed-off' | 'bracket' | 'wavy' | 'scallop' | 'zigzag' | 'checkmark' | 'x-mark';
  preset?: 'error' | 'success' | 'warning' | string;

  color?: string;              // CSS color (default: 'currentColor')
  strokeWidth?: number;        // Line thickness (default: 1)
  padding?: Padding;           // Padding around element (default: 0)
  animate?: boolean | AnimationConfig; // Animation settings (default: true)
  iterations?: number;         // Roughness iterations (default: 2)
  multiline?: boolean;         // Support multiline text (default: false)
  rtl?: boolean;               // Right-to-left support (default: false)
  brackets?: BracketPosition | BracketPosition[]; // For bracket type
  position?: UnderlinePosition | UnderlinePosition[]; // Position(s) for underline/decorative types

  // Wavy, Scallop, and Zigzag annotation options
  amplitude?: number;          // Wave/peak height (default: varies by type)
  frequency?: number;          // Waves/peaks per 100px (default: varies by type)
  inverted?: boolean | string | string[]; // Invert pattern direction (default: false)
                               // Can be boolean, position string, or array of positions

  // Checkmark and X-Mark annotation options
  checkmarkBox?: boolean;      // Show box around mark (checkbox style)
  size?: number;               // Mark size in pixels (default: 16)

  // Independent padding controls for box, circle, and highlight
  paddingX?: number;           // Horizontal padding (overrides padding)
  paddingY?: number;           // Vertical padding (overrides padding)

  // Interactive animations
  showOnHover?: boolean;       // Show annotation on mouse hover (default: false)
  animateOnMove?: boolean;     // Progressive reveal as mouse moves (default: false)
  animateOnLeave?: boolean;    // Reverse animation when mouse leaves (for animateOnMove, default: false)
  loop?: boolean | number;     // Loop animation (true or interval in ms, default: 3000ms)
  loopDelay?: number;          // Delay between hide and show during loop (default: 100ms)
  pulse?: boolean | number;    // Pulse effect (true or interval in ms, default: 2000ms)
  hideAnimation?: boolean | AnimationConfig; // Animate when hiding (default: false)
  reverseOnHide?: boolean;     // Play show animation in reverse when hiding (default: false)
  hoverDelay?: number;         // Delay in ms before showing on hover (default: 0)
  hideDelay?: number;          // Delay in ms before hiding on mouse leave (default: 0)

  // Scroll-triggered annotations (IntersectionObserver)
  showOnVisible?: boolean;     // Auto-show when scrolled into view (default: false)
  visibilityOptions?: {
    threshold?: number;        // 0-1, visibility percentage (default: 0.5)
    rootMargin?: string;       // Viewport margin (default: '0px')
    triggerOnce?: boolean;     // Only trigger once (default: true)
  };

  // Event callbacks
  onBeforeShow?: () => void;
  onShow?: () => void;
  onShowing?: (progress: number) => void;
  onHide?: () => void;
  onRemove?: () => void;
  onError?: (error: Error) => void;
}

// Padding can be specified in multiple ways:
type Padding =
  | number                     // Same padding on all sides: 5
  | [number, number]           // Vertical, Horizontal: [5, 10]
  | [number, number, number, number]; // Top, Right, Bottom, Left: [5, 10, 5, 10]

Annotation Methods

  • show(): Promise<void> - Show the annotation with animation
  • hide(): Promise<void> - Hide the annotation (with optional animation)
  • remove(): void - Remove annotation and clean up
  • isShowing(): boolean - Check if annotation is visible
  • updateColor(color: string): void - Update annotation color
  • updateStrokeWidth(width: number): void - Update stroke width
  • pause(): void - Pause the current animation
  • resume(): void - Resume paused animation
  • replay(): Promise<void> - Hide and show again (replay animation)

annotationGroup(annotations)

Group multiple annotations for coordinated display with sequential animations.

Parameters:

  • annotations: Annotation[] - Array of annotation instances

Returns: AnnotationGroup

Methods:

  • show(): Promise<void> - Show all annotations sequentially
  • hide(): Promise<void> - Hide all annotations

🎨 Examples

Basic Annotations

import { annotate } from 'gritty-notation';

// Underline
const underline = annotate(element, {
  type: 'underline',
  color: '#3498db',
  strokeWidth: 2
});

// Box
const box = annotate(element, {
  type: 'box',
  color: '#e74c3c',
  padding: 10
});

// Circle
const circle = annotate(element, {
  type: 'circle',
  color: '#9b59b6',
  padding: [5, 15]
});

// Highlight
const highlight = annotate(element, {
  type: 'highlight',
  color: '#f39c12'
});

// Strike-through
const strikeThrough = annotate(element, {
  type: 'strike-through',
  color: '#95a5a6'
});

// Crossed-off
const crossedOff = annotate(element, {
  type: 'crossed-off',
  color: '#e74c3c',
  strokeWidth: 3
});

// Bracket
const bracket = annotate(element, {
  type: 'bracket',
  color: '#34495e',
  brackets: ['left', 'right'] // Both sides (default)
});

// Top and bottom brackets
const topBottom = annotate(element, {
  type: 'bracket',
  color: '#2c3e50',
  brackets: ['top', 'bottom']
});

// Single bracket on left
const leftOnly = annotate(element, {
  type: 'bracket',
  color: '#3498db',
  brackets: 'left',
  paddingX: 10
});

// Checkmark
const checkmark = annotate(element, {
  type: 'checkmark',
  color: '#2ecc71',
  checkmarkBox: true // Show checkbox style
});

// X-Mark
const xmark = annotate(element, {
  type: 'x-mark',
  color: '#e74c3c',
  strokeWidth: 2.5,
  position: 'right',
  size: 18,
  paddingX: 8
});

// X-Mark with box (checkbox style)
const xmarkBox = annotate(element, {
  type: 'x-mark',
  color: '#dc2626',
  checkmarkBox: true,
  strokeWidth: 2
});

// Large X-Mark for emphasis
const largeX = annotate(element, {
  type: 'x-mark',
  color: '#ef4444',
  size: 24,
  position: 'right',
  paddingX: 10
});

Wavy Annotation

Perfect for error indicators, warnings, or playful emphasis that looks like spell-check squiggles.

// Error-style wavy underline
const error = annotate(element, {
  type: 'wavy',
  color: '#e74c3c',
  amplitude: 3,    // Wave height
  frequency: 5     // Waves per 100px
});

// Subtle wavy underline
const subtle = annotate(element, {
  type: 'wavy',
  color: '#9b59b6',
  amplitude: 1.5,
  frequency: 3
});

// Dramatic wavy underline
const dramatic = annotate(element, {
  type: 'wavy',
  color: '#f39c12',
  amplitude: 4,
  frequency: 7
});

// Position on top (inverted automatically for natural wave direction)
const topWavy = annotate(element, {
  type: 'wavy',
  color: '#06b6d4',
  position: 'top',
  amplitude: 3,
  frequency: 5,
  paddingY: 8
});

// Multiple positions (top and bottom)
const dualWavy = annotate(element, {
  type: 'wavy',
  color: '#8b5cf6',
  position: ['top', 'bottom'],
  amplitude: 3,
  frequency: 6
});

Scallop Annotation

Elegant rounded arcs create a decorative scalloped edge effect.

// Classic scallop underline
const scallop = annotate(element, {
  type: 'scallop',
  color: '#1abc9c',
  amplitude: 4,
  frequency: 8
});

// Subtle scallop effect
const subtle = annotate(element, {
  type: 'scallop',
  color: '#9b59b6',
  amplitude: 2,
  frequency: 6
});

// Position on top or bottom
const topScallop = annotate(element, {
  type: 'scallop',
  color: '#3b82f6',
  position: 'bottom',
  amplitude: 4,
  frequency: 8,
  paddingY: 8
});

// Inverted scallop pattern
const inverted = annotate(element, {
  type: 'scallop',
  color: '#ec4899',
  position: 'bottom',
  inverted: true,
  amplitude: 4,
  frequency: 8
});

Zigzag Annotation

Sharp triangular peaks add dynamic energy and emphasis.

// Standard zigzag underline
const zigzag = annotate(element, {
  type: 'zigzag',
  color: '#e67e22',
  amplitude: 4,
  frequency: 6
});

// Gentle zigzag
const gentle = annotate(element, {
  type: 'zigzag',
  color: '#2ecc71',
  amplitude: 2,
  frequency: 4
});

// Position on top or bottom
const bottomZigzag = annotate(element, {
  type: 'zigzag',
  color: '#f59e0b',
  position: 'bottom',
  amplitude: 4,
  frequency: 6,
  paddingY: 8
});

// Inverted zigzag pattern
const invertedZigzag = annotate(element, {
  type: 'zigzag',
  color: '#8b5cf6',
  position: 'bottom',
  inverted: true,
  amplitude: 4,
  frequency: 6
});

Checkmark Annotation

Mark items as complete, verified, or approved with sketchy checkmarks.

// Basic checkmark (positioned to the right)
const checkmark = annotate(element, {
  type: 'checkmark',
  color: '#2ecc71',
  strokeWidth: 2.5,
  position: 'right',
  size: 18,
  paddingX: 8
});

// Left-positioned checkmark
const leftCheck = annotate(element, {
  type: 'checkmark',
  color: '#059669',
  strokeWidth: 2.5,
  position: 'left',
  size: 18,
  paddingX: 8
});

// Checkbox style (with box)
const checkbox = annotate(element, {
  type: 'checkmark',
  color: '#6b7280',
  checkmarkBox: true,
  strokeWidth: 2
});

// Large checkmark
const large = annotate(element, {
  type: 'checkmark',
  color: '#10b981',
  size: 24,
  position: 'right',
  paddingX: 10
});

Multiline Support

Annotations automatically adapt to text that wraps across multiple lines.

// Each line gets its own annotation
const multiline = annotate(element, {
  type: 'underline',
  color: '#2ecc71',
  multiline: true,  // Enable multiline support
  animate: {
    duration: 800
  }
});

Grouping Annotations

Coordinate multiple annotations with sequential animations - perfect for storytelling and tutorials.

import { annotate, annotationGroup } from 'gritty-notation';

const a1 = annotate(element1, {
  type: 'underline',
  color: '#e74c3c'
});

const a2 = annotate(element2, {
  type: 'box',
  color: '#3498db'
});

const a3 = annotate(element3, {
  type: 'circle',
  color: '#9b59b6'
});

// Create a group
const group = annotationGroup([a1, a2, a3]);

// Show all annotations sequentially (one after another)
await group.show();

// Hide all annotations at once
group.hide();

Dual Position Annotations

Apply decorative annotations to multiple positions simultaneously:

// Wavy on both top and bottom
const dual = annotate(element, {
  type: 'wavy',
  color: '#9b59b6',
  position: ['top', 'bottom'], // Automatically inverts top for mirror effect
  strokeWidth: 2,
  frequency: 6
});

// Underline on all four sides
const bordered = annotate(element, {
  type: 'underline',
  position: ['top', 'bottom', 'left', 'right'],
  color: '#3498db'
});

Inverted Patterns

Create mirrored wave patterns:

// Inverted wavy (waves go upward)
const inverted = annotate(element, {
  type: 'wavy',
  color: '#e74c3c',
  position: 'top',
  inverted: true // Mirror the wave pattern
});

// Works with scallop and zigzag too
const invertedScallop = annotate(element, {
  type: 'scallop',
  position: 'top',
  inverted: true
});

Vertical Text Support

Automatic detection and proper rendering for vertical writing modes:

// Vertical text (Japanese, Chinese, etc.)
const verticalElement = document.querySelector('.vertical-text');
verticalElement.style.writingMode = 'vertical-rl';

// Strike-through automatically detects vertical orientation
annotate(verticalElement, {
  type: 'strike-through',
  color: '#6b7280',
  strokeWidth: 2
});
// Renders vertically through the center instead of horizontally

// Works with other annotation types too
annotate(verticalElement, {
  type: 'bracket',
  color: '#3b82f6',
  brackets: ['top', 'bottom']
});

Recipe Examples

Common patterns for real-world use cases:

// Error indication with wavy underline
annotate(misspelledWord, {
  type: 'wavy',
  color: '#ef4444',
  position: 'bottom',
  amplitude: 3,
  frequency: 5,
  paddingY: 8
});

// Success indicator with checkmark
annotate(completedTask, {
  type: 'checkmark',
  color: '#10b981',
  strokeWidth: 2.5,
  position: 'right',
  size: 18,
  paddingX: 8
});

// Warning with decorative zigzag
annotate(warningText, {
  type: 'zigzag',
  color: '#f59e0b',
  position: 'bottom',
  amplitude: 4,
  frequency: 6,
  paddingY: 8,
  inverted: true
});

// Bordered text effect
annotate(element, {
  type: 'underline',
  color: '#7c3aed',
  position: ['top', 'bottom'],
  strokeWidth: 2,
  paddingY: 4
});

// Interactive hover effect
annotate(element, {
  type: 'scallop',
  color: '#8b5cf6',
  position: 'bottom',
  paddingY: 8,
  showOnHover: true,
  animate: { duration: 700 }
});

Animation Control

// Simple animation
annotate(element, {
  type: 'underline',
  animate: true
});

// Custom animation
annotate(element, {
  type: 'box',
  animate: {
    duration: 1000,
    delay: 200,
    easing: 'ease-in-out'
  }
});

// No animation
annotate(element, {
  type: 'highlight',
  animate: false
});

Annotation Presets

Use built-in presets for common use cases or create custom ones.

import { annotate, createPreset, getAvailablePresets, presets } from 'gritty-notation';

// Built-in presets
annotate(element, { preset: 'error' });     // Red wavy underline
annotate(element, { preset: 'success' });   // Green checkmark
annotate(element, { preset: 'warning' });   // Orange highlight
annotate(element, { preset: 'info' });      // Blue circle
annotate(element, { preset: 'deleted' });   // Red crossed-off

// Override preset defaults
annotate(element, {
  preset: 'error',
  strokeWidth: 3
});

// Create custom presets
createPreset('brand-highlight', {
  type: 'box',
  color: '#7c3aed',
  strokeWidth: 3,
  padding: 8,
  animate: { duration: 700 }
});

createPreset('todo-item', {
  type: 'checkmark',
  color: '#6b7280',
  checkmarkBox: true,  // Checkbox style
  strokeWidth: 2
});

createPreset('completed', {
  type: 'checkmark',
  color: '#2ecc71',
  strokeWidth: 3
});

// Use custom presets
annotate(element, { preset: 'brand-highlight' });
annotate(element, { preset: 'todo-item' });
annotate(element, { preset: 'completed' });

// Access preset configurations directly
const errorPreset = presets.error;
console.log(errorPreset); // { type: 'wavy', color: '#e74c3c', ... }

// List all available presets
const allPresets = getAvailablePresets();
console.log(allPresets); // ['error', 'success', 'warning', 'info', 'deleted', 'brand-highlight']

Scroll-Triggered Annotations

Automatically show annotations when elements scroll into view.

// Basic scroll trigger
annotate(element, {
  type: 'underline',
  color: '#3498db',
  showOnVisible: true
});

// Custom visibility options
annotate(element, {
  type: 'box',
  showOnVisible: true,
  visibilityOptions: {
    threshold: 0.3,       // Show when 30% visible
    triggerOnce: true,    // Only trigger once
    rootMargin: '0px'
  }
});

// Trigger earlier (before element enters viewport)
annotate(element, {
  preset: 'success',
  showOnVisible: true,
  visibilityOptions: {
    threshold: 0.2,
    rootMargin: '100px'   // Start detecting 100px before viewport
  }
});

Interactive Animations

Hover Interactions

Show annotations when users hover over elements:

// Show on hover
annotate(element, {
  type: 'underline',
  color: '#3498db',
  showOnHover: true
});

// Hover with custom animation
annotate(element, {
  type: 'circle',
  color: '#9b59b6',
  showOnHover: true,
  animate: {
    duration: 400,
    easing: 'ease-out'
  }
});

Timing Controls

Fine-tune when hover animations trigger with delay properties:

// Instant show and hide (default behavior)
annotate(element, {
  type: 'underline',
  color: '#3498db',
  showOnHover: true,
  hoverDelay: 0,
  hideDelay: 0
});

// Delay before showing (like a tooltip)
annotate(element, {
  type: 'underline',
  color: '#e74c3c',
  showOnHover: true,
  hoverDelay: 500  // Wait 500ms before showing
});

// Delay before hiding
annotate(element, {
  type: 'underline',
  color: '#2ecc71',
  showOnHover: true,
  hideDelay: 300  // Wait 300ms before hiding when mouse leaves
});

// Tooltip-like behavior with both delays
annotate(element, {
  type: 'box',
  color: '#9b59b6',
  showOnHover: true,
  hoverDelay: 600,  // Delay showing to avoid accidental triggers
  hideDelay: 200,   // Brief delay before hiding
  animate: { duration: 300 }
});

Mouse Move Animation

Progressively reveal annotations as the mouse moves across elements:

// Underline that draws as mouse moves
annotate(element, {
  type: 'underline',
  color: '#e74c3c',
  strokeWidth: 3,
  animateOnMove: true
});

// Wavy underline with mouse tracking
annotate(element, {
  type: 'wavy',
  color: '#3498db',
  animateOnMove: true
});

// Works with any annotation type
annotate(element, {
  type: 'box',
  color: '#2ecc71',
  animateOnMove: true
});

// Add reverse animation when mouse leaves
annotate(element, {
  type: 'underline',
  color: '#9b59b6',
  strokeWidth: 3,
  animateOnMove: true,
  animateOnLeave: true  // Smoothly reverses when mouse exits
});

Hide Animations

Add smooth exit animations when annotations are hidden:

// Animated hide with showOnHover
annotate(element, {
  type: 'underline',
  color: '#3498db',
  showOnHover: true,
  animate: { duration: 400 },
  hideAnimation: { duration: 300, easing: 'ease-in' }
});

// Slow hide animation
annotate(element, {
  type: 'circle',
  color: '#e74c3c',
  showOnHover: true,
  hideAnimation: { duration: 800, easing: 'ease-out' }
});

// No hide animation (instant)
annotate(element, {
  type: 'box',
  color: '#2ecc71',
  showOnHover: true,
  hideAnimation: false
});

// Reverse animation on hide (plays show animation backwards)
annotate(element, {
  type: 'underline',
  color: '#9b59b6',
  showOnHover: true,
  animate: { duration: 600, easing: 'ease-out' },
  hideAnimation: true,
  reverseOnHide: true  // Erases right-to-left instead of disappearing
});

// Works with manual hide() calls too
const annotation = annotate(element, {
  type: 'highlight',
  color: '#fbbf24',
  hideAnimation: { duration: 500 }
});

await annotation.show();
await annotation.hide();  // Will animate the hide

Loop Animation

Continuously redraw annotations in a loop:

// Loop with default interval (3000ms)
const loopAnnotation = annotate(element, {
  type: 'wavy',
  color: '#e74c3c',
  loop: true
});

// Loop with custom interval (2000ms)
annotate(element, {
  type: 'underline',
  color: '#3498db',
  loop: 2000  // Redraw every 2 seconds
});

// Instant redraw (no delay between hide and show)
annotate(element, {
  type: 'underline',
  color: '#2ecc71',
  loop: 2000,
  loopDelay: 0  // Instant transition between hide and show
});

// Custom loop delay
annotate(element, {
  type: 'wavy',
  color: '#9b59b6',
  loop: 3000,
  loopDelay: 200  // 200ms delay between hide and show
});

Pulse Effect

Create subtle pulsing effects to draw attention:

// Pulse with default interval (2000ms)
const pulseAnnotation = annotate(element, {
  type: 'highlight',
  color: '#fbbf24',
  pulse: true
});

// Pulse with custom interval (1500ms)
annotate(element, {
  type: 'box',
  color: '#2ecc71',
  pulse: 1500  // Pulse every 1.5 seconds
});

Playback Controls

Control annotation playback with pause, resume, and replay:

const annotation = annotate(element, {
  type: 'box',
  color: '#2ecc71',
  animate: { duration: 2000 }
});

// Show the annotation
await annotation.show();

// Pause mid-animation
annotation.pause();

// Resume animation
annotation.resume();

// Replay from the beginning
await annotation.replay();

Independent Padding Controls

Control horizontal and vertical padding separately for box, circle, and highlight:

// Wide box
annotate(element, {
  type: 'box',
  paddingX: 20,  // Wide horizontal padding
  paddingY: 5    // Narrow vertical padding
});

// Tall circle
annotate(element, {
  type: 'circle',
  paddingX: 5,   // Narrow horizontal
  paddingY: 25   // Tall vertical
});

// Custom highlight
annotate(element, {
  type: 'highlight',
  paddingX: 15,
  paddingY: 8
});

Event Callbacks

React to annotation lifecycle events.

annotate(element, {
  type: 'underline',
  color: '#3498db',

  onBeforeShow: () => {
    console.log('About to show annotation');
  },

  onShow: () => {
    console.log('Annotation visible');
    // Track analytics, trigger next step, etc.
  },

  onHide: () => {
    console.log('Annotation hidden');
  },

  onRemove: () => {
    console.log('Annotation removed');
  },

  onError: (error) => {
    console.error('Error:', error);
  }
});

Advanced Padding Options

// Same on all sides
annotate(element, {
  type: 'box',
  padding: 10
});

// Vertical and horizontal
annotate(element, {
  type: 'circle',
  padding: [5, 15]  // [vertical, horizontal]
});

// All four sides
annotate(element, {
  type: 'box',
  padding: [5, 10, 15, 10]  // [top, right, bottom, left]
});

🛠️ Development

# Install dependencies
npm install

# Run tests
npm test

# Run tests with UI
npm run test:ui

# Build the library
npm run build

# Run examples page
npm run dev

# Lint and format
npm run lint
npm run format

🗺️ Roadmap

  • [x] Core rendering engine
  • [x] 12 annotation types (underline, box, circle, highlight, strike-through, crossed-off, bracket, wavy, scallop, zigzag, checkmark, x-mark)
  • [x] Animation system
  • [x] SSR compatibility
  • [x] Event callbacks (lifecycle hooks)
  • [x] Annotation presets (5 built-in + custom)
  • [x] Annotation groups (sequential animations)
  • [x] Multiline support
  • [x] Vertical text support (automatic detection)
  • [x] IntersectionObserver (scroll-triggered annotations)
  • [x] Interactive animations (hover, loop, pulse, playback controls)
  • [x] Independent padding controls (paddingX, paddingY)
  • [x] Dual position support (position arrays)
  • [x] Inverted patterns for decorative annotations
  • [x] Comprehensive Storybook documentation with recipes
  • [ ] Additional annotation types (callout, scribble, arrow)
  • [ ] Advanced animation controls (timeline, spring physics)
  • [ ] Framework wrappers (React, Vue, Svelte)

📄 License

MIT

🙏 Acknowledgments

Inspired by rough-notation by Preet Shihn.