How to Make Sticky (Persistent) Headers and Parked Headers using Javascript (JQuery) and CSS

The following is a Javascript (JQuery) solution for implementing floating (persistent, or sticky) headers of sections as the user scrolls below the header static position. Once the user scrolls past the containing block of the header the header is left parked at the bottom of the containing block so that it resticks to the top of the browser when the user scrolls back up.

What are Sticky (Persistent) Headers and Parked Headers

Let’s explain what I mean when I say sticky and parked headers.

A sticky or persistent header is just an html element that sticks to the top of the browser as you scroll below the header. Technically, you can make them stick to any position on the browser but the most common use I have seen is that of headers to the top of the browser (viewport).

Once the header reaches a certain position the header stops there and stays there till you scroll back up. That is called the parked header.

The following diagram should elucidate things.

Sticky and Parked Headers

The demo will look something like this:

Sticky Headers Demo

Check out the Sticky / Persistent Headers Demo.

The Code

HTML

The abstract structure is as follows:

<content-block class="section persist-area">
    <div class="section-header-wrapper">
        <header-element class="section-header/>
    </div>
    Content
</content-block>

You can choose any element for <content-block> and the <header-element>. I will use <div>s for both. Your Content can be text or other HTML elements. The following is what I used in the demo:

<div class="section persist-area">

    <div class="section-header-wrapper">
        <div class="section-header">Section Header 1</div>
    </div>
    <div class="section-content">
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eu nisi at tellus efficitur lobortis a suscipit elit. Nulla sem arcu, imperdiet id lorem ut, cursus commodo urna. Duis malesuada pulvinar est, non venenatis nisi euismod nec. Integer euismod commodo ipsum, ac malesuada tellus vehicula eu. Aliquam eget erat interdum, accumsan lorem ac, mollis metus. Ut molestie velit a leo rhoncus, maximus lacinia nisi faucibus. Cras aliquet nec odio ac semper. Integer nec facilisis quam. Curabitur mattis tortor sed leo eleifend, eu laoreet ex cursus. Morbi accumsan congue nisi vitae euismod. Cras id ipsum turpis. Aliquam lacinia, dolor sed pretium tristique, leo massa dapibus lorem, at molestie augue sapien non diam. 
    </div>

</div>

<div class="section persist-area">

    <div class="section-header-wrapper">
        <div class="section-header">Section Header 2</div>
    </div>
    <div class="section-content">
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eu nisi at tellus efficitur lobortis a suscipit elit. Nulla sem arcu, imperdiet id lorem ut, cursus commodo urna. Duis malesuada pulvinar est, non venenatis nisi euismod nec. Integer euismod commodo ipsum, ac malesuada tellus vehicula eu. Aliquam eget erat interdum, accumsan lorem ac, mollis metus. Ut molestie velit a leo rhoncus, maximus lacinia nisi faucibus. Cras aliquet nec odio ac semper. Integer nec facilisis quam. Curabitur mattis tortor sed leo eleifend, eu laoreet ex cursus. Morbi accumsan congue nisi vitae euismod. Cras id ipsum turpis. Aliquam lacinia, dolor sed pretium tristique, leo massa dapibus lorem, at molestie augue sapien non diam. 
    </div>

</div>  

CSS

Header States and Positioning

In order to get the sticky and parked effect we will be using CSS. We will need a class for each header state. There are three header states:

  1. Normal
  2. Sticky
  3. Parked

To get the sticky effect we will be using the fixed position to keep it at the top of the viewport. Once we reach the bottom of the containing block of the header we will switch the position of the header to absolute so that we can position it relative to the containing block.

.sticky-active
{
    position: fixed;
    top: 0;
}

.sticky-parked
{
    position: absolute;
    bottom: 0px;
}

For the absolute positioning to work we would need to make the position of the containing block relative.

.section
{
    position: relative;
}

You can also, optionally, set a sticky class that will style the header once it moves from its original position:

.sticky
{
    font-size: 30px;
    background-color: rgba(200, 200, 200, 0.5);
    height: 40px;
}

Header Wrapper

As we move the header using fixed positioning, the area that was occupied by the header would not be reserved for the header anymore and you would see the content jump up and down as you scrolled down. To prevent that we need to give the wrapper class around the header class a discrete height.

.section-header-wrapper
{
    height:20px;
}

Optional Transitioning

If you opted to have the header change style as it moves then add this to make the transitioning look nice:

.section-header
{
    transition: all 0.5s ease;
}

Javascript

I liked CSS-tricks’ Brad‘s code. However, I didn’t like duplicating all the headers. I felt it was a waste of processing power and memory. Instead I opted for changing the header class as we scroll:

