Annotate Playground

Experiment with different settings for the Annotate component. Adjust the controls below to see how different configurations affect the annotation.

This component is built on top of Rough Notation, a JavaScript library for creating hand-drawn-style annotations. The component extends the original library with additional configuration options for better control over animations and display behavior.

This text is being annotated

Configuration

How to Implement

1. Install the required package

pnpm add rough-notation

2. Create a new component named Annotate

This component will be used to render the annotation.

1"use client";
2
3import { useEffect, useRef, ReactNode, useState, useCallback } from "react";
4import { annotate } from "rough-notation";
5
6type RoughAnnotationType = 
7  | "underline"
8  | "box"
9  | "circle"
10  | "highlight"
11  | "strike-through"
12  | "crossed-off"
13  | "bracket";
14
15type BracketType = "left" | "right" | "top" | "bottom";
16
17// Interface for the annotation object returned by rough-notation
18interface RoughNotation {
19  show: () => void;
20  hide: () => void;
21  remove: () => void;
22  color: string | undefined;
23}
24
25interface AnnotateProps {
26  children: ReactNode;
27  type?: RoughAnnotationType;
28  color?: string;
29  hoverColor?: string;
30  animate?: boolean;
31  animationDuration?: number;
32  iterations?: number;
33  padding?: number | [number, number] | [number, number, number, number];
34  brackets?: BracketType | BracketType[];
35  multiline?: boolean;
36  strokeWidth?: number;
37  showOnLoad?: boolean;
38  className?: string;
39  showOnHover?: boolean;
40}
41
42export default function Annotate({
43  children,
44  type = "underline",
45  color = "#FFC107",
46  hoverColor,
47  animate = true,
48  animationDuration = 800,
49  iterations = 2,
50  padding = 5,
51  brackets = ["right", "left"],
52  multiline = true,
53  strokeWidth = 1,
54  showOnLoad = true,
55  className,
56  showOnHover = false,
57}: AnnotateProps) {
58  const elementRef = useRef<HTMLSpanElement>(null);
59  const annotationRef = useRef<RoughNotation | null>(null);
60  const [isHovered, setIsHovered] = useState(false);
61  
62  // Determine if we should respond to hover events
63  const shouldHandleHover = showOnHover || !!hoverColor;
64  
65  // Create annotation only once
66  useEffect(() => {
67    if (!elementRef.current) return;
68    
69    // Create the annotation once
70    annotationRef.current = annotate(elementRef.current, {
71      type,
72      color: color,
73      animate,
74      animationDuration,
75      iterations,
76      padding,
77      brackets,
78      multiline,
79      strokeWidth,
80    }) as RoughNotation;
81
82    // Initial show based on props
83    if (showOnLoad && !showOnHover) {
84      annotationRef.current.show();
85    }
86
87    // Cleanup
88    return () => {
89      if (annotationRef.current) {
90        annotationRef.current.remove();
91      }
92    };
93  // Only recreate annotation when these critical props change
94  }, [type, animate, animationDuration, iterations, padding, brackets, multiline, strokeWidth, showOnLoad, showOnHover]);
95
96  // Handle color updates separately without recreating annotation
97  useEffect(() => {
98    if (!annotationRef.current || !shouldHandleHover) return;
99
100    // Update color without recreating the annotation
101    const currentColor = isHovered && hoverColor ? hoverColor : color;
102    if (annotationRef.current) {
103      annotationRef.current.color = currentColor;
104    }
105  }, [color, hoverColor, isHovered, shouldHandleHover]);
106
107  // Handle hover state showing/hiding
108  useEffect(() => {
109    if (!annotationRef.current) return;
110
111    if (showOnHover) {
112      if (isHovered) {
113        annotationRef.current.show();
114      } else {
115        annotationRef.current.hide();
116      }
117    } else if (showOnLoad) {
118      annotationRef.current.show();
119    } else {
120      annotationRef.current.hide();
121    }
122  }, [isHovered, showOnHover, showOnLoad]);
123
124  // Memoize event handlers to prevent recreation on each render
125  const handleMouseEnter = useCallback(() => {
126    if (shouldHandleHover) {
127      setIsHovered(true);
128    }
129  }, [shouldHandleHover]);
130
131  const handleMouseLeave = useCallback(() => {
132    if (shouldHandleHover) {
133      setIsHovered(false);
134    }
135  }, [shouldHandleHover]);
136
137  return (
138    <span 
139      ref={elementRef} 
140      onMouseEnter={handleMouseEnter}
141      onMouseLeave={handleMouseLeave}
142      className={`${className || ''} inline-block`}
143    >
144      {children}
145    </span>
146  );
147}