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:
transformandopacityare 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:
:activepseudo-class: Targets pointer-down state perskills/apple-design/SKILL.mdguidance to respond immediately rather than on release:focus-visibleinclusion: Provides identical feedback for keyboard navigation without showing focus rings on mouse interactionwill-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:
- Default behavior: Full animation for standard users
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
clickinstead of:active: Delays feedback by 50–100ms - Using
scale(0)orscale(0.5): Violates physical material constraints - Transitioning
width,height, ormargin: 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.97provides optimal physical feedback without layout jitter - Timing: 160ms ease-out transition matches human perception of "instantaneous"
- Trigger:
:activepseudo-class ensures immediate pointer-down response - Property:
transformonly maintains 60fps GPU-composited animation - Accessibility: Always pair with
prefers-reduced-motionmedia 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →