CoreInteractive Buttons and Controlsmotion/react

ElasticPressButton

Physically inspired press deformation button with spring recovery, configurable squash scale, elasticity, border treatment, loading spinner state, and disabled state.

Preview

Customize

6 props
0.91
380

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add elastic-press-button

Writes 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

PropTypeDefault
label

Button action label.

string'Commit Production Release'
pressScale

Target compression scale when pressed.

number0.91
elasticity

Spring recovery stiffness constant.

number380
borderStyle

Perimeter border styling.

'specular' | 'solid' | 'minimal''specular'
loading

Displays an inline spinner and sets aria-busy.

booleanfalse
disabled

Disables interaction and reduces contrast.

booleanfalse

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}