Building an Elastic Volume Slider with Tooltip Using Motion
6 min read
Building an Elastic Volume Slider with Animated Tooltip Using Framer Motion
Modern web interfaces demand responsive, intuitive controls that provide clear feedback to users. Static controls get the job done, but thoughtfully crafted motion transforms basic interactions into memorable experiences.
In this article, I’ll walk through the technical implementation of an elastic volume slider with a dynamic tooltip that provides natural physical feedback using React and Motion.
Implementation: Step by Step
1. Setting Up Motion Values
Framer Motion's useMotionValue
hooks serve as the reactive foundation of our slider:
const clientX = useMotionValue(0);
const overflow = useMotionValue(0);
const scale = useMotionValue(1);
These values work together to track:
- The exact position of the user's pointer
- How far beyond the slider boundaries the user has dragged
- The current scale of the component during interaction
2. Creating Natural Resistance with Decay
To make the slider feel realistic when users drag past its boundaries, we need a way to create increasing resistance. This decay
function applies a mathematical curve that feels natural:
function decay(value: number, max: number) {
if (max === 0) return 0;
let entry = value / max;
let sigmoid = 2 * (1 / (1 + Math.exp(-entry)) - 0.5);
return sigmoid * max;
}
This function creates the "elastic" sensation by:
- Taking the raw overflow distance
- Applying a sigmoid curve (S-shaped) that naturally flattens at extremes
- Returning a value that creates more resistance the further users drag
You can see this in action in the SHSF UI elastic slider demo, which implements this exact approach.
3. Tracking User Interaction Position
We need to know where the user is interacting with our slider. This code detects the position and context:
useMotionValueEvent(clientX, "change", (latest) => {
if (ref.current) {
const { left, right } = ref.current.getBoundingClientRect();
if (latest < left) {
setRegion("left");
newValue = left - latest;
} else if (latest > right) {
setRegion("right");
newValue = latest - right;
} else {
setRegion("middle");
newValue = 0;
// Calculate position percentage for tooltip
const sliderWidth = right - left;
const relativeX = latest - left;
const positionPercentage = (relativeX / sliderWidth) * 100;
setTooltipPosition(Math.max(0, Math.min(100, positionPercentage)));
}
overflow.jump(decay(newValue, MAX_OVERFLOW));
}
});
This tracking system:
- Determines if the user is dragging within bounds or beyond left/right edges
- Sets a region state for conditional animations
- Calculates the tooltip position as a percentage
- Applies our decay function to create the elastic effect
4. Making the Slider Touch-Friendly
For mobile users, we improve the interaction by scaling the slider during touch:
<motion.div
onTouchStart={() => {
setIsInteracting(true);
animate(scale, 1.2);
}}
onTouchEnd={() => {
setIsInteracting(false);
animate(scale, 1);
}}
style={{
scale,
opacity: useTransform(scale, [1, 1.2], [0.7, 1]),
}}
className="flex w-full touch-none select-none items-center justify-center gap-3"
>
This creates a more comfortable touch experience by:
- Enlarging the slider by 20% when touched
- Increasing opacity for better visibility
- Returning smoothly to the original size when released
The SHSF UI library extends this approach across many components to ensure a consistent touch experience.
5. Creating the Elastic Stretching Effect
The core of our elastic slider is how it deforms when stretched:
<motion.div
style={{
scaleX: useTransform(() => {
if (ref.current) {
const { width } = ref.current.getBoundingClientRect();
return 1 + overflow.get() / width;
}
return 1;
}),
scaleY: useTransform(overflow, [0, MAX_OVERFLOW], [1, 0.8]),
transformOrigin: useTransform(() => {
if (ref.current) {
const { left, width } = ref.current.getBoundingClientRect();
return clientX.get() < left + width / 2 ? "right" : "left";
}
return "center";
}),
height: useTransform(scale, [1, 1.2], [6, 12]),
}}
className="w-full"
>
<Slider
value={[volume]}
onValueChange={handleValueChange}
min={0}
max={100}
step={0.01}
className="relative"
/>
</motion.div>
These dynamic transformations create a realistic effect:
- The slider stretches horizontally based on how far past the edge the user drags
- It compresses vertically as it stretches for a rubber-band feeling
- The stretch originates from the opposite side (pulls from the right when dragging left)
- The height increases during touch for better visibility
6. Animating Icons for Context
The volume icons at each end provide visual feedback based on drag direction:
<motion.div
animate={{
scale: region === "left" ? [1, 1.4, 1] : 1,
transition: { duration: 0.25 },
}}
style={{
x: useTransform(() =>
region === "left" ? -overflow.get() / scale.get() : 0
),
}}
>
<VolumeX className="size-5 opacity-70" />
</motion.div>
This creates engaging feedback:
- The appropriate icon pulses when users drag toward it
- Icons move along with the stretch for a connected feel
- Animation timing creates a smooth, professional look
For the complete implementation of these icon animations, you can explore the elastic slider component source code in the SHSF UI documentation.
7. Adding a Dynamic Tooltip
The tooltip shows the current volume and follows the interaction:
<AnimatePresence>
{(isInteracting || scale.get() > 1) && (
<motion.div
initial={{ opacity: 0, y: -20, scale: 0.8 }}
animate={{ opacity: 1, y: -28, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.8 }}
style={{
left: `${tooltipPosition}%`,
x: "-50%",
}}
className="absolute top-0 bg-background/95 px-2 py-1 rounded-md text-xs font-medium shadow-md border border-border/30 pointer-events-none"
>
{volume}%
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 w-2 h-2 bg-background/95 rotate-45 border-r border-b border-border/30"></div>
</motion.div>
)}
</AnimatePresence>
The tooltip enhances usability by:
- Appearing smoothly during interaction
- Following the drag position precisely
- Showing the exact volume percentage
- Disappearing with a clean exit animation
8. Adding Spring Physics for Natural Return
When users release after dragging beyond boundaries, we need to return smoothly:
onLostPointerCapture={() => {
animate(overflow, 0, { type: "spring", bounce: 0.5 });
setIsInteracting(false);
}}
This creates a satisfying conclusion to the interaction:
- The slider snaps back with spring physics
- The bounce value of 0.5 provides just enough elasticity without overdoing it
- The animation feels consistent regardless of how far the user stretched
Conclusion
The elastic boundaries and contextual animations work together to create an intuitive control that enhances user experience.
This implementation demonstrates how attention to motion details can transform standard UI components into delightful interactions without sacrificing functionality or performance.
If you’d like to implement this elastic slider in your own projects, you can use the ready-made component from the SHSF UI library.