to-spring-or-not-to-spring
Back to list

raphaelsalaja/to-spring-or-not-to-spring

To Spring Or Not To Spring

GitHub

Audit animation timing choices to decide when springs versus easing curves produce better motion.

133 installsMotionPerformanceFrontend4 demos

Real-world examples

Live HTML demos for this skill — rendered directly in the page. 4 examples.

  1. 01

    Decision framework

    Ask once: user-driven or system speaking? Tap each motion type to feel Spring, Easing, Linear, or None — and see why that timing wins.

  2. 02

    Springs for gestures

    Drag and flick both cards. The spring keeps velocity and survives interruption; the timed ease always finishes the same way.

  3. 03

    Easing for system state

    Modal enter ease-out / exit ease-in vs a restless spring toast. System announcements want a clear start and end.

  4. 04

    Duration & no animation

    Hover at 150ms vs 400ms, and typing feedback with vs without motion — high-frequency UI should stay quiet.

Skill markdown
# To Spring or Not To Spring

Review animation code for correct timing function selection based on interaction type.

## How It Works

1. Read the specified files (or prompt user for files/pattern)
2. Check against all rules below
3. Output findings in `file:line` format

## Rule Categories

| Priority | Category | Prefix |
|----------|----------|--------|
| 1 | Spring Selection | `spring-` |
| 2 | Easing Selection | `easing-` |
| 3 | Duration | `duration-` |
| 4 | No Animation | `none-` |

## Decision Framework

Ask: **Is this motion reacting to the user, or is the system speaking?**

| Motion Type | Best Choice | Why |
|-------------|-------------|-----|
| User-driven (drag, flick, gesture) | Spring | Survives interruption, preserves velocity |
| System-driven (state change, feedback) | Easing | Clear start/end, predictable timing |
| Time representation (progress, loading) | Linear | 1:1 relationship between time and progress |
| High-frequency (typing, fast toggles) | None | Animation adds noise, feels slower |

## Rules

### Spring Selection Rules

#### `spring-for-gestures`
Gesture-driven motion (drag, flick, swipe) must use springs.

**Fail:**
```tsx
<motion.div
  drag="x"
  transition={{ duration: 0.3, ease: "easeOut" }}
/>
```

**Pass:**
```tsx
<motion.div
  drag="x"
  transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
```

#### `spring-for-interruptible`
Motion that can be interrupted must use springs.

**Fail:**
```tsx
// User can click again mid-animation
<motion.div
  animate={{ x: isOpen ? 200 : 0 }}
  transition={{ duration: 0.3 }}
/>
```

**Pass:**
```tsx
<motion.div
  animate={{ x: isOpen ? 200 : 0 }}
  transition={{ type: "spring", stiffness: 400, damping: 25 }}
/>
```

#### `spring-preserves-velocity`
When velocity matters, use springs to preserve input energy.

**Fail:**
```tsx
// Fast flick and slow flick animate identically
onDragEnd={(e, info) => {
  animate(target, { x: 0 }, { duration: 0.3 });
}}
```

**Pass:**
```tsx
// Fast flick moves faster than slow flick
onDragEnd={(e, info) => {
  animate(target, { x: 0 }, {
    type: "spring",
    velocity: info.velocity.x,
  });
}}
```

#### `spring-params-balanced`
Spring parameters must be balanced; avoid excessive oscillation.

**Fail:**
```tsx
transition={{
  type: "spring",
  stiffness: 1000,
  damping: 5, // Too low - excessive bounce
}}
```

**Pass:**
```tsx
transition={{
  type: "spring",
  stiffness: 500,
  damping: 30, // Balanced - settles quickly
}}
```

### Easing Selection Rules

#### `easing-for-state-change`
System-initiated state changes should use easing curves.

**Fail:**
```tsx
// Toast notification using spring
<motion.div
  animate={{ y: 0 }}
  transition={{ type: "spring" }}
/>
// Feels restless for a simple announcement
```

**Pass:**
```tsx
<motion.div
  animate={{ y: 0 }}
  transition={{ duration: 0.2, ease: "easeOut" }}
/>
```

#### `easing-entrance-ease-out`
Entrances must use ease-out (arrive fast, settle gently).

**Fail:**
```css
.modal-enter {
  animation-timing-function: ease-in;
}
```

**Pass:**
```css
.modal-enter {
  animation-timing-function: ease-out;
}
```

#### `easing-exit-ease-in`
Exits must use ease-in (build momentum before departure).

**Fail:**
```css
.modal-exit {
  animation-timing-function: ease-out;
}
```

**Pass:**
```css
.modal-exit {
  animation-timing-function: ease-in;
}
```

#### `easing-transition-ease-in-out`
View/mode transitions use ease-in-out for neutral attention.

**Pass:**
```css
.page-transition {
  animation-timing-function: ease-in-out;
}
```

#### `easing-linear-only-progress`
Linear easing only for progress bars and time representation.

**Fail:**
```css
.card-slide {
  transition: transform 200ms linear; /* Mechanical feel */
}
```

**Pass:**
```css
.progress-bar {
  transition: width 100ms linear; /* Honest time representation */
}
```

### Duration Rules

#### `duration-press-hover`
Press and hover interactions: 120-180ms.

**Fail:**
```css
.button:hover {
  transition: background-color 400ms;
}
```

**Pass:**
```css
.button:hover {
  transition: background-color 150ms;
}
```

#### `duration-small-state`
Small state changes: 180-260ms.

**Pass:**
```css
.toggle {
  transition: transform 200ms ease;
}
```

#### `duration-max-300ms`
User-initiated animations must not exceed 300ms.

**Fail:**
```tsx
<motion.div transition={{ duration: 0.5 }} />
```

**Pass:**
```tsx
<motion.div transition={{ duration: 0.25 }} />
```

#### `duration-shorten-before-curve`
If animation feels slow, shorten duration before adjusting curve.

**Fail (common mistake):**
```css
/* Trying to fix slowness with sharper curve */
.element {
  transition: 400ms cubic-bezier(0, 0.9, 0.1, 1);
}
```

**Pass:**
```css
/* Fix slowness with shorter duration */
.element {
  transition: 200ms ease-out;
}
```

### No Animation Rules

#### `none-high-frequency`
High-frequency interactions should have no animation.

**Fail:**
```tsx
// Animated on every keystroke
function SearchInput() {
  return (
  

More from raphaelsalaja