The Instagram app has a nice sticky header that pushes the current one up in place of the new one. I found a great tutorial on how to do this natively for Android, but I\'m
I tried solutions provided above but none of them working perfectly without bug (mostly bug on scrolling and browser compatibility). Just found this one https://github.com/sarahdayan/feedify and it's working good for me.
The solution @chris-spittles provided helped me out a lot but I want to also provide a vanilla Javascript solution. Additionally, I've added functionality that handles the last sticky for situations where you don't want it to continue to be fixed past it's content area.
https://codepen.io/jamigibbs/pen/QZWjBy
const StickySubsections = (function(){
let el
return {
elements: function () {
return {
stickies: [...document.querySelectorAll('.followMeBar')]
}
},
init: function () {
el = this.elements()
this.load()
},
load: function () {
this.setupStickyWrap()
window.addEventListener('scroll', () => this.whenScrolling())
},
setupStickyWrap: function(){
el.stickies.forEach((sticky, i) => {
const stickyWrap = this.addWrap(sticky, 'sticky-wrap')
const heightToTop = sticky.getBoundingClientRect().top + window.scrollY
const outerHeight = sticky.offsetHeight
stickyWrap.parentElement.id = `sticky-content-${i}`
sticky.setAttribute('data-originalPosition', heightToTop)
sticky.setAttribute('data-originalHeight', outerHeight)
stickyWrap.style.height = outerHeight + 'px'
})
},
addWrap: function(el, className, wrap = 'div') {
const wrapper = document.createElement(wrap)
wrapper.classList.add(className)
el.parentNode.insertBefore(wrapper, el)
wrapper.appendChild(el)
return wrapper
},
elementExists: function(el){
return typeof(el) != 'undefined' && el != null
},
stickyPosition: function(el){
return el.getAttribute('data-originalPosition')
},
nextStickyPosition: function(current, next){
return next.getAttribute('data-originalPosition') - current.getAttribute('data-originalHeight')
},
scrollingPositionTop: function(el){
return el.getBoundingClientRect().top + window.scrollY
},
offsetTop: function(el){
return el.getBoundingClientRect().top
},
scrollingPositionBottom: function(el){
return el.getBoundingClientRect().bottom + window.scrollY
},
headerPosition: function(){
return window.scrollY
},
bottomSectionHit: function(contentElement, sticky){
const contentSection = document.getElementById(contentElement)
const sectionBottom = contentSection.getBoundingClientRect().bottom + window.scrollY
const stickyPositionScrolling = sticky.getBoundingClientRect().bottom + window.scrollY
return stickyPositionScrolling >= sectionBottom
},
whenScrolling: function() {
el.stickies.forEach((sticky, i) => {
const nextSticky = el.stickies[i + 1]
const prevSticky = el.stickies[i - 1]
if (this.stickyPosition(sticky) <= this.headerPosition()) {
sticky.classList.add('fixed')
if (this.elementExists(nextSticky)) {
while (this.scrollingPositionBottom(sticky) >= this.nextStickyPosition(sticky, nextSticky) + 50) {
sticky.classList.add('absolute')
sticky.style.top = this.nextStickyPosition(sticky, nextSticky)
}
// Handle last sticky element
} else {
if (this.bottomSectionHit(`sticky-content-${i}`, sticky)) {
sticky.classList.remove('fixed')
}
}
} else {
sticky.classList.remove('fixed')
if (this.elementExists(prevSticky) && window.scrollY <= this.stickyPosition(sticky)){
prevSticky.classList.remove('absolute')
prevSticky.removeAttribute('style')
}
}
})
}
}
}())
StickySubsections.init()
First off, thanks to @Chris Spittles for his excellent answer.
I've created a modified version that removes the need to wrap each sticky element, as it simply changes their relative position instead of using fixed positioning.
var stickyHeaders = (function() {
var $stickies;
var load = function(stickies, target) {
if (typeof stickies === "object" && stickies instanceof jQuery && stickies.length > 0) {
$stickies = stickies.each(function() {
var $thisSticky = $(this);
$thisSticky
.data('originalPosition', $thisSticky.offset().top)
.data('originalHeight', $thisSticky.outerHeight());
});
target.off("scroll.stickies").on("scroll.stickies", function(event) {
_whenScrolling(event);
});
}
};
var _whenScrolling = function(event) {
var $scrollTop = $(event.currentTarget).scrollTop();
$stickies.each(function(i) {
var $thisSticky = $(this),
$stickyPosition = $thisSticky.data('originalPosition'),
$newPosition,
$nextSticky;
if ($stickyPosition <= $scrollTop) {
$newPosition = Math.max(0, $scrollTop - $stickyPosition);
$nextSticky = $stickies.eq(i + 1);
if($nextSticky.length > 0) {
$newPosition = Math.min($newPosition, ($nextSticky.data('originalPosition') - $stickyPosition) - $thisSticky.data('originalHeight'));
}
} else {
$newPosition = 0;
}
$thisSticky.css('transform', 'translateY(' + $newPosition + 'px)');
//could just as easily use top instead of transform
//$thisSticky.css('top', $newPosition + 'px');
});
};
return {
load: load
};
})();
$(function() {
stickyHeaders.load($(".followMeBar"), $(window));
});
The css is simplified to:
.followMeBar {
background: #999;
padding: 10px 20px;
position: relative;
z-index: 1;
color: #fff;
}
/* For aesthetics only */
body {
margin: 0;
font-family: Segoe, "Segoe UI", "DejaVu Sans", "Trebuchet MS", Verdana, sans-serif;
}
http://plnkr.co/edit/wk3h40LfBdN1UFtDLZgY?p=preview
And here's another example showing how you can have an offset when using a fixed header:
http://plnkr.co/edit/8YBqdCIKruVKYRXbZnCp?p=preview
There's not a quick or easy answer to this but with a bit of creative cajoling we can emulate the same functionality.
What we need is a series of elements we can identify, loop over and then set up so that when we hit their position on the page the previous item is pushed up and the new item becomes fixed. We will need to retrieve the element's initial position using jQuery's offset().top
method and store it in a data
tag so we can reference it later. Then the rest will be calculated as we scroll.
This should do the trick:
var stickyHeaders = (function() {
var $window = $(window),
$stickies;
var load = function(stickies) {
if (typeof stickies === "object" && stickies instanceof jQuery && stickies.length > 0) {
$stickies = stickies.each(function() {
var $thisSticky = $(this).wrap('<div class="followWrap" />');
$thisSticky
.data('originalPosition', $thisSticky.offset().top)
.data('originalHeight', $thisSticky.outerHeight())
.parent()
.height($thisSticky.outerHeight());
});
$window.off("scroll.stickies").on("scroll.stickies", function() {
_whenScrolling();
});
}
};
var _whenScrolling = function() {
$stickies.each(function(i) {
var $thisSticky = $(this),
$stickyPosition = $thisSticky.data('originalPosition');
if ($stickyPosition <= $window.scrollTop()) {
var $nextSticky = $stickies.eq(i + 1),
$nextStickyPosition = $nextSticky.data('originalPosition') - $thisSticky.data('originalHeight');
$thisSticky.addClass("fixed");
if ($nextSticky.length > 0 && $thisSticky.offset().top >= $nextStickyPosition) {
$thisSticky.addClass("absolute").css("top", $nextStickyPosition);
}
} else {
var $prevSticky = $stickies.eq(i - 1);
$thisSticky.removeClass("fixed");
if ($prevSticky.length > 0 && $window.scrollTop() <= $thisSticky.data('originalPosition') - $thisSticky.data('originalHeight')) {
$prevSticky.removeClass("absolute").removeAttr("style");
}
}
});
};
return {
load: load
};
})();
$(function() {
stickyHeaders.load($(".followMeBar"));
});
.followMeBar {
background: #999;
padding: 10px 20px;
position: relative;
z-index: 1;
color: #fff;
}
.followMeBar.fixed {
position: fixed;
top: 0;
width: 100%;
box-sizing: border-box;
z-index: 0;
}
.followMeBar.fixed.absolute {
position: absolute;
}
/* For aesthetics only */
body {
margin: 0;
font-family: Segoe, "Segoe UI", "DejaVu Sans", "Trebuchet MS", Verdana, sans-serif;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="followMeBar">A</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">B</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">C</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">D</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">E</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">F</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">G</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">H</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">I</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">J</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">K</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">L</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">M</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">N</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">O</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">P</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">Q</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">R</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">S</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">T</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">U</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">V</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">W</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">X</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">Y</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<div class="followMeBar">Z</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
Before you say "What?! I just went through all of that when there's a CSS only version?!" It only works in a couple of browsers. Try this in firefox for example:
.sticky {
position: -webkit-sticky;
position: -moz-sticky;
position: -o-sticky;
position: -ms-sticky;
position: sticky;
top: 0;
left: 0;
right: 0;
display: block;
z-index: 1;
background: #999;
color: #fff;
padding: 10px 20px;
}
/* For aesthetics only */
body {
margin: 0;
font-family: Segoe, "Segoe UI", "DejaVu Sans", "Trebuchet MS", Verdana, sans-serif;
}
<div data-lorem="p">
<span class="sticky">a</span>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
</div>
<div data-lorem="p">
<span class="sticky">b</span>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
</div>
<div data-lorem="p">
<span class="sticky">c</span>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
</div>
<div data-lorem="p">
<span class="sticky">d</span>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
</div>
<div data-lorem="p">
<span class="sticky">e</span>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
</div>
<div data-lorem="p">
<span class="sticky">f</span>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
</div>
<div data-lorem="p">
<span class="sticky">g</span>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
</div>
<div data-lorem="p">
<span class="sticky">h</span>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
</div>
http://caniuse.com/#feat=css-sticky
This doesn't always work well if there's content above the div with the sticky headers:
$thisSticky.css('transform', 'translateY(' + $newPosition+ 'px)');
But this line does the trick
//could just as easily use top instead of transform
$thisSticky.css('top', $newPosition+ 'px');
I was having trouble getting Chris' answer to work accurately for me as all the stickies were inside a relatively positioned div (with a header above it all, outside of the relative div) - the answer to this is just storing the .offset().top of the relative container div in a var and subtracting it from the .css('top', value) in the script. As in chris' version, the top value is pushing off the top of the document which works fine. When you add in the relative div, top is now pushing from the top of that instead, so any space above the relative div is included in the value which is something you wouldn't want. Hope this helps someone else. James