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 (<buttonclassName={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}><spanref={eyesRef}className="absolute flex items-center gap-[0.375em] right-[1em] bottom-[0.5em] h-[0.75em] z-0 pointer-events-none"><motion.spanclassName="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 }}><spanclassName="absolute top-1/2 left-1/2 w-[0.375em] h-[0.375em] bg-black rounded-full"style={pupilStyle}/></motion.span></span><motion.spanclassName={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"23export function CreepyButtonDemo() {4return (5 <CreepyButton>6 Hover Me7 </CreepyButton>8)9}
Props
| Prop Name | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | - | The content to be displayed in the button. |
| className | string | - | Class for the button container. |
| coverClassName | string | - | Class for the button visible cover. |