本文详细解析如何使用 JavaScript 实现自定义滚动条,解决默认样式受限问题。核心内容包括滚动条高度与可视区域的比例计算、点击按钮时的移动距离控制(含单次点击与长按区分)、以及拖拽滚动条时页面滚动的同步算法。提供基于 seajs 和 jQuery 的完整示例代码,涵盖 CSS 布局与 JS 逻辑实现。
自定义滚动条的需求背景
在实际开发中,当内容超出固定容器范围时,通常需要通过滚动条来展示溢出部分。
然而,浏览器默认的滚动条样式往往难以满足产品设计需求,且 CSS 对原生滚动条样式的控制能力有限。此时,借助 JavaScript 实现自定义滚动条成为了一种可行的解决方案。
虽然网络上存在多种现成的插件,但自行开发不仅更贴合具体需求,还能在编写过程中加深理解,实现真正的技术自主。
核心问题与框架结构
在实现过程中,主要需要解决以下三个核心问题:
- 1. 滚动条的高度设定
- 2. 点击滚动条上下按钮时,滚动条应移动的距离
- 3. 拖动滚动条时,每移动 1px 滚动条,页面内容需滚动的距离
整体框架结构大致如下:

滚动条高度与比例计算
首先分析第一个问题。
已知内容区域的总高度、可视区域高度以及滚动条的可滚动区域高度。由于内容区域与滚动条的移动距离成正比,该问题可通过比例关系轻松解决:
滚动条可移动范围 / 滚动条高度 = 内容高度 / 内容可视高度
按钮点击与长按交互逻辑
接下来探讨点击按钮时的滚动行为。
此处通过参数 distance 设定每次点击按钮时内容区域滚动的距离。调整该值可控制滚动速度,若有更优方案欢迎交流。
已知内容区域的每次滚动距离后,需进一步计算滚动条相应的移动距离:
滚动条可移动范围 / 滚动条每次移动距离 = 内容区域高度 / 内容区域每次移动多少距离
效果展示如下:

此外,还需区分单次点击与长按操作。
通过判断按钮按下到松开的时间间隔来实现:设定时间小于 100ms 为单次点击,否则视为长按。

拖拽同步滚动算法
关于拖动滚动条时的同步滚动问题:每移动 1px 滚动条,内容区域需滚动多少?
首先计算 1px 占滚动条可移动范围的百分比,再用内容区域高度除以该百分比,即可得出滚动条每移动 1px 时内容区域的相对滚动距离。
内容区域滚动的距离 = 内容区域高度 / (滚动条滚动区域 / 1)

