Lens Text
Text held out of focus until the pointer passes over it like a magnifying glass.
Installation
npx shadcn@latest add @loomui/lens-textUsage
import { LensText } from "@/components/ui/lens-text"<LensText>He was dead the whole time.</LensText>Two copies of the same words sit on top of each other. The bottom one is
blurred and is the real text: filters are invisible to assistive technology, so
the accessible name, the selection and the copy-paste all stay there. The top
one is sharp, aria-hidden, and masked down to a circle at the pointer.
That same circle is punched out of the blurred copy. Without the hole the sharp glyphs would sit on a blurred set of glyphs and read as a halo rather than as focus.
The blurred copy sits in a padded, rounded panel, and the blur goes on a span inside it rather than on the panel itself. Blur the panel and its glow spreads past its own edges, where the mask cuts it off square. Frosted glass with four hard corners. That is the tell that this is two divs.
Position and radius are written to CSS custom properties from a single
requestAnimationFrame loop, never to React state. Moving the mouse across a
paragraph costs zero renders.
Weight and pace
follow is the time constant of that loop, in milliseconds. The lens eases toward the pointer instead of being pinned to it, and opens and
closes on the same clock a little slower. It reads as glass being moved rather
than a mask being switched on.
<LensText follow={220}>Slow, heavy glass.</LensText>Around 130 feels attached to the pointer. Past 250 the lens starts
trailing far enough behind that aiming it becomes work. follow={0} locks it
to the pointer and snaps it open, which is also what
prefers-reduced-motion: reduce does.
Shaping the lens
size is the diameter. feather is how much of it fades at the edge. blur is
how far out of focus the rest of the text sits, and radius rounds the frosted
panel.
<LensText size={180} feather={0.2} blur={10} radius="1em">
A wide lens with a near-hard edge.
</LensText>feather: 0 gives a hard cut, which reads as a hole in frosted glass.
feather: 1 falls off from the centre and reads as a soft wash. Push blur
past about 12 and the resting text stops looking like words, which is the
right call for a spoiler and the wrong one for a headline. radius takes any
CSS length, and in em it tracks whatever type size it is wrapped around.
Magnifying
magnify scales the text under the lens, anchored to the pointer. The lens then
reads as curved glass rather than a window.
<LensText ring magnify={1.08}>
Held at arm's length.
</LensText>Keep it under about 1.1. Past that the magnified glyphs no longer line up
with the blurred ones around the edge of the lens, and the illusion breaks.
ring draws a hairline circle at the lens edge, which sells the glass and
gives the pointer something to hold.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
blur | number | 8 | Blur radius of the resting text, in pixels. |
size | number | 110 | Diameter of the lens in pixels. |
feather | number | 0.5 | How much of the lens edge fades out, 0 to 1. |
magnify | number | 1 | Scale under the lens. 1 is a flat window. |
follow | number | 130 | Ms the lens takes to catch up. 0 locks it. |
radius | string | "0.35em" | Corner radius of the frosted panel. |
ring | boolean | false | Draw a hairline ring at the lens edge. |
disabled | boolean | false | Render the text sharp, with no lens. |
className | string | none | Merged onto the wrapper. Type styles here. |
Any other <span> prop is forwarded.
Accessibility
Blurred text is unreadable to anyone who cannot aim a pointer at it. Use it
for copy people can afford to miss. Spoilers, teasers, a redaction gag. Never
put it on something load-bearing. On touch, pen and coarse pointers the
component detects the missing hover and renders the text sharp instead, and
disabled does the same on demand. The sharp copy is aria-hidden and
pointer-events-none, so a screen reader reads the words once, unblurred.