DocumentationCreepy Button

Creepy Button

A button that watches your cursor, perfect for adding character to your UI.

Install using CLI

npx shadcn@latest add "https://vengeance-ui.vercel.app/r/creepy-button.json"

Install Manually

1

Install dependencies

npm install framer-motion clsx tailwind-merge
2

Add util file

lib/utils.ts

import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3

Copy the source code

Copy the code below and paste it into components/ui/creepy-button.tsx

"use client";
import React, { useRef, useState } from "react";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
interface CreepyButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
className?: string;
coverClassName?: string;
}
type Coords = { x: number; y: number };
export const CreepyButton = ({
children,
className,
coverClassName,
onClick,
...props
}: CreepyButtonProps) => {
const eyesRef = useRef<HTMLSpanElement>(null);
const [eyeCoords, setEyeCoords] = useState<Coords>({ x: 0, y: 0 });
const [isHovered, setIsHovered] = useState(false);
const updateEyes = (e: React.MouseEvent | React.TouchEvent) => {
const userEvent =
"touches" in e ? (e as React.TouchEvent).touches[0] : (e as React.MouseEvent);
if (!eyesRef.current) return;
const eyesRect = eyesRef.current.getBoundingClientRect();
const eyesCenter = {
x: eyesRect.left + eyesRect.width / 2,
y: eyesRect.top + eyesRect.height / 2,
};
const cursor = { x: userEvent.clientX, y: userEvent.clientY };
const dx = cursor.x - eyesCenter.x;
const dy = cursor.y - eyesCenter.y;
const angle = Math.atan2(-dy, dx) + Math.PI / 2;
const visionRangeX = 180;
const visionRangeY = 75;
const distance = Math.hypot(dx, dy);
const x = (Math.sin(angle) * Math.min(distance, visionRangeX)) / visionRangeX;
const y = (Math.cos(angle) * Math.min(distance, visionRangeY)) / visionRangeY;
setEyeCoords({ x, y });
};
const resetEyes = () => {
setEyeCoords({ x: 0, y: 0 });
setIsHovered(false);
};
const pupilStyle = {
transform: `translate(calc(-50% + ${eyeCoords.x * 50}%), calc(-50% + ${eyeCoords.y * 50}%))`,
};
return (
<button
className={cn(
"relative min-w-[9em] rounded-xl bg-black cursor-pointer outline-none select-none group",
className
)}
onClick={onClick}
onMouseMove={(e) => {
updateEyes(e);
setIsHovered(true);
}}
onTouchMove={updateEyes}
onMouseLeave={resetEyes}
{...props}
>
<span
ref={eyesRef}
className="absolute flex items-center gap-[0.375em] right-[1em] bottom-[0.5em] h-[0.75em] z-0 pointer-events-none"
>
<motion.span
className="relative w-[0.75em] bg-white rounded-full overflow-hidden"
animate={{ height: ["0.75em", "0.75em", "0em", "0.75em"] }}
transition={{ duration: 3, times: [0, 0.92, 0.96, 1], repeat: Infinity }}
>
<span
className="absolute top-1/2 left-1/2 w-[0.375em] h-[0.375em] bg-black rounded-full"
style={pupilStyle}
/>
</motion.span>
</span>
<motion.span
className={cn(
"absolute inset-0 block rounded-xl bg-blue-500 text-white font-bold",
"flex items-center justify-center px-4 py-2",
coverClassName
)}
animate={{ rotate: isHovered ? -12 : 0 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
{children}
</motion.span>
<span className="block opacity-0 px-4 py-2 font-bold">
{children}
</span>
</button>
);
};
export default CreepyButton;

Usage

1import CreepyButton from "@/components/ui/creepy-button"
2
3export function CreepyButtonDemo() {
4return (
5 <CreepyButton>
6 Hover Me
7 </CreepyButton>
8)
9}

Props

Prop NameTypeDefaultDescription
childrenReact.ReactNode-The content to be displayed in the button.
classNamestring-Class for the button container.
coverClassNamestring-Class for the button visible cover.