完整代码实现与注意事项
以下是完整的 Demo 代码:
注意:代码基于 seajs 模块加载规范编写,请留意文件加载配置。
CSS 样式部分:
.wapper{scrollbar-3dlight-color:#000; position:relative; height:302px;width:300px;overflow:hidden;margin:0 auto;line-height:40px;text-align:center;}
.area{background-color:#E2E2EF;width:100%; position:absolute;top:0px;left:0px;}
.bar{position:absolute;top:0px;right:0px; height:100%;width:1rem;background-color:#ccc;}
.scroll,.middle,.forward,.backward{display:block;cursor:pointer;position:absolute;right:0px;width:100%;}
.forward,.backward{height:16px;background-color:#6868B1;}
.middle{background-color:rgba(255, 255, 255, 0.22);top:16px;cursor:auto;}
.scroll{position:absolute;top:0px;background-color:#C2C2E9;}
.forward{top:0px;}
.backward{bottom:0px;}
HTML 结构部分:
<div class="wapper">
<div class="area">
<p>1、this is content</p>
<p>2、this is content</p>
<p>3、this is content</p>
<p>4、this is content</p>
<p>5、this is content</p>
<p>6、this is content</p>
<p>7、this is content</p>
<p>8、this is content</p>
<p>9、this is content</p>
<p>10、this is content</p>
<p>11、this is content</p>
</div>
<div class="bar">
<span class="forward"></span>
<span class="middle"><em class="scroll"></em></span>
<span class="backward"></span>
</div>
</div>
<script type="text/javascript" src="../../lib/seajs/sea.js"></script>
<script type="text/javascript" src="../../lib/base/1.0.x/base.js"></script>
<script type="text/javascript">
seajs.use(['lib/jquery/1.11.x/index.js', '_example/simulationScroll/simulationScroll.js'], function($, scroll) {
scroll.init({
wapper: $('.wapper'),
distance: 10,
});
});
JavaScript 逻辑部分:
define(function(require, exports, module) {
'use strict';
var $ = require('lib/jquery/1.11.x/index.js');
var parameter = null;
//检测设备类型
var startWhen, endWhen, moveWhen;
var u = navigator.userAgent;
if ( u.match(/\b(Windows\sNT|Macintosh)\b/) ) {
// 鼠标
startWhen = 'mousedown';
endWhen = 'mouseup';
moveWhen = 'mousemove';
} else {
// 触摸屏
startWhen = 'touchstart';
endWhen = 'touchend';
moveWhen = 'touchmove';
}
var simulation = {
_mousedownTimer: 0,
_setintervalId: 0,
_longClick: false, //是否长点击
_turnOf: null, //滚动方向
init: function(options) {
var t = this;
t._scroll = $('.scroll'); //滚动条
t._wapper = options.wapper.find('.area'); //内容区域
t._distance = options.distance; //点击上下按钮页面每次滚动的距离
var forward = $('.forward'),
middle = $('.middle'),
backward = $('.backward');
parameter = {
view: t._wapper.parent().innerHeight(), //视图高度
page: t._wapper.height(), //内容高度
barArea: 0, //滚动条可移动范围
scrollHeight: 0, //滚动条的高度
scrollDistance: 0 //滚动条每次滚动的距离
};
//初始化滚动条
if (parameter.page > parameter.view) {
//滚动条可移动范围
middle.height( parameter.view - forward.height() * 2);
parameter.barArea = middle.height();
//滚动条高度 = 滚动条可滚动范围 / (页面高度 / 可视高度)的百分比
parameter.scrollHeight = parameter.barArea / (parameter.page / parameter.view) ;
t._scroll.height(parameter.scrollHeight);
//滚动条每次滚动的距离 = 滚动条可移动范围 * 页面每次滚动的百分比
parameter.scrollDistance = parameter.barArea / (parameter.page / t._distance) ;
//拖动滚动条
t.liveEvent();
//点击向前按钮,如果按下鼠标到松开鼠标的时长<100ms,则为单次点击
forward.bind(startWhen, function(e){
t._turnOf = 'forward';
t.longPress(e, t.direction );
}).bind(endWhen, function(e) {
t.mouseupFun(e, t.direction);
t._turnOf = null;
});
//点击向后按钮
backward.bind(startWhen, function(e){
t.longPress(e, t.direction );
}).bind(endWhen, function(e){
t.mouseupFun(e, t.direction );
});
//注册鼠标滚动事件
// FF
if(document.addEventListener){
document.addEventListener('DOMMouseScroll',t.mouseRuning,false);
}
//其它浏览器
document.onmousewheel = t.mouseRuning;
}
},
//鼠标滚动
mouseRuning: function(e) {
var t = simulation;
e = e || window.event;
//ie、FF
if (e.detail) {
if (e.detail < 0) {
t._turnOf = 'forward';
t.direction ();
} else{
t._turnOf = null;
t.direction ();
}
// chrome
} else if(e.wheelDelta) {
if (e.wheelDelta > 0) {
t._turnOf = 'forward';
t.direction ();
} else{
t._turnOf = null;
t.direction ();
}
}
},
//判断是否长点击
longPress: function(e, moveFun ) {
var t = this;
if ( u.match(/\b(Windows\sNT|Macintosh)\b/) ) {
e = e || window.event;
// 限制为鼠标左键点击才触发
if (/^mouse/.test(e.type) && e.which !== 1) {
return;
}
}
t._setintervalId = setInterval(function(){
t._mousedownTimer += 10;
if( t._mousedownTimer >= 100 ){
moveFun();
}
},20);
},
mouseupFun: function(e, moveFun) {
var t = this;
if ( u.match(/\b(Windows\sNT|Macintosh)\b/) ) {
e = e || window.event;
// 限制为鼠标左键点击才触发
if (/^mouse/.test(e.type) && e.which !== 1) {
return;
}
}
clearTimeout(t._setintervalId);
if( t._mousedownTimer < 100 ) {
moveFun();
}
t._mousedownTimer = 0;
},
direction:function() {
var t = simulation,
barTop = t._scroll.position().top,
pageTop = t._wapper.position().top,
moveDistance = {};
if ( t._turnOf === 'forward') {
//页面到顶,不执行任何操作
if (barTop == 0) {
return;
}
moveDistance = {
page: pageTop + t._distance,
bar: barTop - parameter.scrollDistance
}
//如果滚动条距离顶部的距离少 < 每次滚动的距离,或者已经滚动到顶部,则不再滚动
if(barTop < parameter.scrollDistance || barTop <= 0){
moveDistance = {
page: 0,
bar: 0
}
}
} else {
//页面到底,不执行任何操作
if (barTop == parameter.barArea - parameter.scrollHeight){
return;
}
moveDistance = {
page: pageTop - t._distance,
bar: barTop + parameter.scrollDistance
};
// 如果滚动条距离底部的距离值 < 每次滚动的距离 或者已经到底部,则一次滚到底
if ( moveDistance.bar + parameter.scrollHeight >= parameter.barArea) {
moveDistance = {
page: parameter.view - parameter.page,
bar: parameter.barArea - parameter.scrollHeight
};
}
}
t._scroll.css({top: moveDistance.bar});
t._wapper.css({top: moveDistance.page});
},
//拖动滚动条
liveEvent: function() {
var t = this,
draging = false,
currentY = 0,
lastY = 0,
pageY = 0;
//检测设备类型
var _ua = function(e) {
var Pos = null;
if ( u.match(/\b(Windows\sNT|Macintosh)\b/) ) {
e = e || window.event;
// 限制为鼠标左键点击才触发
if (/^mouse/.test(e.type) && e.which !== 1) {
return;
}
Pos = {
left : e.pageX,
top: e.pageY
}
} else {
Pos = {
left : e.originalEvent.targetTouches[0].pageX,
top: e.originalEvent.targetTouches[0].pageY
}
}
return Pos;
};
var _start = function(e) {
//监控鼠标
e.preventDefault();
if (t._scroll.get(0).setCapture) {
t._scroll.get(0).setCapture();
}
draging = true;
//记录当前滚动条的坐标
lastY = t._scroll.position().top;
//记录按下鼠标的坐标
pageY = _ua(e).top;
};
var _drag = function(e) {
if( draging ) {
var pageTop = t._wapper.position().top;
var barTop = t._scroll.position().top;
//滚动条每移动1px,页面相对滚动Npx 再 * 当前滚动条的到顶部的距离
var pageMoveDistance = -(parameter.page / (parameter.barArea / 1)) * barTop;
if (lastY + ( _ua(e).top - pageY ) < 0) {
currentY = 0;
pageMoveDistance = 0;
} else if( lastY + ( _ua(e).top - pageY) + parameter.scrollHeight >= parameter.barArea) {
currentY = parameter.barArea - parameter.scrollHeight;
pageMoveDistance = parameter.view - parameter.page;
}
else {
currentY = lastY + ( _ua(e).top - pageY);
}
t._scroll.css({ top:currentY});
t._wapper.css({top: pageMoveDistance});
}
};
var _end = function(e) {
if (draging) {
draging = false;
//在IE下释放对鼠标的控制
if (t._scroll.get(0).setCapture) {
t._scroll.get(0).releaseCapture();
}
document.onmousemove = null;
document.onmouseup = null;
}
};
t._scroll.bind( startWhen, _start );
t._wapper.bind( startWhen, _start );
$(document).bind( moveWhen, _drag );
$(document).bind( endWhen, _end );
$(document).bind('blur', _end);
}
}
return simulation;
});
以上即为 JavaScript 模拟滚动条的实现代码,希望能为相关学习提供参考。