Cursor
Custom cursor follower that trails the pointer. Install Cursor from the VLLNT UI registry with the shadcn CLI.
Custom cursor follower that trails the pointer. Part of the utility family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.
Preview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
Add Cursor to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/cursor.jsonSource
"use client";
import * as React from "react";
import { cn } from "../../lib/utils";
/** Props for {@link Cursor}. */
export type CursorProps = React.ComponentPropsWithoutRef<"div"> & {
/** Diameter of the follower dot in pixels. Defaults to `24`. */
size?: number;
};
type Point = {
x: number;
y: number;
};
function usePointerPosition(): Point | undefined {
const [point, setPoint] = React.useState<Point>();
React.useEffect(() => {
if (typeof window === "undefined") {
return;
}
const onMove = (event: PointerEvent): void => {
setPoint({ x: event.clientX, y: event.clientY });
};
window.addEventListener("pointermove", onMove, { passive: true });
return () => {
window.removeEventListener("pointermove", onMove);
};
}, []);
return point;
}
/**
* Circular follower that tracks the pointer as a custom cursor overlay.
*
* Pointer tracking is direct feedback and keeps following under reduced
* motion; `motion-reduce` drops the smoothing transition.
*
* @example
* ```tsx
* <Cursor size={32} />
* ```
*/
export const Cursor = ({
className,
ref,
size = 24,
style,
...props
}: CursorProps & { ref?: React.Ref<HTMLDivElement> }) => {
const point = usePointerPosition();
return (
<div
aria-hidden="true"
className={cn(
"pointer-events-none fixed left-0 top-0 z-50 -translate-x-1/2 -translate-y-1/2 rounded-full border border-foreground bg-foreground/20 backdrop-invert transition-transform duration-100 ease-out motion-reduce:transition-none",
point ? "opacity-100" : "opacity-0",
className,
)}
ref={ref}
style={{
height: `${size}px`,
transform: point
? `translate(${point.x}px, ${point.y}px) translate(-50%, -50%)`
: undefined,
width: `${size}px`,
...style,
}}
{...props}
/>
);
};
Cursor.displayName = "Cursor";
Stories
Explore every variant and state in the interactive Storybook:
Preview
Switch between light and dark to inspect the embedded Storybook preview.