No.146 · Cursors· New
Infinite Highlight Sweep
A second cursor — not yours — that glides in and drags a highlight across the line that matters, left to right: the box grows behind the words and they turn the accent colour with a crisp edge that travels with it. It lets go, holds, lets the highlight fade, and moves on to the next phrase, looping for as long as it is on screen.
The prompt
Written for the variant you picked above: Teal
Build a scripted "demo cursor" that highlights text the way a person drags a selection — an animation, not a pointer effect. The visitor's own cursor is never hidden or changed. Markup: <SweepText text="…" order={n} as="span|p|h2" /> renders one span per word (spaces between, "\n" as <br>) and marks the element data-hs-text with its order. [[…]] inside the text marks the phrase to sweep (otherwise every word is swept). Targets are swept in ascending order and loop forever. Layers inside an isolated, position:relative host: selection boxes behind the content (z 0), the content (z 1), the cursor above (z 2), all aria-hidden except the content. Measuring: for the current target, take its marked word spans' getBoundingClientRects in host-local px (corrected by rect.width / offsetWidth) and group them into visual lines by top edge, so a phrase that wraps is swept line by line. The box, one per visual line: border-radius 7px; background the accent at 16%; a 1px inset ring of the accent at 48%; 6px padding either side and 2px above and below the words' boxes. Its left edge is fixed at the line start and its right edge is tied to the cursor's x. The words get background-image: linear-gradient(90deg, accent 0 var(--f), currentColor var(--f)) clipped to the text (-webkit-text-fill-color: transparent), and --f is set per word to (cursor x − word left) every frame, so the accent edge is crisp and moves exactly with the cursor. Clearing transitions a registered --hs-mix number from 1 to 0 so the accent fades back to the ink. The cursor: a 26px arrow with every corner rounded (32-unit SVG: M5.77 3.24 L28.12 10.61 A3 3 0 0 1 28.07 16.33 L20.61 18.63 A3 3 0 0 0 18.63 20.61 L16.33 28.07 A3 3 0 0 1 10.61 28.12 L3.24 5.77 A2 2 0 0 1 5.77 3.24 Z), filled with the accent, no keyline, soft dark drop shadow, rounded tip as the hotspot. Timeline (one rAF loop, sleeping offscreen via IntersectionObserver or while document.hidden): wait 500ms; glide in along a quadratic curve from below-right to just left of the first line's start (900ms, cubic in-out, fading in); press (scale 0.9, 160ms) and the box appears; drag right at about 380px/s (≥900ms for a single line, cubic in-out; across wrapped lines ease in on the first, steady in the middle, ease out on the last, with a 280ms hop down-left to each next line start); release; drift to rest just below-right of the box's end (520ms ease-out); hold 2.4s; the boxes and colour fade (420ms) as the cursor drifts away; pause 1.2s; next target, wrapping to the first after the last, forever. No controls. A width change mid-sweep restarts the current target with fresh measurements. Reduced motion: no animation: the first target is shown highlighted and the arrow rests past its end. One accent colour prop drives the boxes, ring, swept words and arrow; fill, ring, ink and arrow props override each one. On a near-black page (#050505) with a big white Geist heading. Selected variant — Teal (teal): Accent #5fd4bf (teal). This selection takes precedence over references to other themes or options in the general brief. Implement this selected appearance; do not add a variant picker or alternative appearances unless requested. Implementation requirements: Use React and TypeScript. Keep the component self-contained, with uniquely scoped CSS and no animation libraries. Include required imports, styles and font setup in the result. Interaction: Watch: the demo cursor sweeps “you'll actually be there.”, lets it fade, sweeps the paragraph's first phrase, and starts again, forever. Highlight Sweep is the play-once version. Motion: Glide 900ms, press 160ms, drag ≈380px/s, rest 520ms, hold 2.4s, fade 420ms, gap 1.2s, repeat forever. The rAF loop sleeps offscreen or in a hidden tab. Accessibility: The text stays real text and reads normally; boxes and the demo cursor are aria-hidden. It has no pause control by design, so it suits short decorative passages; it stops offscreen and in hidden tabs, and reduced motion shows a still, highlighted state instead. Forced colours drop the gradient text fill. Responsive behavior: Lines are measured from the text as it wraps at that width, and a wrapped phrase is swept line by line; a resize restarts the current sweep. Touch: Identical on touch — it never depended on the pointer. Preserve keyboard access, visible focus and reduced-motion behavior. Keep fictional demo data and simulated service calls in a separate usage example; expose callbacks for real actions and recover from rejected requests. Keep usage-example links inside the demo: use local buttons or prevent navigation. Demonstration links must not navigate to Vitrine routes or open other pages; preserve real URL support in the reusable component itself. Verify the result at narrow and wide viewport sizes, with keyboard input and reduced motion enabled.
Notes
- Interaction
- Watch: the demo cursor sweeps “you'll actually be there.”, lets it fade, sweeps the paragraph's first phrase, and starts again, forever. Highlight Sweep is the play-once version.
- Motion
- Glide 900ms, press 160ms, drag ≈380px/s, rest 520ms, hold 2.4s, fade 420ms, gap 1.2s, repeat forever. The rAF loop sleeps offscreen or in a hidden tab.
- Accessibility
- The text stays real text and reads normally; boxes and the demo cursor are aria-hidden. It has no pause control by design, so it suits short decorative passages; it stops offscreen and in hidden tabs, and reduced motion shows a still, highlighted state instead. Forced colours drop the gradient text fill.
- Responsive
- Lines are measured from the text as it wraps at that width, and a wrapped phrase is swept line by line; a resize restarts the current sweep.
- On touch
- Identical on touch — it never depended on the pointer.