How to Implement Responsive Button Press Feedback with Proper Transform Scale Values

Use transform: scale(0.97) with a 160ms ease-out transition on :active for performant, accessible press feedback that mimics physical button compression.

Implementing responsive button press feedback requires balancing visual realism, GPU performance, and accessibility standards. The emilkowalski/skills repository codifies a battle-tested pattern for this interaction across its animation standards and design engineering guidelines. This article breaks down the exact values, timing, and implementation from the source code.

Why Transform Scale Values Matter for Press Feedback

The repository's animation standards specify scale values between 0.95 and 0.98 for press states. A scale(0.97) reduction—approximately 3% shrinkage—creates the visual impression of a physical surface compressing under pressure without triggering layout recalculation or perceptible displacement.

According to skills/review-animations/STANDARDS.md, the constraints on these values serve multiple purposes:

  • Compositor-only rendering: transform and opacity are GPU-accelerated properties that bypass layout and paint phases
  • Physical realism: Values approaching scale(0) would cause elements to disappear entirely, violating material physics
  • Jitter prevention: Subtle scaling avoids the 1px rounding errors that plague larger transform values on subpixel-positioned elements

The Canonical CSS Implementation

The repository provides a complete implementation pattern in skills/emil-design-eng/SKILL.md. This CSS follows the standards exactly:

/* Core timing variables */
:root {
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.5rem 1rem;
  background: var(--color-primary);
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  /* GPU acceleration hint */
  will-change: transform;
}

/* Press feedback: the critical implementation */
.button:active,
.button:focus-visible {
  transform: scale(0.97);
  transition: transform 160ms var(--ease-out);
}

/* Accessibility: respect reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
  .button:active,
  .button:focus-visible {
    transition: none;
    transform: none;
  }
}

Key implementation details from the source:

  • :active pseudo-class: Targets pointer-down state per skills/apple-design/SKILL.md guidance to respond immediately rather than on release
  • :focus-visible inclusion: Provides identical feedback for keyboard navigation without showing focus rings on mouse interaction
  • will-change: transform: Signals compositor preparation—use sparingly, only on elements expecting frequent interaction

Timing and Easing Specifications

The 160ms duration with an ease-out curve derives from specific performance constraints documented in skills/review-animations/STANDARDS.md:

Parameter Value Rationale
Duration 160ms Upper bound of 100–160ms "instantaneous" feedback window
Easing cubic-bezier(0.23, 1, 0.32, 1) Deceleration curve—fast start, gentle settle
Property transform only Guarantees 60fps on all target devices

The ease-out curve produces a perceptually faster response than linear timing while maintaining smooth completion. The repository explicitly prohibits spring physics or bounce effects for press feedback—these introduce latency and feel unresponsive.

Framework Integration Examples

React Component

// Button.tsx
import './Button.css';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary';
}

export const Button = ({ 
  children, 
  variant = 'primary',
  className = '',
  ...rest 
}: ButtonProps) => (
  <button 
    className={`button button--${variant} ${className}`}
    {...rest}
  >
    {children}
  </button>
);

The component delegates all press feedback to CSS, following the repository's separation-of-concerns pattern.

CSS Custom Properties for Theming

/* Standardized scale tokens per STANDARDS.md */
:root {
  --scale-press: 0.97;
  --scale-press-strong: 0.95;
  --duration-press: 160ms;
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}

.button--primary:active {
  transform: scale(var(--scale-press));
  transition: transform var(--duration-press) var(--ease-out);
}

.button--destructive:active {
  /* Stronger feedback for high-consequence actions */
  transform: scale(var(--scale-press-strong));
}

Accessibility Requirements

The repository mandates dual support for motion preferences:

  1. Default behavior: Full animation for standard users
  2. prefers-reduced-motion: reduce: Complete disablement of transforms and transitions

This pattern appears consistently across skills/review-animations/STANDARDS.md and skills/improve-animations/AUDIT.md. Never reduce to transform: scale(1) alone—this still triggers compositor work for no visual benefit.

Common Implementation Mistakes

Per skills/improve-animations/AUDIT.md, these anti-patterns flag automated review:

  • Animating on click instead of :active: Delays feedback by 50–100ms
  • Using scale(0) or scale(0.5): Violates physical material constraints
  • Transitioning width, height, or margin: Causes layout thrashing
  • Durations exceeding 200ms: Perceived as sluggish, not responsive

Source Files and Standards

File Path Purpose
skills/review-animations/STANDARDS.md Canonical scale (0.97) and timing (160ms) definitions
skills/emil-design-eng/SKILL.md CSS implementation reference
skills/apple-design/SKILL.md Pointer-down response timing guidelines
skills/improve-animations/AUDIT.md Automated detection of missing/incorrect press feedback

Summary

  • Scale value: 0.97 provides optimal physical feedback without layout jitter
  • Timing: 160ms ease-out transition matches human perception of "instantaneous"
  • Trigger: :active pseudo-class ensures immediate pointer-down response
  • Property: transform only maintains 60fps GPU-composited animation
  • Accessibility: Always pair with prefers-reduced-motion media query disablement

Frequently Asked Questions

What happens if I use scale(0.9) instead of scale(0.97)?

A 10% reduction feels exaggerated and can cause perceptible layout shift on adjacent elements. The 0.95–0.98 range in skills/review-animations/STANDARDS.md was determined through device testing to provide unambiguous feedback without visual instability.

Why 160ms specifically and not 150ms or 200ms?

The 100–160ms window represents the threshold where human perception transitions from "instantaneous" to "animated." The repository standardizes on 160ms as the upper bound to accommodate lower-refresh-rate displays while maintaining responsiveness on 120Hz+ devices.

Should I animate the release state separately?

No—the repository pattern specifies a single :active state with symmetric transition timing. Asymmetrical press/release timing creates cognitive dissonance; the ease-out curve naturally handles both phases when the :active state clears.

How do I test reduced motion support?

Enable "Reduce motion" in system accessibility settings (macOS: System Settings > Accessibility > Display; iOS: Settings > Accessibility > Motion). The button should show no transform change and complete state transitions instantly.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →