A technical breakdown of SVG-driven annotations, viewport intersection triggers, and multi-layered ResizeObserver alignments in React.
Implementation breakdown of the Highlighter component, covering its viewport-triggered annotation logic, dynamic resizing strategy, and CSS positioning mechanics.
[!NOTE]
This component is inspired by the Magic UI Highlighter and utilizes the Rough Notation library and Framer Motion (motion/react) for lightweight, sketchy annotations.
In web interface design, annotations (often referred to as micro-notations) are dynamic visual markers overlaid on text or elements to emphasize key details and guide reader focus. Rather than using static CSS styling (like simple background fills or basic borders), these web annotations simulate hand-drawn, human markings, such as:
The -10% Safe Boundary: Setting the intersection Margin to -10% acts as an offset buffer. Drawing instructions only trigger once the text card is inside the viewport by at least 10%, ensuring users actually witness the hand-drawn stroke writeout animation rather than having it finish before scrolling down to it.
Single Dispatch (once: true): The sketch animation runs once per load cycle. After triggering, it remains drawn and doesn't replay when the user scrolls back and forth.
Conditional Visibility Gate: The boolean trigger combines with property overrides:
const shouldShow = !isView || isInView
If isView is set to false, the component bypasses viewport intersection restrictions and draws immediately on render load.
Standard useEffect runs asynchronously after the browser paints the screen. If we initialized SVG annotations there, users would briefly see plain, unannotated text, followed a fraction of a second later by the drawing appearing unexpectedly.
By executing changes in useLayoutEffect, configurations are resolved synchronously before visual paint updates, ensuring a seamless visual insertion.
annotate(element, config): Binds the rough-notation engine to the span node. It crawls the inline element bounds (including line wraps if multiline: true) and appends a hidden absolute styling SVG container.
Reference Caching: Assigns the output builder to local references outside the layout scope so that the cleanup routine can safely scrub the SVG node on unmount.
.show(): Triggers the SVG path stroke drawing animation.
rough-notation calculates absolute coordinates at creation time. If layout reflows occur, the hand-drawn SVG overlay will stay in its old layout position, causing misaligned and broken outlines.
The component solves this issues by registering a dual-target ResizeObserver:
resizeObserver = new ResizeObserver(() => { currentAnnotation.hide() currentAnnotation.show()})resizeObserver.observe(element)resizeObserver.observe(document.body)
Why is it necessary to observe both the local element and the global document.body instead of just one?
Why we cannot only observe element:ResizeObserver only triggers when the width or height of the observed target changes.
If an accordion/dropdown opens above the highlighted text, or a lazy-loaded image finishes downloading, the highlighted element is pushed down the page (its position shifts, but its width and height remain unchanged). Without tracking document.body, the highlight stays behind in the old spot.
Why we cannot only observe document.body:
If we only monitor document.body, local layout updates that target only the span (such as hovering font scaling, local text replacements, or grid alignment adjustments) might not change the total size of document.body. This would leave the highlight misaligned.
The container wrapper is styled with a carefully selected combination of CSS properties:
className="relative inline-block bg-transparent"
relative Positioning Context:rough-notation overlays the absolute SVG lines. Declaring relative anchors these coordinates relative to the text block, preventing drawing offsets.
inline-block Display Mode: Standard <span> tags are inline elements and lack rigid layout properties, often confusing width calculations. inline-block permits text wrap loops inside paragraph streams while reporting robust width/height boundaries for the observer APIs.
bg-transparent Base: Standard background shading can obscure the SVG brush stroke design. Setting background color transparency keeps colors clean.