Physically inspired press deformation button with spring recovery, configurable squash scale, elasticity, border treatment, loading spinner state, and disabled state.
Preview
Customize
6 props0.91
380
Installation
Copies the component source and its CSS Module into your project.
npx motionfoundry add elastic-press-buttonWrites components/ui/elastic-press-button.tsx and components/styles/elastic-press-button.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
example.tsx
1import { ElasticPressButton } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <ElasticPressButton />6 );7}Props
Accessibility
Sets aria-busy={loading} and disabled={disabled || loading} to prevent duplicate form submissions and announces state changes to screen readers.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
components/ui/elastic-press-button.tsx
1"use client";2 3import * as React from "react";4import { motion, useReducedMotion } from "motion/react";5import styles from "../styles/elastic-press-button.module.css";6 7/** Native button attributes whose names collide with Motion's animation and gesture handlers. */8type MotionConflictingKeys = "onAnimationStart" | "onDrag" | "onDragStart" | "onDragEnd";9 10export interface ElasticPressButtonProps11 extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, MotionConflictingKeys> {12 /** Button label */13 label?: string;14 /** Scale factor during active press */15 pressScale?: number;16 /** Spring stiffness for recovery */17 elasticity?: number;18 /** Border visual treatment */19 borderStyle?: "specular" | "solid" | "minimal";20 /** Loading state */21 loading?: boolean;22}23 24export function ElasticPressButton({25 children,26 label = "Commit Production Release",27 pressScale = 0.91,28 elasticity = 380,29 borderStyle = "specular",30 loading = false,31 disabled = false,32 className = "",33 ...rest34}: ElasticPressButtonProps) {35 const prefersReducedMotion = useReducedMotion();36 const isInactive = disabled || loading;37 38 return (39 <motion.button40 type="button"41 disabled={isInactive}42 aria-busy={loading}43 whileHover={prefersReducedMotion || isInactive ? undefined : { scale: 1.03, y: -1 }}44 whileTap={45 prefersReducedMotion || isInactive46 ? undefined47 : { scaleX: 1 + (1 - pressScale) * 0.45, scaleY: pressScale }48 }49 transition={{ type: "spring", stiffness: elasticity, damping: 14, mass: 0.45 }}50 className={`${styles.button} ${styles[borderStyle]} ${isInactive ? styles.disabled : ""} ${className}`.trim()}51 {...rest}52 >53 {loading && <span aria-hidden="true" className={styles.spinner} />}54 <span>{loading ? "Processing..." : children ?? label}</span>55 </motion.button>56 );57}