// get the sticky element
const stickyElm = document.querySelector('header')
const observer = new IntersectionObserver(
([e]) => e.target.classList.toggle('isSticky', e.intersectionRatio < 1),
{threshold: [1]}
);
observer.observe(stickyElm)
body{ height: 200vh; font:20px Arial; }
section{
background: lightblue;
padding: 2em 1em;
}
header{
position: sticky;
top: -1px; /* ➜ the trick */
padding: 1em;
padding-top: calc(1em + 1px); /* ➜ compensate for the trick */
background: salmon;
transition: .1s;
}
/* styles for when the header is in sticky mode */
header.isSticky{
font-size: .8em;
opacity: .5;
}
<section>Space</section>
<header>Sticky Header</header>
top
值需为-1px
,否则元素将永远不会与浏览器窗口顶部相交(从而永远不会触发intersection observer)。
为了抵消这1px
的隐藏内容,应该在粘性元素的边框或填充中添加额外的1px
空间。
或者,如果您希望保持CSS不变(top:0
),则可以通过在intersection observer级别上添加设置rootMargin:'-1px 0px 0px 0px'
来应用“校正”(如@mattrick在他的答案中所示)。
scroll
事件监听器的演示:scrollCallback
(以便在需要时解除绑定)// get the sticky element
const stickyElm = document.querySelector('header');
// get the first parent element which is scrollable
const stickyElmScrollableParent = getScrollParent(stickyElm);
// save the original offsetTop. when this changes, it means stickiness has begun.
stickyElm._originalOffsetTop = stickyElm.offsetTop;
// compare previous scrollTop to current one
const detectStickiness = (elm, cb) => () => cb & cb(elm.offsetTop != elm._originalOffsetTop)
// Act if sticky or not
const onSticky = isSticky => {
console.clear()
console.log(isSticky)
stickyElm.classList.toggle('isSticky', isSticky)
}
// bind a scroll event listener on the scrollable parent (whatever it is)
// in this exmaple I am throttling the "scroll" event for performance reasons.
// I also use functional composition to diffrentiate between the detection function and
// the function which acts uppon the detected information (stickiness)
const scrollCallback = throttle(detectStickiness(stickyElm, onSticky), 100)
stickyElmScrollableParent.addEventListener('scroll', scrollCallback)
// OPTIONAL CODE BELOW ///////////////////
// find-first-scrollable-parent
// Credit: https://dev59.com/iFsV5IYBdhLWcg3wrAZJ#42543908
function getScrollParent(element, includeHidden) {
var style = getComputedStyle(element),
excludeStaticParent = style.position === "absolute",
overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
if (style.position !== "fixed")
for (var parent = element; (parent = parent.parentElement); ){
style = getComputedStyle(parent);
if (excludeStaticParent && style.position === "static")
continue;
if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX))
return parent;
}
return window
}
// Throttle
// Credit: https://jsfiddle.net/jonathansampson/m7G64
function throttle (callback, limit) {
var wait = false; // Initially, we're not waiting
return function () { // We return a throttled function
if (!wait) { // If we're not waiting
callback.call(); // Execute users function
wait = true; // Prevent future invocations
setTimeout(function () { // After a period of time
wait = false; // And allow future invocations
}, limit);
}
}
}
header{
position: sticky;
top: 0;
/* not important styles */
background: salmon;
padding: 1em;
transition: .1s;
}
header.isSticky{
/* styles for when the header is in sticky mode */
font-size: .8em;
opacity: .5;
}
/* not important styles*/
body{ height: 200vh; font:20px Arial; }
section{
background: lightblue;
padding: 2em 1em;
}
<section>Space</section>
<header>Sticky Header</header>
top: 0
,那么它将永远不会与浏览器窗口的顶部相交。设置 top: -1px
将允许与顶部有 1px
的重叠(尽管内容中的 1px
将不可见,因此需要进行补偿)。聪明。 - Daniel Tononborder-top: 1px solid transparent
来避免使用calc函数。 - Daniel Tonone.target.classList.toggle('isSticky', e.boundingClientRect.top < 0)
- Flowgram我发现一种与@vsync的答案有些类似的解决方案,但它不需要您在样式表中添加的“hack”。您只需更改IntersectionObserver的边界就可以避免移动元素本身到视口之外:
const observer = new IntersectionObserver(callback, {
rootMargin: '-1px 0px 0px 0px',
threshold: [1],
});
observer.observe(element);
如果有人通过Google搜索到这里,他们自己的工程师使用IntersectionObserver、自定义事件和 Sentinel 提供了一个解决方案:
https://developers.google.com/web/updates/2017/09/sticky-headers
:stuck
选择器改变了CSS使其不再粘性。你会进入一个无限循环。CSS被精心设计以避免这种情况发生。 - Perry:hover
规则重置元素的大小,导致 :hover
不再适用,然后再次重置大小,再次应用 :hover
,如此反复。:stuck
不会使任何事情变得更糟。 - Armen Michaeliapply_stickies()
window.addEventListener('scroll', function() {
apply_stickies()
})
function apply_stickies() {
var _$stickies = [].slice.call(document.querySelectorAll('.sticky'))
_$stickies.forEach(function(_$sticky) {
if (CSS.supports && CSS.supports('position', 'sticky')) {
apply_sticky_class(_$sticky)
}
})
}
function apply_sticky_class(_$sticky) {
var currentOffset = _$sticky.getBoundingClientRect().top
var stickyOffset = parseInt(getComputedStyle(_$sticky).top.replace('px', ''))
var isStuck = currentOffset <= stickyOffset
_$sticky.classList.toggle('js-is-sticky', isStuck)
}
position: sticky
。 - Daniel Tonon目前没有原生解决方案。请参见定位当前处于“粘滞”状态的position:sticky元素。但是我有一个CoffeeScript解决方案,可以与原生的position: sticky
和实现粘性行为的polyfill一起使用。
将“sticky”类添加到您想要粘滞的元素中:
.sticky {
position: -webkit-sticky;
position: -moz-sticky;
position: -ms-sticky;
position: -o-sticky;
position: sticky;
top: 0px;
z-index: 1;
}
$ -> new StickyMonitor
class StickyMonitor
SCROLL_ACTION_DELAY: 50
constructor: ->
$(window).scroll @scroll_handler if $('.sticky').length > 0
scroll_handler: =>
@scroll_timer ||= setTimeout(@scroll_handler_throttled, @SCROLL_ACTION_DELAY)
scroll_handler_throttled: =>
@scroll_timer = null
@toggle_stuck_state_for_sticky_elements()
toggle_stuck_state_for_sticky_elements: =>
$('.sticky').each ->
$(this).toggleClass('stuck', this.getBoundingClientRect().top - parseInt($(this).css('top')) <= 1)
注意:此代码仅适用于垂直黏性位置。
offsetTop
的解决方案。 - BigglesZX我知道这个问题提出已经有一段时间了,但我找到了一个很好的解决方案。插件stickybits在支持时使用position: sticky
,并在元素被“粘住”的时候应用一个类。我最近使用它取得了不错的结果,并且在撰写本文时,该插件正在积极开发中(对我来说是一个 plus):)
position: sticky
(ie11,edge)的浏览器中进行测试时,才会出现这些问题。 - DaveygetComputedStyle()
获取元素的所需粘性top
位置getBoundingClientRect()
获取元素的当前视口偏移量classList.toggle()
添加类const el = document.querySelector(".my-sticky-element");
window.addEventListener("scroll", () => {
const stickyTop = parseInt(window.getComputedStyle(el).top);
const currentTop = el.getBoundingClientRect().top;
el.classList.toggle("is-sticky", currentTop === stickyTop);
});
我在我的主题中使用这个片段来添加.is-stuck
类到.site-header
当它处于固定位置时:
// noinspection JSUnusedLocalSymbols
(function (document, window, undefined) {
let windowScroll;
/**
*
* @param element {HTMLElement|Window|Document}
* @param event {string}
* @param listener {function}
* @returns {HTMLElement|Window|Document}
*/
function addListener(element, event, listener) {
if (element.addEventListener) {
element.addEventListener(event, listener);
} else {
// noinspection JSUnresolvedVariable
if (element.attachEvent) {
element.attachEvent('on' + event, listener);
} else {
console.log('Failed to attach event.');
}
}
return element;
}
/**
* Checks if the element is in a sticky position.
*
* @param element {HTMLElement}
* @returns {boolean}
*/
function isSticky(element) {
if ('sticky' !== getComputedStyle(element).position) {
return false;
}
return (1 >= (element.getBoundingClientRect().top - parseInt(getComputedStyle(element).top)));
}
/**
* Toggles is-stuck class if the element is in sticky position.
*
* @param element {HTMLElement}
* @returns {HTMLElement}
*/
function toggleSticky(element) {
if (isSticky(element)) {
element.classList.add('is-stuck');
} else {
element.classList.remove('is-stuck');
}
return element;
}
/**
* Toggles stuck state for sticky header.
*/
function toggleStickyHeader() {
toggleSticky(document.querySelector('.site-header'));
}
/**
* Listen to window scroll.
*/
addListener(window, 'scroll', function () {
clearTimeout(windowScroll);
windowScroll = setTimeout(toggleStickyHeader, 50);
});
/**
* Check if the header is not stuck already.
*/
toggleStickyHeader();
})(document, window);
只需使用原生JS。您还可以使用lodash的节流函数来预防一些性能问题。
const element = document.getElementById("element-id");
document.addEventListener(
"scroll",
_.throttle(e => {
element.classList.toggle(
"is-sticky",
element.offsetTop <= window.scrollY
);
}, 500)
);
position: sticky
是Chrome实现的,所以我正在寻找一种可用的方法! - AlecRust