Modeling Inertia Scrolling with Physics: From Theory to Vue Implementation
This article explains how to apply middle‑school physics concepts to implement realistic inertia (momentum‑based) scrolling and bounce effects in web front‑ends, compares popular UI libraries, derives the necessary formulas, chooses suitable acceleration constants, and provides a complete Vue.js code example.
When building interactive web pages we should not only focus on algorithms and code efficiency but also apply discipline knowledge such as physics; the combination of theory and practice greatly aids component selection and implementation understanding. This article revisits middle‑school physics and flexibly applies it to the implementation of inertia scrolling effects.
Inertia scrolling (also called momentum‑based scrolling) first appeared in iOS. When a user swipes a page and lifts the finger, the page continues to scroll for a short time, with speed and duration proportional to the gesture intensity. In Safari, the CSS rule -webkit-overflow-scrolling: touch enables this native bounce behavior.
Popular UI Library Effects
A comparison of two common H5 component libraries shows different inertia behaviors:
weui picker – the inertia effect is very weak; scrolling stops almost immediately after the finger is lifted.
vant picker – the inertia effect is clearer, but the bounce at the top/bottom still uses the same coefficient as ordinary scrolling, making the rebound feel disconnected.
Applying a Physics Model
The term “inertia” originates from Newton’s First Law: an object in motion stays in motion unless acted upon by a force. We can therefore model the scrolling target as a slider in a classic physics “slider‑block” system.
Phase 1 – Acceleration : While the user drags, a pulling force F_pull exceeds friction F_friction, causing the slider to accelerate from rest.
Phase 2 – Deceleration : After the finger is released, only friction acts, and the slider decelerates to a stop.
Key quantitative indicators are scroll distance s1, duration t1, and velocity v1. Using the displacement formula s = v0·t + ½·a·t² we obtain the velocity relationship for the first phase and, similarly, for the second phase with initial velocity v1 and final velocity 0. The derived formulas are illustrated in the following images:
In practice, squaring v1 makes the computed inertia distance too large, so the square operation is omitted and a constant acceleration A is introduced. Extensive testing shows a suitable value of A = 0.003.
Inertia Speed Curve
During the deceleration phase the displacement difference decreases over equal time intervals, matching the CSS ease‑out timing function ( cubic‑bezier(0,0,.58,1)). To achieve a more natural feel we adjust the curve to cubic‑bezier(.17,.89,.45,1):
Rebound (Bounce) Modeling
When the sliding element reaches a container boundary, a spring attached to the left end of the slider simulates the bounce. The process is divided into two stages:
Stage a : The slider pulls the spring while friction still acts, causing a rapidly decreasing speed.
Stage b : The spring releases, first accelerating the slider then decelerating it as friction dominates.
Using the same slider model we derive the bounce distance formulas; a constant B = 10 satisfies the derived equations.
CSS Animation Duration
No bounce: 2500ms Strong bounce: 400ms Weak bounce: 800ms Reset after bounce:
500msStart / Pause Conditions
Inertia starts only when the last touchmove and touchend events occur within 300ms and the traveled distance exceeds 15px. If the user touches the element again before inertia ends, the animation is paused; the current transform: matrix() is read via getComputedStyle and getPropertyValue, the Y‑offset is extracted, and the element’s translate value is updated.
Example Code (Vue.js)
<html>
<body>
<div id="app"></div>
<template id="tpl">
<div ref="wrapper"
@touchstart.prevent="onStart"
@touchmove.prevent="onMove"
@touchend.prevent="onEnd"
@touchcancel.prevent="onEnd"
@transitionend="onTransitionEnd">
<ul ref="scroller" :style="scrollerStyle">
<li v-for="item in list">{{item}}</li>
</ul>
</div>
</template>
<script>
new Vue({
el: '#app',
template: '#tpl',
computed: {
scrollerStyle() {
return {
'transform': `translate3d(0, ${this.offsetY}px, 0)`,
'transition-duration': `${this.duration}ms`,
'transition-timing-function': this.bezier,
};
},
},
data() {
return {
minY: 0,
maxY: 0,
wrapperHeight: 0,
duration: 0,
bezier: 'linear',
pointY: 0,
startY: 0,
offsetY: 0,
startTime: 0,
momentumStartY: 0,
momentumTimeThreshold: 300,
momentumYThreshold: 15,
isStarted: false,
};
},
mounted() {
this.$nextTick(() => {
this.wrapperHeight = this.$refs.wrapper.getBoundingClientRect().height;
this.minY = this.wrapperHeight - this.$refs.scroller.getBoundingClientRect().height;
});
},
methods: {
onStart(e) {
const point = e.touches ? e.touches[0] : e;
this.isStarted = true;
this.duration = 0;
this.stop();
this.pointY = point.pageY;
this.momentumStartY = this.startY = this.offsetY;
this.startTime = Date.now();
},
onMove(e) {
if (!this.isStarted) return;
const point = e.touches ? e.touches[0] : e;
const deltaY = point.pageY - this.pointY;
this.offsetY = Math.round(this.startY + deltaY);
const now = Date.now();
if (now - this.startTime > this.momentumTimeThreshold) {
this.momentumStartY = this.offsetY;
this.startTime = now;
}
},
onEnd() {
if (!this.isStarted) return;
this.isStarted = false;
if (this.isNeedReset()) return;
const absDeltaY = Math.abs(this.offsetY - this.momentumStartY);
const duration = Date.now() - this.startTime;
if (duration < this.momentumTimeThreshold && absDeltaY > this.momentumYThreshold) {
const momentum = this.momentum(this.offsetY, this.momentumStartY, duration);
this.offsetY = Math.round(momentum.destination);
this.duration = momentum.duration;
this.bezier = momentum.bezier;
}
},
onTransitionEnd() {
this.isNeedReset();
},
momentum(current, start, duration) {
const durationMap = { noBounce: 2500, weekBounce: 800, strongBounce: 400 };
const bezierMap = {
noBounce: 'cubic-bezier(.17,.89,.45,1)',
weekBounce: 'cubic-bezier(.25,.46,.45,.94)',
strongBounce: 'cubic-bezier(.25,.46,.45,.94)',
};
let type = 'noBounce';
const deceleration = 0.003;
const bounceRate = 10;
const bounceThreshold = 300;
const maxOverflowY = this.wrapperHeight / 6;
let overflowY;
const distance = current - start;
const speed = 2 * Math.abs(distance) / duration;
let destination = current + speed / deceleration * (distance < 0 ? -1 : 1);
if (destination < this.minY) {
overflowY = this.minY - destination;
type = overflowY > bounceThreshold ? 'strongBounce' : 'weekBounce';
destination = Math.max(this.minY - maxOverflowY, this.minY - overflowY / bounceRate);
} else if (destination > this.maxY) {
overflowY = destination - this.maxY;
type = overflowY > bounceThreshold ? 'strongBounce' : 'weekBounce';
destination = Math.min(this.maxY + maxOverflowY, this.maxY + overflowY / bounceRate);
}
return { destination, duration: durationMap[type], bezier: bezierMap[type] };
},
isNeedReset() {
let offsetY;
if (this.offsetY < this.minY) offsetY = this.minY;
else if (this.offsetY > this.maxY) offsetY = this.maxY;
if (typeof offsetY !== 'undefined') {
this.offsetY = offsetY;
this.duration = 500;
this.bezier = 'cubic-bezier(.165,.84,.44,1)';
return true;
}
return false;
},
stop() {
const matrix = window.getComputedStyle(this.$refs.scroller).getPropertyValue('transform');
this.offsetY = Math.round(+matrix.split(')')[0].split(', ')[5]);
},
},
});
</script>
</body>
</html>References
weui picker – https://github.com/Tencent/weui.js/blob/master/src/picker/scroll.js
vant picker – https://youzan.github.io/vant/mobile.html#/zh-CN/picker
Newton’s First Law – https://baike.baidu.com/item/牛顿第一运动定律
Uniform acceleration – https://baike.baidu.com/item/匀加速运动
Online cubic‑bezier editor – http://cubic-bezier.com/
Demo (CodePen) – https://codepen.io/JunreyCen/pen/arRYem
better‑scroll documentation – https://ustbhuangyi.github.io/better-scroll/doc/
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Aotu Lab
Aotu Lab, founded in October 2015, is a front-end engineering team serving multi-platform products. The articles in this public account are intended to share and discuss technology, reflecting only the personal views of Aotu Lab members and not the official stance of JD.com Technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