$(document).ready( function() {     

    function updateTableHeaders() {
       $(".persist-area").each(function() {

           var el             = $(this),
               offset         = el.offset(),
               scrollTop      = $(window).scrollTop(),
               sectionHeader = $(".section-header", this),
               sectionHeaderWrapper = $(".section-header-wrapper", this),
               sectionHeaderWrapperOffset = sectionHeaderWrapper.offset(),
               sectionContent = $(".section-content", this),
               sectionContentOffset = sectionContent.offset();

           if ((scrollTop > sectionHeaderWrapperOffset.top))
           {
                sectionHeader.addClass("sticky");

                if ((scrollTop < offset.top + el.outerHeight() - sectionHeader.height()))
                {
                    sectionHeader.removeClass("sticky-parked");
                    sectionHeader.addClass("sticky-active");
                } else
                {
                    sectionHeader.removeClass("sticky-active");
                    sectionHeader.addClass("sticky-parked");
                }
           } else
           {
                sectionHeader.removeClass("sticky-active");
                sectionHeader.removeClass("sticky");
           }

       });
    }

    updateTableHeaders();
    $(window).scroll(updateTableHeaders);
});

If you don’t want to have the optional sticky class to style the header as its removed from its usual position just delete all lines with just sticky in them.

Basically we check every persist-area element, and see where the viewport is in relation to it. If the top of the viewport (scrollTop) has passed the top of the header wrapper (sectionHeaderWrapperOffset.top) then add the sticky class.

Next we need to check whether the header of that area should be active or parked. We want the header to be active (sticky at the top) as long as the top of the viewport is still within the persist-area element.

When should the header turn from active to parked? If we wait till the bottom of the persist-area element goes above the top of the viewport then the top of the header will be at the bottom of the persist-area element, which would look weird. Instead, we want the the header to turn to parked once the bottom of the header reaches the bottom of the persist-area element, hence we check for (scrollTop < offset.top + el.outerHeight() - sectionHeader.height()).

Notice that whenever we add the active class we remove the parked class. This is because they are mutually exclusive.

The Result

See the Pen Sticky/Persistent Headers by Ahmed Amayem (@amayem) on CodePen.

Dealing with the Bottom Margin, Border and Padding

You will notice in the demo that the parked headers will be parked at the very bottom of the persist-area element, meaning it will be parked after the bottom margin, border and padding. If you have large values for those properties then it might not look so nice.

You may want the headers to be parked at the end of the content only. Let’s look at different ways of doing this:

  1. Change HTML, CSS and Javascript:
    • Put the header inside the content div
    • Make the content CSS have a relative position
    • Change the Javascript to calculate the content div offset
  2. Change CSS and Javascript:
    • Calculate the bottom margin, border and padding and change the calculation of when to chaneg from active to parked
    • Statically set the bottom attribute of the parked header to equal the bottom margin, border and padding.
  3. Change CSS:
    • Remove any bottom margin, border and padding in your css to begin with
  4. Change Javascript:
    • Calculate the bottom margin, border and padding using Javscript
    • Dynamically adjust the parked header css using javascript

Change the HTML, CSS and Javascript

Pros

  1. Least number of calculations
  2. Works for containing persist-area divs with different margins, borders and paddings

Cons

  1. Requires some structural HTML changes

HTML

By moving the headers into the content divs we can position them absolutuely at the bottom of the the content, which will be above the bottom margin, border and padding of the persist-area:

<div class="section persist-area">

    <div class="section-header-wrapper">
        <div class="section-header">Section Header 1</div>
    </div>
    <div class="section-content">
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eu nisi at tellus efficitur lobortis a suscipit elit. Nulla sem arcu, imperdiet id lorem ut, cursus commodo urna. Duis malesuada pulvinar est, non venenatis nisi euismod nec. Integer euismod commodo ipsum, ac malesuada tellus vehicula eu. Aliquam eget erat interdum, accumsan lorem ac, mollis metus. Ut molestie velit a leo rhoncus, maximus lacinia nisi faucibus. Cras aliquet nec odio ac semper. Integer nec facilisis quam. Curabitur mattis tortor sed leo eleifend, eu laoreet ex cursus. Morbi accumsan congue nisi vitae euismod. Cras id ipsum turpis. Aliquam lacinia, dolor sed pretium tristique, leo massa dapibus lorem, at molestie augue sapien non diam. 
    </div>

</div>

CSS

In order for the absolute positioning of the header to work in relation to the content we need to make the content div have a position of relative. Add the following to the CSS:

.section-content
{
    position: relative;
}

Javascript

In the original we calculated when the top of the window had reached the bottom of the containing div. Instead change the calculations to calculate the bottom of the content div:

function updateTableHeaders() {
   $(".persist-area").each(function() {

       var el             = $(this),
           offset         = el.offset(),
           scrollTop      = $(window).scrollTop(),
           sectionHeader = $(".section-header", this),
           sectionHeaderWrapper = $(".section-header-wrapper", this),
           sectionHeaderWrapperOffset = sectionHeaderWrapper.offset(),
           sectionContent = $(".section-content", this),
           sectionContentOffset = sectionContent.offset();

       if ((scrollTop > sectionHeaderWrapperOffset.top))
       {
            sectionHeader.addClass("sticky");

            if ((scrollTop < sectionContentOffset.top + sectionContent.outerHeight() - sectionHeader.height()))
            {
                sectionHeader.removeClass("sticky-parked");
                sectionHeader.addClass("sticky-active");
            } else
            {
                sectionHeader.removeClass("sticky-active");
                sectionHeader.addClass("sticky-parked");
            }
       } else
       {
            sectionHeader.removeClass("sticky-active");
            sectionHeader.removeClass("sticky");
       }

   });
}

Result

See the Pen Sticky/Persistent Headers Parked at End of Content (HTML, CSS, JS Version) by Ahmed Amayem (@amayem) on CodePen.

Change CSS and Javascript

Pros

  1. Does not requires structural HTML changes

Cons

  1. Does not work for containing persist-area divs with different margins, borders and paddings

Javascript

Adjust your Javascript to change the sticky header class to parked when you reach the top of the bottom padding, by calculate the bottom margin, border and padding and using that number in the calculations:

var bottomMBP = parseInt($($(".persist-area")[0]).css("margin-bottom"), 10) + parseInt($($(".persist-area")[0]).css("borderBottomWidth"), 10) + parseInt($($(".persist-area")[0]).css("padding-bottom"), 10);

function updateTableHeaders() {
   $(".persist-area").each(function() {

       var el             = $(this),
           offset         = el.offset(),
           scrollTop      = $(window).scrollTop(),
           sectionHeader = $(".section-header", this),
           sectionHeaderWrapper = $(".section-header-wrapper", this),
           sectionHeaderWrapperOffset = sectionHeaderWrapper.offset();

       if ((scrollTop > sectionHeaderWrapperOffset.top))
       {
            sectionHeader.addClass("sticky");

            if ((scrollTop < offset.top + el.outerHeight() - bottomMBP - sectionHeader.height()))
            {
                sectionHeader.removeClass("sticky-parked");
                sectionHeader.addClass("sticky-active");
            } else
            {
                sectionHeader.removeClass("sticky-active");
                sectionHeader.addClass("sticky-parked");
            }
       } else
       {
            sectionHeader.removeClass("sticky-active");
            sectionHeader.removeClass("sticky");
       }

   });
}

CSS

Calculate the bottom margin border and padding of the persist-area divs, either manually or by getting the value from your javascript above, then just simply change the bottom value of your parked class to that number:

.sticky-parked
{
    position: absolute;
    bottom: 20px;
}

Result

See the Pen Sticky/Persistent Headers Parked at End of Content (CSS, JS Version) by Ahmed Amayem (@amayem) on CodePen.

Change CSS

Pros

  1. Does not requires structural HTML changes
  2. Does not require any extra Javascript calculations

Cons

  1. Does not work for containing persist-area divs with different margins, borders and paddings
  2. Limits your styling of the persist-area divs because you can’t style their bottom margins, border or paddings.

CSS

This is straighforward, just remove any margin, border and padding from the bottom of the persist-area divs:

.section
{
    margin: auto;
    padding: 20px;
    width: 500px;
    position: relative;
    font-size: 20px;
    margin-bottom: 0px;
    padding-bottom: 0px;
    border-bottom: 0px;
}

Result

See the Pen Sticky/Persistent Headers Parked at End of Content (CSS) by Ahmed Amayem (@amayem) on CodePen.

Change Javascript

Pros

  1. Does not requires structural HTML changes
  2. Works for containing persist-area divs with different margins, borders and paddings

Cons

  1. A bit more calculation intensive

This method is more intensive in terms of caclulations than the others, but it will give you the ability to dynamically park the sticky header at the end of the content, no matter what the bottom margin, border and padding of the divs. Therefore this can be used in cases where you have different persist-area divs with different bottom margins, borders and paddings.

function updateTableHeaders() {
   $(".persist-area").each(function() {

       var el             = $(this),
           offset         = el.offset(),
           scrollTop      = $(window).scrollTop(),
           sectionHeader = $(".section-header", this),
           sectionHeaderWrapper = $(".section-header-wrapper", this),
           sectionHeaderWrapperOffset = sectionHeaderWrapper.offset(),
           sectionContent = $(".section-content", this),
           sectionContentOffset = sectionContent.offset();

       if ((scrollTop > sectionHeaderWrapperOffset.top))
       {

       var bottomMBP = parseInt(el.css("margin-bottom"), 10) + parseInt(el.css("borderBottomWidth"), 10) + parseInt(el.css("padding-bottom"), 10);  
       sectionHeader.addClass("sticky");

         if ((scrollTop < offset.top + el.outerHeight() - bottomMBP - sectionHeader.height()))
            {
                sectionHeader.removeClass("sticky-parked");
          sectionHeader.css("bottom", "");
                sectionHeader.addClass("sticky-active");
            } else
            {
                sectionHeader.removeClass("sticky-active");
                sectionHeader.addClass("sticky-parked");
          sectionHeader.css("bottom", bottomMBP)
            }
       } else
       {
            sectionHeader.removeClass("sticky-active");
            sectionHeader.removeClass("sticky");
       }

   });
}

Result

See the Pen Sticky/Persistent Headers Parked at End of Content (JS Version) by Ahmed Amayem (@amayem) on CodePen.

References

  1. CSS-tricks’ Brad Sticky Header Implementation‘s code

Ahmed Amayem has written 90 articles

A Web Application Developer Entrepreneur.