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-notation2. 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}