Magnifier
A magnifying glass that really enlarges the page under it. Closer over controls, closer still while you hold.
npx shadcn@latest add https://hoverui.fun/ r/ magnifier.jsonAdds components/cursors/magnifier.tsx. The CLI also installs the useCursor hook it runs on, once, into your hooks folder. The files are yours from there.
Move through the preview, then hover the controls and press.Open this on a computer to move it around: it follows a mouse, not a finger.
- Kind
- Lenses
- Hook
- useCursor
- Dependencies
- None
- License
- MIT
How it behaves
A magnifying glass that really magnifies: the page under the lens shows at twice its size. Over anything clickable it zooms closer; hold to look closer still. Chromium browsers draw the zoom; elsewhere the glass passes over the page unchanged.
Usage
import { MagnifierCursor } from "@/components/cursors/magnifier";
export function Section() {
return (
<section>
<MagnifierCursor />
{/* this section now wears Magnifier */}
</section>
);
}Put it inside <body> in your root layout to wear it on every page. Nested cursors take over inside their own element.
Props
- className
- Colors any outline and the pointer dot through currentColor, e.g. text-pink-500. Keep blend modes and filters off it: either would cut the lens off from the page it sees.
- Hover targets
- a, button, input, select, textarea, label, [role=button], [data-cursor-label]
Source
"use client";
import { useId, useRef } from "react";
import { cursorRoot, useCursor } from "@/hooks/use-cursor";
/** The lens: its radius in px, and how much it enlarges at rest. */
const R = 40;
const ZOOM = 2;
/** The furthest any pixel is pulled: the rim's, where the view inside reaches out half the radius. */
const PULL = R * (1 - 1 / ZOOM);
/**
* A displacement map that magnifies, as a data URL: inside the lens every pixel looks back toward the centre by
* (1 - 1/zoom) of its distance, so the page under the middle fills the glass; outside it, nothing moves.
*/
function zoomMap() {
const size = R * 2;
const canvas = document.createElement("canvas");
canvas.width = canvas.height = size;
const ctx = canvas.getContext("2d")!;
const img = ctx.createImageData(size, size);
for (let y = 0; y < size; y++)
for (let x = 0; x < size; x++) {
const [dx, dy] = [x + 0.5 - R, y + 0.5 - R];
const inside = dx * dx + dy * dy < R * R ? 1 - 1 / ZOOM : 0;
const i = (y * size + x) * 4;
img.data[i] = 127.5 - (dx * inside * 127.5) / PULL;
img.data[i + 1] = 127.5 - (dy * inside * 127.5) / PULL;
img.data[i + 2] = 128;
img.data[i + 3] = 255;
}
ctx.putImageData(img, 0, 0);
return canvas.toDataURL();
}
/** The magnifying glass: rim, glint and handle, its lens centred; exported so a page can draw it at rest. */
export function MagnifierArt({ className = "" }: { className?: string }) {
return (
<svg viewBox="-50 -50 100 100" className={`overflow-visible ${className}`} aria-hidden>
<path d="M33 33 62 62" stroke="currentColor" strokeWidth="13" strokeLinecap="round" />
<path d="M33 33 62 62" stroke="#7a4a24" strokeWidth="10" strokeLinecap="round" />
<circle r="48" fill="none" stroke="currentColor" strokeWidth="5" />
<circle r="48" fill="none" stroke="#c9a54a" strokeWidth="3" />
<path d="M-30-24a38 38 0 0 1 22-14" fill="none" stroke="#fff" strokeWidth="3" strokeLinecap="round" opacity="0.7" />
</svg>
);
}
/**
* A magnifying glass that really magnifies: the page under the lens shows at twice its size. Over anything clickable it
* zooms closer; hold to look closer still. Chromium browsers draw the zoom; elsewhere the glass passes over the page
* unchanged.
*/
export function MagnifierCursor({ className = "" }: { className?: string }) {
const filter = `zoom${useId().replace(/[^\w-]/g, "")}`;
const s = useRef({ x: 0, y: 0, power: 1, scale: 0 });
const ref = useCursor((p, el) => {
const st = s.current;
const [defs, lens, glass] = el.children as HTMLCollectionOf<HTMLElement>;
st.x += (p.x - st.x) * p.ease(22);
st.y += (p.y - st.y) * p.ease(22);
// Closer over a control, closer still while held: 2x, about 2.5x, about 3.3x. Past that the zoom turns blocky.
st.power += ((p.down ? 1.4 : p.hover ? 1.2 : 1) - st.power) * p.ease(10);
const map = defs.querySelector("feImage")!;
if (!map.getAttribute("href")) map.setAttribute("href", zoomMap());
const scale = Math.round(PULL * 2 * st.power);
if (scale !== st.scale) defs.querySelector("feDisplacementMap")!.setAttribute("scale", String((st.scale = scale)));
lens.style.transform = glass.style.transform = `translate(${st.x}px, ${st.y}px)`;
});
// No blend mode or filter on the root: either would cut the lens off from the page it enlarges.
return (
<div ref={ref} aria-hidden className={`${cursorRoot} ${className}`}>
<svg className="absolute size-0">
<filter id={filter} x="0" y="0" width="100%" height="100%" colorInterpolationFilters="sRGB">
<feImage x="0" y="0" width={R * 2} height={R * 2} preserveAspectRatio="none" result="map" />
<feDisplacementMap in="SourceGraphic" in2="map" scale={PULL * 2} xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>
<div className="absolute -translate-1/2 rounded-full" style={{ width: R * 2, height: R * 2, backdropFilter: `url(#${filter})` }} />
<MagnifierArt className="absolute size-[88px] -translate-1/2" />
</div>
);
}
It runs on the useCursor hook, which has its own source and API.
More lenses cursors
- BlendInverts whatever is beneath it. Grows over anything clickable.
- GlassA lens of frosted glass that blurs whatever it passes over. Widens over anything clickable.
- FocusBlurs its area except a sharp circle at the pointer, like a shallow depth of field. Opens up over controls.
- GogglesNight-vision goggles that turn whatever they pass over green. They gain up over controls; a click flashes them.
- DropletA water drop that refracts the page under it. Swells over controls; a click sends ripples through the page, bending its words.
- LiquidA drop of liquid glass that bends the page at its rim. Over controls it flows out to wrap them in a glass pill; press to squeeze it.
- GlitchThe page round the pointer splits into red, green and blue and tears sideways in fits, with no frame. It takes over controls and glitches harder; a click throws a burst.
Made a cursor of your own? Contributed cursors carry their maker's name on their page.
Contribute a cursor