gritty-notation
v0.2.0
Published
A modern, performant library for creating and animating hand-drawn annotations on web pages inspired by rough notation
Maintainers
Readme
Gritty Notation
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 annotateconfig: 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 animationhide(): Promise<void>- Hide the annotation (with optional animation)remove(): void- Remove annotation and clean upisShowing(): boolean- Check if annotation is visibleupdateColor(color: string): void- Update annotation colorupdateStrokeWidth(width: number): void- Update stroke widthpause(): void- Pause the current animationresume(): void- Resume paused animationreplay(): 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 sequentiallyhide(): 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 hideLoop 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.
