CSS & JS Effect – Smooth scrollIntoView
Default scroll behaviour (jump)
先搭个场景
HTML
<body> <header style="height: 128px; background-color: lightgreen;"> <ul> <li><a href="/src/home/home.html#hero-section">hero</a></li> <li><a href="/src/home/home.html#product-section">product</a></li> <li><a href="/src/home/home.html#service-section">service</a></li> <li><a href="/src/home/home.html#review-section">review</a></li> </ul> </header> <main> <section id="hero-section" style="height: 30vh; background-color: pink; scroll-margin-top: 64px;"> hero </section> <section id="product-section" style="height: 30vh; background-color: lightblue; scroll-margin-top: 64px;"> product </section> <section id="service-section" style="height: 150vh; background-color: lightgray; scroll-margin-top: 64px;"> service </section> <section id="review-section" style="height: 30vh; background-color: lightcoral; scroll-margin-top: 64px;"> review </section> <section id="review-section" style="height: 330vh; background-color: lightcoral; scroll-margin-top: 64px;"> review 22 </section> </main> <footer style="height: 500vh; background-color: lightgreen;">footer</footer> <script type="module" src="./home.ts"></script> </body>
Styles

* { margin: 0; padding: 0; box-sizing: border-box; *:focus { outline: none; } } header, section, footer { display: flex; justify-content: center; align-items: flex-start; font-size: 24px; padding-top: 24px; } header ul { display: flex; gap: 48px; }
效果
by default,anchor + URL hash 会自动 scroll to target element,但是它是直接 jump scroll,而不是 smooth scroll。
Smooth scrollIntoView
我们使用 JavaScript DOM & BOM API 把 jump scroll 换成 smooth scroll。
const anchors = Array.from(document.querySelectorAll<HTMLAnchorElement>('a')); const currentPage = window.location.origin + window.location.pathname; for (const anchor of anchors) { const samePage = anchor.href.startsWith(currentPage); if (!samePage) continue; if (!anchor.href.includes('#')) continue; anchor.addEventListener('click', e => { const hash = new URL(anchor.href).hash; const elementToScroll = document.querySelector<HTMLElement>(hash); if (!elementToScroll) return; elementToScroll.scrollIntoView({ behavior: 'smooth' }); e.preventDefault(); window.history.pushState(null, '', anchor.href); }); }
最关键的一句是 scrollIntoView({ behavior: 'smooth' })。
behavior: 'smooth' 需要 IOS 16 才能支持
效果
scroll-margin-top & scrollIntoView center
by default anchor + URL hash scroll 或 scrollIntoView 都是 scroll to element 的头部
上面的 spacing 是 scroll-margin-top 造成的
如果没有加上这个 CSS Styles,那它会完全贴在最上面,不美观。
另外,我们也可以让它 scroll 去 element 的其它位置,比如 center, bottom
效果
有一点需要注意,不管 viewport 的高度是否比 element to scroll 大,只要我们指定去 center,那它就会 scroll to center,看上面 scroll to service section 的例子。
如果不喜欢这个体验,我们可以做一些小调整
const viewportHeight = window.innerHeight; const elementToScrollHeight = elementToScroll.offsetHeight; elementToScroll.scrollIntoView({ behavior: 'smooth', block: viewportHeight > elementToScrollHeight ? 'center' : 'start', });
先判断 viewport 和 element to scroll 的大小才决定 scroll to 'start' or 'center'
效果
nested scroll
<main style="height: 100vh; overflow: auto;">
效果
注:不管是用 scrollIntoView 或者 anchor + URL hash 都一样
两个 scrollbar 都会移动。
最上层的 scrollTop 会是 128px,也就是 scroll 到刚刚好碰到 main element,然后再继续 scroll main element。
冷知识 – 当 scroll-margin-top 遇上 overflow: hidden
平常我们会使用 scroll-margin-top 让 anchor + URL hash scroll to element 时上头留一点 spacing,这样会比较美观。
但假如 element to scroll 刚巧设有 overflow hidden,那 sroll-margin-top 就会失效。
原因我不清楚,但破解之法是把 hidden 换成 clip。
当然这不是万能药,clip 和 hidden 是有区别的,clip 不能 scroll 只能隐藏,而且需要 IOS 16 才能支持。
如果真的避不开,那可能需要使用 scrollTo 替代 scrollIntoView 来实现了。