Getting a sticky header to "push up", like in Instagram's iPhone app using CSS and jQuery
JavascriptHtmlCssInstagramStickyJavascript Problem Overview
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 looking to do it with JavaScript and CSS.
I was able to get my header to switch out for a new one, but I can't seem to find a way to mimic the way Instagram does it. Any help is greatly appreciated.
Edit: I was able to get the header to stick to the top of the page when scrolling using waypoints as Cj in the comments pointed out. (link to waypoints). The main issue I'm having is getting the "push up" effect that instagram uses in their mobile app for iPhone. I would link to an example but I've never seen it used before.
**Edit 2: Using parts of the codepen that @Chris provided I was able to get the headers to stick. I then added a .slideUp effect. My issue now is getting the .slideUp effect to only happen when the next header is reached. Right now the effect activates on scroll.
Here is the code:
(function() {
function stickyTitles(stickies) {
this.load = function() {
stickies.each(function(){
var thisSticky = jQuery(this);
jQuery.data(thisSticky[0], 'pos', thisSticky.offset().top);
});
}
this.scroll = function() {
stickies.each(function(){
var thisSticky = jQuery(this),
pos = jQuery.data(thisSticky[0], 'pos');
if (pos <= jQuery(window).scrollTop()) {
thisSticky.addClass("fixed");
// added this
$(".followMeBar:parent").slideUp();
} else {
thisSticky.removeClass("fixed");
}
});
}
}
jQuery(document).ready(function(){
var newStickies = new stickyTitles(jQuery(".followMeBar"));
newStickies.load();
jQuery(window).on("scroll", function() {
newStickies.scroll();
});
});
})();
Javascript Solutions
Solution 1 - Javascript
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>
#Here's the CSS only version: 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>
Solution 2 - Javascript
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:
Solution 3 - Javascript
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.
Solution 4 - Javascript
Make sure to wrap it in div
Notice the 6th heading as it is not wrapped in div
h1, h2{
position: sticky;
top: 0;
}
div{
height: 500px;
}
<div>
<h1>lorem ipsum doloro sit <br> amet 1</h1>
</div>
<div>
<h1>lorem ipsum doloro sit 2</h1>
</div>
<div>
<h2>lorem ipsum doloro sit 3</h2>
</div>
<div>
<h1>lorem ipsum doloro sit <br> amet 4</h1>
</div>
<div>
<h2>lorem ipsum doloro sit <br> amet 5</h2>
</div>
<h2>lorem ipsum doloro sit <br> amet 6</h2>
<div></div>
<div>
<h1>lorem ipsum doloro sit 7</h1>
</div>
<div>
<h1>lorem ipsum doloro sit <br> amet 8</h1>
</div>
<div>
<h1>lorem ipsum doloro sit <br> amet 9</h1>
</div>
Solution 5 - Javascript
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
Solution 6 - Javascript
A basic version of the solution:
CSS
.sectionWrapper{
background-color: #1E1E1E;
padding: 10px 20px;
color: #FF7B05;
width: 100%
height: 45px;
min-height: 45px;
margin-top: 30px;
position: relative;
z-index: 10;
}
.sectionHeader{
}
.sectionEmbed {
}
.sectionFixed {
background-color: #1E1E1E;
padding: 10px 20px;
color: #FF7B05;
width: 100%;
height: 45px;
position: fixed;
top: 0;
left: 0;
margin: 0;
}
HTML
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 sectionWrapper sectionEmbed">
<div class="sectionHeader">Headers here</div>
</div>
JQuery
//check doc is ready
$(function() {
$(window).on("scroll.sectionWrapper", function() {
stickSections();
});
});
//function stickSection
function stickSections()
{
//detach the listener
$(window).off("scroll.sectionWrapper");
$scrollPos = $(window).scrollTop();
$currentSticky = null;
$('.sectionWrapper').each(function(){
$(this).children().removeClass('sectionFixed');
if($scrollPos >= ($(this).offset().top - ($(this).height() - $(this).children().height() - 6)) ){
$currentSticky = $(this);
}
else $(this).children().addClass('sectionEmbed');
});
//apply the fixed class to our scrollPos match
if($currentSticky) $currentSticky.children().addClass('sectionFixed');
//reattach the listener
$(window).on("scroll.sectionWrapper", function() {
stickSections();
});
}
EXAMPLE
https://codepen.io/ZombiesByte/pen/EyYwVO
Have fun :)
Solution 7 - Javascript
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');
Solution 8 - Javascript
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()