hover-ui

Focus

Blurs its area except a sharp circle at the pointer, like a shallow depth of field. Opens up over controls.

npx shadcn@latest add https://hoverui.fun/r/focus.json

Adds components/cursors/focus.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.

Kind
Lenses
Dependencies
None
License
MIT

How it behaves

Blurs its area except a sharp circle at the pointer, like a shallow depth of field; it opens up over anything clickable and racks in on press.

Usage

Any element
import { FocusCursor } from "@/components/cursors/focus";

export function Section() {
  return (
    <section>
      <FocusCursor />
      {/* this section now wears Focus */}
    </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

focus.tsx
"use client";

import { useRef } from "react";
import { cursorRoot, useCursor } from "@/hooks/use-cursor";

/** Blurs its area except a sharp circle at the pointer, like a shallow depth of field; it opens up over anything clickable and racks in on press. */
export function FocusCursor({ className = "" }: { className?: string }) {
  const r = useRef(70);
  const ref = useCursor((p, el) => {
    r.current += ((p.down ? 40 : p.hover ? 110 : 70) - r.current) * p.ease(10);
    // The blur covers only the visible part of the area, with a hole where the pointer is.
    const a = el.parentElement!.getBoundingClientRect();
    const left = Math.max(a.left, 0);
    const top = Math.max(a.top, 0);
    const [blur, dot] = el.children as HTMLCollectionOf<HTMLElement>;
    Object.assign(blur.style, {
      left: `${left}px`,
      top: `${top}px`,
      width: `${Math.min(a.right, innerWidth) - left}px`,
      height: `${Math.min(a.bottom, innerHeight) - top}px`,
      maskImage: `radial-gradient(circle ${r.current}px at ${p.x - left}px ${p.y - top}px, transparent 60%, black 100%)`,
    });
    dot.style.transform = `translate(${p.x}px, ${p.y}px)`;
  });

  // No blend mode or filter on the root: either would cut the blur off from the page beneath.
  return (
    <div ref={ref} aria-hidden className={`${cursorRoot} ${className}`}>
      <div className="absolute backdrop-blur-[3px]" />
      <div className="absolute size-1.5 -translate-1/2 rounded-full bg-current" />
    </div>
  );
}

It runs on the useCursor hook, which has its own source and API.

More lenses cursors

Made a cursor of your own? Contributed cursors carry their maker's name on their page.

Contribute a cursor