CSS & JS Effect – sticky horizontal scrollbar
需求

这个是 Google Ads 里的 table。
那个 horizontal scrollbar 可以 sticky bottom。
我们知道 scrollbar 是游览器原生的,我们能做的 styling 少之又少,挺多只能调 size, color 而已。要让它 sticky bottom 根本不可能。
实现思路
首先要弄一个假的 scrollbar 出来。
怎么理解,怎么弄?
做一个 div1 > div2
div1 设置 max-width, overflow auto
div2 设置 width。
这样一个 horizontal scrollbar 就出来的。
这个 div 里只有一个 scrollbar 没有其它内容,所以它看上去就是一个 scrollbar。
而 div 是可以 sticky bottom 的 (当然上面这个例子,使用的不是 CSS 原生的 sticky 功能,而是模拟的)。
这样我们就有了一个可以 sticky bottom 的 scrollbar。
接着我们把原生的 horizontal scrollbar hide 起来,这样看上去就 ok 了。
看是没有问题了,但是交互还需要搞一搞。
监听假 scrollbar 同步 scrollLeft 给 container,反过来也需要,监听 container scroll 同步 scrollLeft 给假 scrollbar。
这样就大功告成了。
破坏性
但凡 “假的 / 模拟的” 都是旁门左道,一定会引起一些 bug 之类的。所以一定要控制好范围,避免失控。
下图是我们常见的盒子 container

offsetHeight 的计算是 border to border (border + scrollbar + padding + content)
clientHeight 的计算是 padding to padding (padding + content, 没有 border 和 scrollbar)
问题来了,我们的假 scrollbar 要放在 container 里面还是外面?
如果放在里面,那么它会在 padding 之上 (因为它算是内容丫),而不是取代 native scrollbar 的位置 (padding 之下)。
于是我们需要 remove container 的 padding-bottom 然后在假 scrollbar 补回 padding-bottom。
即便看上去没问题,但是 clientHeight 的计算肯定就错了,因为假 scrollbar 被当成了内容,而 clientHeight 是不应该计算 scrollbar height 的。
把假 scrollbar 放到 container 外面也有类似的问题,我们需要 remove container 的 border-bottom,然后在假 scrollbar 补回 border-bottom。
这回 clientHeight 对了,但是 offsetHeight 计算却错了,少算了一个 border-bottom。
所以不管放哪一边总会影响到某些地方,这就是旁门左道的代价。
下面例子我选择放外面,因为放里面还需要用上 sticky left 会更麻烦。
Step by Step
搭环境
index.html
<div class="vertical-container">
<div class="horizontal-container">
<div class="my-content"></div>
</div>
</div>
index.scss
.vertical-container {
width: max-content;
margin-inline: auto;
max-height: 512px;
overflow-y: auto;
}
.horizontal-container {
max-width: 768px;
overflow: auto;
&.hide-scrollbar {
&::-webkit-scrollbar {
height: 0;
}
scrollbar-width: none;
}
}
.my-content {
width: 500px;
height: 10px;
background-color: pink;
}
注意那个 class hide-scrollbar
由于 JS 无法 querySelector 伪元素,所以 hide scrollbar 只能让 CSS 负责了。
index.ts
首先 query container
const container = document.querySelector<HTMLElement>('.horizontal-container')!;
然后做一些 first time setup
// 创捷 scrollbar
const scrollbar = document.createElement('div');
// 创建 scrollbar content
const scrollbarContent = document.createElement('div');
// 把 scrollbar content 插入到 scrollbar
scrollbar.appendChild(scrollbarContent);
// 把 scrollbar 插入到 container next sibling
container.parentElement!.insertBefore(scrollbar, container.nextElementSibling); // 设置 scrollbar 一些 style
scrollbar.style.overflowX = 'auto';
scrollbar.style.overflowY = 'hidden'; // 监听 scroll 同步 container 和 scrollbar 的 scrollLeft
scrollbar.addEventListener('scroll', () => {
container.scrollLeft = scrollbar.scrollLeft;
});
container.addEventListener('scroll', () => {
scrollbar.scrollLeft = container.scrollLeft;
});
因为我们需要监听 container resize,所以特别区分 first time setup。
接着,封装一个 getContainerInfo 函数
// container info 接口
interface ContainerInfo {
clientWidth: number;
scrollWidth: number;
hasScrollbar: boolean;
scrollbarHeight: number;
} // 记入最后一次的 scrollbar height
let lastScrollbarHeight = 0;
function getContainerInfo(container: HTMLElement): ContainerInfo {
// getElementSize 是一个方便拿 element size 的功能,把它当作是 getComputedStyle 就可以了
const containerSize = getElementSize(container);
const containerClientWidth = containerSize.client.width;
const containerScrollWidth = containerSize.scroll.size.width;
// 判断有没有 scrollbar 出现
const hasScrollbar = containerClientWidth !== containerScrollWidth;
// 计算 native scrollbar 的 height
let scrollbarHeight = containerSize.offset.height - containerSize.border.block - containerSize.client.height; // 因为我们会 hide native scrollbar,
// 所以第一次可以拿到 scrollbar height 但是第二次可能就拿不到了
// 所以我们需要把 scrollbar height 存起来
if (hasScrollbar && scrollbarHeight !== 0) {
lastScrollbarHeight = scrollbarHeight;
} // 第二次拿不到 scrollbar height 的时候,我们拿存起来的来用
if (hasScrollbar && scrollbarHeight === 0) {
scrollbarHeight = lastScrollbarHeight;
// Firefox 是永远拿不到 scrollbar height 的,给它一个默认 12 就好。
if (scrollbarHeight === 0) scrollbarHeight = 12;
} return {
clientWidth: containerClientWidth,
scrollWidth: containerScrollWidth,
hasScrollbar,
scrollbarHeight,
};
}
在 first setup 之前 getContainerInfo

必须提前读取 container information,如果在 first setup 后才读取会导致游览器立刻 repaint / reflow。
接着,封装一个 updateSize 函数
function updateSize(
container: HTMLElement,
scrollbar: HTMLElement,
scrollbarContent: HTMLElement,
containerInfo: ContainerInfo,
) {
const { clientWidth, scrollWidth, hasScrollbar, scrollbarHeight } = containerInfo;
// 如果需求 scrollbar 那就 hide native scrollbar
container.classList[hasScrollbar ? 'add' : 'remove']('hide-scrollbar');
// 如果不需要 scrollbar 就 display none 假 scrollbar
if (!hasScrollbar) scrollbar.style.display = 'none'; if (hasScrollbar) {
// 如果需要 scrollbar 就 update scrollbar 和 scrollbar content 的 size
scrollbar.style.removeProperty('display');
scrollbar.style.maxWidth = `${clientWidth}px`;
scrollbar.style.maxHeight = `${scrollbarHeight}px`;
scrollbarContent.style.height = `${scrollbarHeight}px`;
scrollbarContent.style.width = `${scrollWidth}px`;
}
}
在 first setup 之后调用 updateSize for firstload

做一个 resize 监听
// StgResizeObserver 是一个基于 RxJS 的 ResizeObserver
// 把它当作 native 的 ResizeObserver 看待就可以了
const ro = new StgResizeObserver();
// 注意监听的是 container 的所有 child elements
// 因为 container 已经 overflow 了,它是不会 resize 的, resize 的是它的 children
merge(...Array.from(container.children).map(el => ro.observe(el))).subscribe(() => {
// 每当 resize 就重新 getContainerInfo + updateSize
const containerInfo = getContainerInfo(container);
updateSize(container, scrollbar, scrollbarContent, containerInfo);
});
这样就大功告成了。
提醒:container 不支持放 border 哦,因为我选择的是把假 scrollbar 放到 container 之外,如果是放在 container 里面的话就支持 border 但不支持 padding,同时需要 sticky left 比较麻烦。有兴趣的可以自己玩一玩。
至于如何让假 scrollbar sticky bottom,请参考:CSS & JS Effect – Simulation Position Sticky (用 JavaScript 实现 position sticky)
CSS & JS Effect – sticky horizontal scrollbar的更多相关文章
- 前端工程师面试问题归纳(一、问答类html/css/js基础)
一.参考资源 1.前端面试题及答案整理(一) 2.2017年前端面试题整理汇总100题 3.2018最新Web前端经典面试试题及答案 4.[javascript常见面试题]常见前端面试题及答案 5.W ...
- CSS & JS 制作滚动幻灯片
==================纯CSS方式==================== <!DOCTYPE html> <html> <head> <met ...
- 【转】Maven Jetty 插件的问题(css/js等目录死锁)的解决
Maven Jetty 插件的问题(css/js等目录死锁,不能自动刷新)的解决: 1. 打开下面的目录:C:\Users\用户名\.m2\repository\org\eclipse\jetty ...
- Css Js Loader For Zencart
Css Js Loader 描述:这个插件很早就出来了,可能知道人非常少 这个插件的功能是整合所有的网站的CSS和JS内容到一个文件里边. 因为CSS和JS文件到了一个文件,加快了程序的运行 在配合其 ...
- 购物车数字加减按钮HTML+CSS+JS(有需要嫌麻烦的小伙伴拿走不谢)
之前在写详情页的时候,如下图 因为自己嫌麻烦,就去看其他网站是怎么写的,想直接拿来用,后来看来看去觉得写得很麻烦,于是最后还是决定自己写,附上HTML+CSS+JS代码,一条龙一站式贴心服务2333 ...
- vs合并压缩css,js插件——Bundler & Minifier
之前做了一个大转盘的抽奖活动,因为比较火,部分用户反馈看不到页面的情况,我怀疑js加载请求过慢导致,所以今天针对之前的一个页面进行调试优化. 首先想到的是对页面的js和css进行压缩优化,百度了下vs ...
- nginx资源定向 css js路径问题
今天玩玩项目,学学nginx发现还不错,速度还可以,但是CSS JS确无法使用,原来Iginx配置时需要对不同类型的文件配置规则,真是很郁闷,不过想想也还是很有道理.闲暇之际,把配置贴上来.#user ...
- IIS7的集成模式下如何让自定义的HttpModule不处理静态文件(.html .css .js .jpeg等)请求
今天将开发好的ASP.NET站点部署到客户的服务器上后,发现了一个非常头疼的问题,那么就是IIS7的应用程序池是集成模式的话,ASP.NET项目中自定义的HttpModule会处理静态文件(.html ...
- 网站加载css/js/img等静态文件失败
网站加载css/js/img等静态文件失败,报网站http服务器内部500错误.而服务器中静态文件存在且权限正常. 从浏览器中直接访问文件,出来乱码.这种问题原因在于iis中该网站mime配置报错,不 ...
- 【前端】Sublime text3 插件HTML/CSS/JS prettify 格式化代码
1.首先安装插件 菜单的preference->packages control,然后输入install .. 回车,再输入HTML/CSS/JS prettify 再回车,重启后就可以了. 2 ...
随机推荐
- 全网最适合入门的面向对象编程教程:15 类和对象的 Python 实现-__slots__魔法方法
全网最适合入门的面向对象编程教程:15 类和对象的 Python 实现-__slots__魔法方法 摘要: 本文主要介绍了 Python 中创建自定义类时不同实例属性保存的基本原理和缺点,介绍了__s ...
- 全网最适合入门的面向对象编程教程:03 类和对象的Python实现-为自定义类添加属性
摘要: 本文主要介绍了,当使用 Python 创建自定义类时,如何为其添加属性,包括为类和实例添加属性两种,以及如何获取自定义的属性等内容. 往期推荐: 学嵌入式的你,还不会面向对象??! 全网最适合 ...
- Vue this.$refs的使用
ref 写在标签上时:this.$refs.名字 获取的是标签对应的dom元素 ref 写在组件上时:这时候获取到的是子组件的引用
- Django 结合Vue实现前端页面导出为PDF
Django结合Vue实现前端页面导出为PDF by:授客 QQ:1033553122 测试环境 Win 10 Python 3.5.4 Django-2.0.13.tar.gz 官方下载地址: ht ...
- P10245 Swimming Pool题解
P10245 Swimming Pool 题意 给你四条边 \(abcd\),求这四条边是否可以组成梯形. 思路 这显然是一道简单的普通数学题. 判断是否能构成梯形只需看四条边是否能满足,上底减下底的 ...
- 《HelloGitHub》第 100 期
兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. github.com/521xueweihan/HelloG ...
- Scratch植物大战僵尸全套素材包免费下载
scratch植物大战僵尸全套素材包,包含227个丰富多样的素材,涵盖角色.背景.动态gif.为Scratch创作者提供丰富资源,助力创作精彩作品. 免费下载地址:www.xiaohujing.com ...
- STM32F103 SPI详解及示例代码
1 SPI协议详解 SPI是串行外设接口(Serial Peripheral Interface)的缩写,是美国摩托罗拉公司(Motorola)最先推出的一种同步串行传输规范,也是一种单片机外设芯片串 ...
- EdgeOne安全专项实践:上传文件漏洞攻击详解与防范措施
前言 今天,我们将深入探讨上传文件漏洞攻击,这部分内容是EdgeOne专项实践篇的一部分.在本章中,我们不会涉及文件漏洞的含义.原理或站点配置等基础教程,如果你对这些内容感兴趣,可以参考这篇文章:探索 ...
- 【Java,IDEA】配置文件快速生成
比如这里的druid连接配置文件,和mybatis的mapper配置文件就是使用模版创建好的 在创建文件时会有选项选择: