Getting a sticky header to "push up", like in Instagram's iPhone app using CSS and jQuery

JavascriptHtmlCssInstagramSticky

Javascript 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>

http://caniuse.com/#feat=css-sticky

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:

http://plnkr.co/edit/8YBqdCIKruVKYRXbZnCp?p=preview

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()

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRyan RichView Question on Stackoverflow
Solution 1 - JavascriptChris SpittlesView Answer on Stackoverflow
Solution 2 - JavascriptjamesView Answer on Stackoverflow
Solution 3 - JavascriptwebchunView Answer on Stackoverflow
Solution 4 - JavascriptmmmchoView Answer on Stackoverflow
Solution 5 - Javascriptuser2668414View Answer on Stackoverflow
Solution 6 - JavascriptDalView Answer on Stackoverflow
Solution 7 - JavascriptspnkrView Answer on Stackoverflow
Solution 8 - Javascriptjami0821View Answer on Stackoverflow