You Might Also Like
Elastic Drag Slider — Free Spring Physics JS Snippet
Elastic Drag Slider · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Elastic Drag Slider — A Real Spring-Physics Simulation for a Range Input Thumb

Most "springy" sliders on the web fake it with a CSS transition: left 0.3s cubic-bezier(...) that includes a slight overshoot curve. That produces a fixed, identical bounce no matter how the user interacts with it — drag the thumb one pixel or fling it across the whole track, and you get the exact same canned animation. This snippet does something different: it runs an actual spring simulation, every animation frame, driven by the real velocity of the user's drag. Flick the thumb hard and it overshoots further and takes longer to settle; nudge it gently and it barely wobbles. That distinction — physically *simulated* motion versus a pre-baked easing curve — is the entire point of the snippet, and the about text below walks through exactly how the loop works.
The three numbers that define a spring
The whole simulation lives in three variables: position (where the thumb currently is, 0-100), target (where it is being pulled toward), and velocity (how fast position is currently changing). Nothing else is needed to simulate a damped spring. Every animation frame, two forces are computed and summed into an acceleration: springForce = -STIFFNESS * (position - target) — a restoring force proportional to how far the thumb is from its target, always pointing back toward it, exactly like Hooke's law for a physical spring — and dampingForce = -DAMPING * velocity, a force that always opposes the current velocity, bleeding energy out of the system so it does not oscillate forever. velocity += acceleration; position += velocity; is literally Euler integration: update velocity from acceleration, then update position from velocity, once per frame.
Why STIFFNESS and DAMPING are tuned as a pair
STIFFNESS (0.22 here) controls how hard the spring pulls back toward the target — higher values snap back faster but overshoot more violently. DAMPING (0.62) controls how much that overshoot is resisted — too little damping and the thumb oscillates visibly several times before settling (an "underdamped" spring); too much and it never overshoots at all, which defeats the point of an elastic slider (a "critically damped" or "overdamped" spring). These two constants were tuned together by hand until a fast flick produced one clean, visible overshoot and settle rather than a pinball-like wobble — changing one without the other is the most common way to make a spring feel wrong.
Turning drag speed into launch velocity
While the pointer is actively down, the thumb tracks the cursor's position exactly (position = val) rather than lagging behind through the spring — a slider should feel perfectly responsive while you are holding it. But on every pointermove, the code also computes velocity = ((val - lastPointerValue) / dt) * 16, a real speed estimate from how far the value moved divided by how much time passed. That velocity is not used while dragging, but the moment pointerup fires, it becomes the *initial velocity fed into the spring loop* — so a fast flick releases with real carried momentum, producing a bigger overshoot, while a slow, careful drag releases with almost no velocity and barely overshoots at all. This is the key mechanism that makes the spring feel connected to the user's actual gesture rather than generic.
The requestAnimationFrame settle-and-stop loop
tick() runs on requestAnimationFrame, updates the physics, calls render(), and then checks whether the system is close enough to rest (Math.abs(velocity) < SETTLE_EPSILON and Math.abs(position - target) < SETTLE_EPSILON) to stop scheduling itself. This matters for two reasons: it avoids running an animation loop forever after the spring has visually stopped moving, and it snaps position exactly to target on the final frame so floating-point drift never leaves the thumb a fraction of a pixel off from its true value.
Why the fill and thumb are allowed to overshoot past 0-100 visually
render() deliberately clamps the *fill bar width* to 0-100% but lets the *thumb's left position* overshoot slightly past those bounds during the spring's bounce, since a real spring released near either end of the track should be able to visibly push past the edge before springing back — clamping the thumb too aggressively would silently remove the elastic feel exactly where it is most visible.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Hand this snippet's JS to an AI assistant like Claude and ask it to explain, line by line, how springForce and dampingForce combine into acceleration inside tick() — understanding that one function is the fastest way to really grasp spring physics for UI. From there, good extensions to request: a second spring axis for a 2D drag pad instead of a 1D slider, a "snap to nearest step" behavior once the spring settles (e.g. rounding to the nearest 10), or exposing STIFFNESS/DAMPING as live-adjustable sliders themselves so you can feel the tuning tradeoffs interactively.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a range slider in plain HTML, CSS, and JavaScript whose thumb visibly overshoots and springs back into place using real spring physics, no libraries, no CSS transition doing the settling motion.
Requirements:
- A draggable circular thumb on a horizontal track, with a filled portion of the track showing the current value, plus a numeric value label that updates live.
- Implement an actual spring simulation with three tracked numbers per frame: current position, a target position, and current velocity — do not use a CSS transition or cubic-bezier timing function to produce the overshoot/settle motion.
- Every animation frame (via requestAnimationFrame), compute a restoring spring force proportional to the negative of (position minus target) scaled by a stiffness constant, and a damping force proportional to the negative of velocity scaled by a damping constant; sum them into an acceleration, integrate acceleration into velocity, and integrate velocity into position.
- While the user is actively dragging the thumb with the pointer, the thumb must track the pointer's position exactly (no lag or spring lag during the drag itself) — the spring should only take over once the pointer is released.
- While dragging, continuously measure the real velocity of the pointer's movement (change in value divided by change in time between move events) and carry that measured velocity into the spring simulation as its initial velocity at the moment of release, so a fast flick produces a bigger overshoot than a slow drag.
- Automatically stop the requestAnimationFrame loop once velocity and the distance to target both drop below a small threshold, snapping exactly to the target value to avoid floating-point drift, and restart the loop cleanly if the user interacts again.
- Support clicking anywhere on the track to set a new target (animated through the same spring, not an instant jump) and full keyboard arrow-key control that also goes through the spring rather than jumping instantly.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Drag the thumb slowlyThe thumb tracks your cursor exactly with no lag, and the value label updates in real time as you move.
- 2Drag it quickly and release mid-motionOn release, the thumb keeps moving briefly in the direction you were dragging, overshoots past where you let go, then springs back and settles — the faster the flick, the bigger the overshoot.
- 3Release it gently near the middleA slow, careful drag produces almost no overshoot at all, since very little velocity was captured at the moment of release.
- 4Click anywhere on the track (not the thumb)The target jumps to that position and the spring animates the thumb there from its current position, still with a small settle motion.
- 5Use the arrow keys after focusing the thumbLeft/Right or Up/Down nudge the value by 5 and animate through the same spring physics rather than jumping instantly, keeping keyboard control visually consistent with drag.
- 6Watch it near the track edgesFlick hard toward 0 or 100 and the thumb visibly pushes slightly past the edge before the spring pulls it back, since only the fill bar — not the thumb's position — is hard-clamped.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
A CSS cubic-bezier transition always plays the exact same fixed curve regardless of how the interaction happened — a one-pixel nudge and a full-track flick would animate identically. The whole point of this snippet is that the overshoot amount and settle time genuinely depend on how fast the user was dragging when they released, which requires computing real velocity from pointer movement and feeding it into an actual spring equation every frame — something a static CSS curve structurally cannot do.
STIFFNESS scales the restoring force pulling position toward target — raise it for a snappier, faster-reacting spring, lower it for a looser, slower one. DAMPING scales the force that resists velocity and bleeds energy out of the system — raise it to reduce oscillation (less bouncy, settles faster), lower it to allow more visible overshoot and wobble before it settles. Change them together: a high-stiffness, low-damping spring will oscillate wildly, while a high-stiffness, high-damping spring will feel snappy with barely any bounce at all.
tick() checks whether both velocity and the remaining distance to target are below a small epsilon threshold; once true (and the user is not actively dragging), it snaps position exactly to target, zeroes velocity, and does not reschedule itself. This avoids burning CPU on an animation loop that is visually already at rest, and the final snap avoids leaving position a fraction of a pixel off from target due to floating-point rounding across many frames.
Yes. Keep position/target/velocity in refs (not state, since they change every animation frame and do not need to trigger re-renders directly) and drive the visual thumb/fill styles imperatively inside the tick loop, same as the vanilla version. Start the requestAnimationFrame loop from pointer event handlers attached in a useEffect (React) or onMounted (Vue) or ngAfterViewInit (Angular), and make sure to call cancelAnimationFrame on whatever ID the loop last scheduled inside the component's cleanup/unmount hook so a lingering spring animation does not keep writing to a detached DOM node.
Increase both STIFFNESS and DAMPING proportionally for a snappier-but-still-controlled feel, or lower both proportionally for a looser, slower one — keep their ratio roughly similar to what shipped here to avoid the underdamped or overdamped extremes described above. SETTLE_EPSILON generally does not need to change; it is independent of stiffness and damping and just defines "close enough to stop animating."