Spring button
Compare a lightweight hover response with a tactile spring on press.
CSS hover transition
Spring press feedback
Source code
Copy and paste into your projectSpringButton.tsx
"use client"; import { motion, useReducedMotion } from "framer-motion";import Link from "next/link";import type { ComponentProps, ReactNode } from "react"; type ButtonProps = ComponentProps<typeof motion.button> & { children: ReactNode; pressSpring?: boolean;}; type LinkProps = { href: string; children: ReactNode; className?: string; variant?: "outline" | "filled";}; const spring = { type: "spring", stiffness: 500, damping: 28, mass: 0.5 } as const;const MotionLink = motion.create(Link); export function SpringButton({ children, className = "", pressSpring = true, ...props }: ButtonProps) { const prefersReducedMotion = useReducedMotion(); const canSpring = pressSpring && !prefersReducedMotion; return ( <motion.button {...props} whileTap={canSpring ? { scale: 0.94 } : undefined} transition={canSpring ? spring : undefined} className={`relative isolate overflow-hidden rounded-full bg-neutral-800 px-4 py-2 text-sm font-medium text-white focus:outline-none before:absolute before:inset-0 before:-z-10 before:scale-95 before:rounded-[inherit] before:bg-neutral-700 before:opacity-0 before:transition-[transform,opacity] before:duration-150 before:ease-out hover:before:scale-100 hover:before:opacity-100 focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 dark:bg-neutral-200 dark:text-neutral-900 dark:before:bg-white dark:focus-visible:ring-neutral-500 dark:focus-visible:ring-offset-neutral-900 motion-reduce:before:transition-none ${className}`} > {children} </motion.button> );} export function SpringLinkButton({ href, children, className = "", variant = "outline" }: LinkProps) { const prefersReducedMotion = useReducedMotion(); const variantClassName = variant === "filled" ? "border-neutral-800 bg-neutral-800 text-white hover:border-neutral-700 hover:bg-neutral-700 dark:border-neutral-200 dark:bg-neutral-200 dark:text-neutral-900 dark:hover:border-white dark:hover:bg-white" : "border-neutral-200 bg-white text-neutral-700 hover:border-neutral-300 hover:text-neutral-900"; return ( <MotionLink href={href} whileTap={prefersReducedMotion ? undefined : { scale: 0.96 }} transition={spring} className={`inline-flex items-center rounded-full border px-3 py-1.5 text-sm font-medium transition-transform duration-150 hover:-translate-y-px focus:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 ${variantClassName} ${className}`} > {children} </MotionLink> );}