Skip to main content
Version: Next

withDecay

The withDecay modifier simulates natural momentum with friction. It's perfect for scenarios where you want values to gradually slow down, like a thrown ball losing speed or a scroll that decelerates naturally.

Why Use withDecay?

Decay animations feel natural because they:

  • Simulate physics - Realistic momentum and friction
  • Feel responsive - Natural slowdown after gestures
  • Work great with gestures - Perfect for drag, scroll, and wheel interactions
  • No fixed destination - Values slow down naturally rather than reaching a target

Basic Syntax

withDecay(velocity, options?)

Parameters

  • velocity (required): The initial velocity. Higher values = faster initial movement.
  • options (optional): Configuration object for decay behavior.

Simple Example

The simplest decay animation:

import { useValue, withDecay, animate } from 'react-ui-animate';

function MyComponent() {
const [x, setX] = useValue(0);

return (
<>
<button onClick={() => setX(withDecay(0.5))}>
Start Decay
</button>
<animate.div
style={{
translateX: x,
width: 100,
height: 100,
background: 'teal',
}}
/>
</>
);
}

Configuration Options

OptionTypeDefaultDescription
onStart() => void-Called when animation starts
onChange(value) => void-Called on every frame with current value
onComplete() => void-Called when animation finishes

Real-World Examples

Example 1: Drag with Momentum

Perfect for draggable elements that continue moving after release:

import { useValue, useDrag, withDecay, animate } from 'react-ui-animate';

function DraggableWithMomentum() {
const ref = useRef(null);
const [x, setX] = useValue(0);
const [velocity, setVelocity] = useValue(0);

useDrag(ref, ({ down, movement, velocity: dragVelocity }) => {
if (down) {
setX(movement.x);
setVelocity(dragVelocity.x);
} else {
// Apply decay with the drag velocity
setX(withDecay(velocity * 0.1));
}
});

return (
<animate.div
ref={ref}
style={{
translateX: x,
width: 100,
height: 100,
background: 'teal',
cursor: 'grab',
}}
/>
);
}

Example 2: Scroll Momentum

Simulate natural scroll deceleration:

function ScrollableContent() {
const [scrollY, setScrollY] = useValue(0);
const [velocity, setVelocity] = useValue(0);

useScroll(window, ({ scroll, velocity: scrollVelocity }) => {
setScrollY(scroll.y);
setVelocity(scrollVelocity.y);
});

const applyMomentum = () => {
// Continue scrolling with decay
setScrollY(withDecay(velocity * 0.05));
};

return (
<div>
<button onClick={applyMomentum}>Apply Momentum</button>
<animate.div
style={{
translateY: -scrollY,
}}
>
{/* Content */}
</animate.div>
</div>
);
}

Example 3: Flick Gesture

Create a flickable card:

function FlickableCard() {
const ref = useRef(null);
const [x, setX] = useValue(0);
const [velocity, setVelocity] = useValue(0);

useDrag(ref, ({ down, movement, velocity: dragVelocity }) => {
if (down) {
setX(movement.x);
setVelocity(dragVelocity.x);
} else {
// Flick with decay based on drag velocity
if (Math.abs(velocity) > 0.5) {
setX(withDecay(velocity * 0.15));
} else {
// Snap back if velocity is too low
setX(withSpring(0));
}
}
});

return (
<animate.div
ref={ref}
style={{
translateX: x,
width: 200,
height: 200,
background: 'teal',
borderRadius: 8,
cursor: 'grab',
}}
>
Flick me
</animate.div>
);
}
import React from 'react';
import { useValue, animate, withDecay } from 'react-ui-animate';
import '../../styles.css';

export default function BasicExample() {
  const [obj, setObj] = useValue({ x: 0, y: 0, width: 100, height: 100 });

  return (
    <div className="container">
      <button
        className="button buttonPrimary"
        onClick={() =>
          setObj(
            withDecay(0.3, {
              onStart: () => console.log('START'),
              onComplete: () => console.log('Animation complete'),
            })
          )
        }
      >
        Start
      </button>
      <button
        className="button buttonSecondary"
        onClick={() => setObj({ x: 0, y: 0, width: 100, height: 100 })}
      >
        Reset
      </button>

      <animate.div
        style={{
          width: obj.width,
          height: 100,
          backgroundColor: 'teal',
          margin: '20px auto 0',
          translateX: obj.x,
          translateY: obj.y,
          borderRadius: 8,
        }}
      />
    </div>
  );
}

Understanding Velocity

Velocity determines how fast the animation starts:

  • Low velocity (0.1-0.3) - Slow, gentle deceleration
  • Medium velocity (0.5-1.0) - Moderate speed, natural feel
  • High velocity (1.5+) - Fast start, longer deceleration

Using with Gestures

withDecay works perfectly with gesture hooks:

useDrag(ref, ({ down, movement, velocity }) => {
if (down) {
setX(movement.x);
} else {
// Use drag velocity for natural momentum
setX(withDecay(velocity.x * 0.1));
}
});

Using Callbacks

Track decay progress:

function DecayAnimation() {
const [x, setX] = useValue(0);

const startDecay = () => {
setX(withDecay(0.5, {
onStart: () => {
console.log('Decay started');
},
onChange: (value) => {
console.log('Current position:', value);
},
onComplete: () => {
console.log('Decay complete - stopped');
},
}));
};

return (
<>
<button onClick={startDecay}>Start Decay</button>
<animate.div
style={{
translateX: x,
width: 100,
height: 100,
background: 'teal',
}}
/>
</>
);
}

Common Patterns

Pattern 1: Drag and Release

useDrag(ref, ({ down, movement, velocity }) => {
if (down) {
setX(movement.x);
} else {
// Continue with momentum
setX(withDecay(velocity.x * 0.1));
}
});

Pattern 2: Scroll Momentum

useScroll(container, ({ velocity }) => {
// Apply decay when scroll stops
setScrollPosition(withDecay(velocity.y * 0.05));
});

Pattern 3: Flick to Dismiss

useDrag(ref, ({ down, movement, velocity }) => {
if (down) {
setX(movement.x);
} else {
// Flick with high velocity = dismiss
if (Math.abs(velocity.x) > 1.0) {
setX(withDecay(velocity.x * 0.2));
// Dismiss when far enough
if (Math.abs(x) > 200) {
onDismiss();
}
} else {
// Snap back
setX(withSpring(0));
}
}
});

Best Practices

✅ Do

  • Use withDecay with gesture hooks for natural momentum
  • Adjust velocity multiplier based on your needs (0.05-0.2 is common)
  • Combine with withSpring for snap-back behavior
  • Use for scroll, drag, and wheel interactions

❌ Don't

  • Don't use withDecay for fixed destination animations (use withSpring or withTiming)
  • Don't set velocity too high - can cause janky animations
  • Don't forget to handle edge cases (snap back, boundaries)
  • Don't use for simple show/hide animations

Performance Tips

  1. Decay is optimized - Very performant for gesture-based animations
  2. Use transforms - translateX, translateY are GPU-accelerated
  3. Combine with gestures - Works best with useDrag, useScroll, useWheel
  4. Set boundaries - Prevent values from going too far

Troubleshooting

Animation doesn't slow down:

  • Check that velocity is being set correctly
  • Verify you're using withDecay correctly

Animation stops too quickly:

  • Increase velocity multiplier
  • Check if boundaries are cutting off the animation

Animation feels janky:

  • Make sure you're using transforms, not layout properties
  • Reduce velocity if too high

Next Steps

  • Learn about useDrag for drag interactions
  • Explore useScroll for scroll-based animations
  • Check out withSequence to combine decay with other animations