Grow your CSS skills. Land your dream job.

Persistent Headers

Published by Chris Coyier

This is some code to get the header of some content area to stay visible at the top of the screen as you scroll through that content. Then go away when you've scrolled past that relevant section.


Header... persisting.

Couple things to know before we get started:

  1. There are many like it, but this one is mine.
  2. This doesn't work (yet) on mobile (at least mobile Safari that I looked at). Because I don't know that much about JavaScript on mobile and how to detect scroll events and scroll position and stuff.

HTML

For the sake of demo purposes, we'll identify areas we wish to apply persistent headers to via a class name "persist-area" and the header within we indent to be persistent with "persist-header". These are totally unsemantic class names, but you'd fix that in your own implementation by just knowing your own markup and applying appropriate selectors.

<article class="persist-area">
   <h1 class="persist-header">
   <!-- stuff and stuff -->
</article>

jQuery JavaScript

This is the plain English of what we are going to do:

  1. Loop through each persistent area and clone the header. The cloned header remains invisible until we need it.
  2. Every time the window scrolls, we run some tests.
  3. If we have scrolled into an area which should have a persistent header, but the header would be hidden, we reveal our cloned header, in a fixed position. All other persistent headers will be hidden.

The whole kit and kaboodle:

function UpdateTableHeaders() {
   $(".persist-area").each(function() {
   
       var el             = $(this),
           offset         = el.offset(),
           scrollTop      = $(window).scrollTop(),
           floatingHeader = $(".floatingHeader", this)
       
       if ((scrollTop > offset.top) && (scrollTop < offset.top + el.height())) {
           floatingHeader.css({
            "visibility": "visible"
           });
       } else {
           floatingHeader.css({
            "visibility": "hidden"
           });      
       };
   });
}

// DOM Ready      
$(function() {

   var clonedHeaderRow;

   $(".persist-area").each(function() {
       clonedHeaderRow = $(".persist-header", this);
       clonedHeaderRow
         .before(clonedHeaderRow.clone())
         .css("width", clonedHeaderRow.width())
         .addClass("floatingHeader");
         
   });
   
   $(window)
    .scroll(UpdateTableHeaders)
    .trigger("scroll");
   
});

CSS

The only CSS we're directly changing with JavaScript is the visibility of the persistent header. I feel like that's pretty acceptable. All other styling is rightfully a part of the class. It's pretty light, all we need is:

.floatingHeader {
  position: fixed;
  top: 0;
  visibility: hidden;
}

And so...

Use as you will.

View Demo   Download Files

Comments

  1. Yea techniques like this will not work on mobile due to the limited and still crappy support for fixed positioning. Not to mention that scroll events on mobile are slow due to scrolling algorithms used to accelerate touch movement.

    Recalculating scroll position and then placing absolutely on the page would be the only effective way to due this on modern mobile browsers. But its a less than usable experience.

    Great post thou!

    -B

  2. Aaron
    Permalink to comment#

    This should work on iOS5, from the looks of it.

    • Andrew
      Permalink to comment#

      Event timing seems a little off but it does work in iOS5

    • The way I understood it is, iOS 5 will support fixed positioning with a different vendor prefix. So fixed positioning by default will still be disabled, but with the special vendor prefix, you can override the default.

    • Sorry, guess I should do my research BEFORE posting. Fixed position is supported by default, it’s the new touch-scrolling overflow property that requires a special prefix:
      -webkit-overflow-scrolling: touch;

  3. Croata
    Permalink to comment#

    Chris do you use the example of 3 header, so each time q reaches a new header as above, disappears !

    Now if I just use a header, as far as it will continue to appear, how can I determine ?

  4. Lara
    Permalink to comment#

    Dosen’t work on iPad …

    • DeltaDave
      Permalink to comment#

      “This doesn’t work (yet) on mobile (at least mobile Safari that I looked at).”

    • Leslie
      Permalink to comment#

      It’s working in iOS 5 already :)

  5. Andrey
    Permalink to comment#

    There is one thing that I don’t like about position:fixed, is that it fixes also left position. When window’s width is less than fixed header’s width scrolling left will move all page content except the fixed part.

    But it is possible to change left position of fixed element on scroll and resize window events. You have to add something like this for each “.persist-area” in UpdateTableHeaders function:

    
    floatingHeader.css("left",  $(".persist-header", this).offset().left - $(window).scrollLeft());
    
  6. Marcos
    Permalink to comment#

    Really nice, and clever way to do it, thanks a lot :)
    Correct me if I am wrong please, but apparently the header will in this case always be positioned relatively to the window as it uses a fixed position right? so If you like to use a parent element and position the header to the element which could only appear in the middle of screen wouldn’t be possible?

  7. Permalink to comment#

    Nice post Chris.

    One tip is the performance associated with attaching handlers to the scroll event, particularly ones that query the DOM on every single scroll. Much better to cache the DOM references instead of looking them up each time.

    Twitter had this issue once and John Resig wrote a helpful post with some alternative JS options – http://ejohn.org/blog/learning-from-twitter/

  8. Learned a lot from this post.

    Including the fact that it’s “kit and kaboodle.”

    Not kitten kaboodle.

  9. @Chris Coyier, I had used some jquery on an iPad micro site for my company, It detected scroll position and would update the position of a menu bar, I also used it to auto scroll to the top of specific sections based on current position. I’ll get a link to you for that.

  10. Permalink to comment#

    Brilliant, thanks a lot. I always wanted to know how to do that.

  11. Sarah Saunders
    Permalink to comment#

    Very nice! Ive seen this done on iphone apps and thought it was a great technique. Never thought of how to accomplish this via html, js, etc. Thanks!!

  12. Great example Chris.

    I’ve done the same lately, check this out:

    studio.conduit.com

  13. xzyfer
    Permalink to comment#

    You should take a look at the iScroll project (http://cubiq.org/iscroll). It was created to solve this problem specifically for iOS devices, but ofcause works on other modern mobile platforms.

    The main advantage of iScroll is that it achieves effect my intelligently using CSS. This means web browsers, mobile or otherwise, will use hardware acceleration. This results in a really smooth effect.

  14. xzyfer
    Permalink to comment#

    You should take a look at the iScroll project (http://cubiq.org/iscroll-4). It was created to solve this problem specifically for iOS devices, but ofcause works on other modern mobile platforms.

    The main advantage of iScroll is that it achieves effect my intelligently using CSS. This means web browsers, mobile or otherwise, will use hardware acceleration. This results in a really smooth effect.

  15. Rahmat Irfan Denas
    Permalink to comment#

    you really have very cool stuff to give

  16. Thats a nice usability feature for tablebased content. Hope that in future more sites would like tricks like that. Something to improve the usability on table content could be too to colour every second line different with the :nth-child selector =)

  17. Some guy
    Permalink to comment#

    They are OK for table headers, but should never be used for text headings, or for site header, because it will overlap content and will make it unreadable, you have to scroll up to read it. Fixed elements drag too much attention, away from content (I hate these fixed facebook buttons at edge of some sites that follow scrollbar).

    • There just isn’t enough information here to make a call whether you should use a technique like this or not in an actual design. Like every tutorial ever written, actual usage depends on actual scenarios.

  18. Ben
    Permalink to comment#

    Why are you triggering a scroll event at the end? Wouldnt this be fired anyway since you arent using preventDefault?

  19. I feel like the header should stop 20px or so before the bottom, but that adds a bit of complexity to the issue. It just doesn’t make a ton of sense for the header to be all the way at the bottom of the box, so that absolutely none of the content can be seen. That’s just my opinion though, I’m sure there are cases where I’d want it to work exactly like this too.

  20. Jonathan [JCM]
    Permalink to comment#

    Chris, you set as var clonedRow, but use clonedHeaderRow

    Nice post! =)

  21. Permalink to comment#

    Thanks for the post! I’ve been thinking about learning this one for a short while, but hadn’t had time to investigate. Happy it was delivered right to my inbox. Not to mention I’m a long time fan of this website and tuts involved. Thanks again!

  22. This works perfectly on my Asus Transformer.

  23. p. thompson
    Permalink to comment#

    Thanks! I was just wondering how to do that. :)

  24. DAM! I love this new design…just had to say.

  25. Permalink to comment#

    Awesome tutorial ! I always wondered how this is done. Hope it works across platforms too ! Thanks Chris.

  26. Permalink to comment#

    Nice script, but I cant get it to work good for tables with dynamic width.

    The td’s in the cloned header tr wont inherit the column width of the tbody td’s.

    Quick&dirty; to illustrate what I’m trying to describe :

    
       <table class="persist-area">
    
            <thead>
              <tr class="persist-header">
                 <th>Colr</th>
                 <th>Coluder</th>
                 <th>Colueader</th>
                 <th>Column Two Header</th>
                 <th>CoOne Header</th>
                 <th>Column Two Header</th>
                 <th>C Header</th>
                 <th>ColumTwo Header</th>
                 <th>Colader</th>
                 <th>Coluder</th>
    
              </tr>
            </thead>
            <tbody>
              <tr>
    
                 <td>table1 data1</td>
                 <td>table1 data1</td>
    
                 <td>table1 data1</td>
                 <td>table1 data1</td>
    
                 <td>table1 daasddddddddddddddddddddddddddddddddddddddta1</td>
                 <td>table1 datasdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddda1</td>
    
                 <td>table1 data1</td>
                 <td>table1 data1</td>
    
                 <td>table1 data1</td>
                 <td>table1 data1</td>
              </tr>         
            
            </tbody>
          </table>
    
    • teapot
      Permalink to comment#

      try this see if it works:

      var $floatingHeader = persist_header.clone();

      $floatingHeader.children().width(function (i, val) {
      return persist_header.children().eq(i).width();
      });

      $floatingHeader.css(“width”, persist_header.width()).addClass(“floatingHeader”);
      persist_header.before($floatingHeader);

    • Permalink to comment#

      Thanks Teapot.

      Worked like a charm with minor fixes.

      
      // DOM Ready
      $(function() {
      
      
         
         
          var $floatingHeader = $(".persist-header", this).clone();
      
          $floatingHeader.children().width(function (i, val) {
          return $(".persist-header").children().eq(i).width();    
          });
      
          $floatingHeader.css("width", $(".persist-header", this).width()).addClass("floatingHeader");
          $(".persist-header", this).before($floatingHeader);   
         
      
         $(window)
          .scroll(UpdateTableHeaders)
          .trigger("scroll");
      
      });
      
      
    • Mike
      Permalink to comment#

      Sorry, but I couldn’t get your code to work properly. It would generate the number of persist-header’s on the page for each persist-area. I put it inside an .each() and fixed that issue, but then it would only have the correct width for the first table on my page (all tables had dynamic widths). I rewrote it to the following.

      var floatingHeader;
      
      	$(".persist-area").each(function(){
      
      		floatingHeader = $(".persist-header", this);
      		
      		floatingHeader.before(floatingHeader.clone());
      	
      		floatingHeader.children().css("width", function(i, val){
      			return $(floatingHeader).children().eq(i).css("width", val);
      		});
      	
      		floatingHeader.addClass("floatingHeader");
      		
      	});
      
  27. Great! I was impressed how a few lines of JavaScript can do something so useful & needed.

  28. Timothy
    Permalink to comment#

    Hey Mr. Coyier,
    Would you mind me asking what you use for showing code in your blog posts? I looked for a blog article on this (and other) sites, and I found a few neat things, but nothing looked quite as nice as what you use. Thanks!

  29. Charbs
    Permalink to comment#

    That’s great man. Very useful.

  30. Kiran Tambe
    Permalink to comment#

    Gmail inbox has this. thanks chris for such great tips.

  31. I did some android testing. It doesn’t do anything on the stock webkit browser. Firefox reports the event after I release my finger. Opera Mobile seems to report the event, but the header gets drawn in the wrong place. It has the same delay as FF.

    I didn’t bother testing on Opera Mini.

  32. Permalink to comment#

    There is a error in the script.

    persistent-area and persist-area are mixed in html and javascript

  33. Permalink to comment#

    Fantastic, Chris. Thanks a ton. Just what I needed for a current project.

    For my needs, I wanted the element to appear only after scrolling down a bit. So to the element itself I added display: none, and then to .floatingHeader I added display: block.

    Like this:

    .elementClass {
    display: none; /*hides element until needed*/
    }
    .floatingHeader {
    position: fixed;
    top: 0;
    visibility: hidden;
    display: block; /*shows element when needed*/
    }

    Example:

    http://www.beamingbioneersvermont.com/presenters.php

    [scroll down to see it in action]

    Thanks again!

  34. Hans Kuijpers
    Permalink to comment#

    Thanks for this great piece of code. I’m implementing it on a productpage with a large table.
    I wonder how I should alter the code so that the header disappears when the bottom of the header touches the end of the table. Now it disappears when the top of the header touches the end of the table. This results in a header appearing below the table.

    or even better… let the header (thead) disappear when the bottom of the header touches the top of the footer (tfoot).

  35. jonny
    Permalink to comment#

    Is there a way to make it work with jquery-2.0.2.min.js

  36. Alex
    Permalink to comment#

    Hi! Thanks for your tutorial. It´s fantastic!

    Now I´m triying to implement this code in WordPress but I can´t do it.

    I don´t know why but it doesn´t work. Can anyone help me please?

    I think the problem is in line:

    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"

    Cause without this linea it doesn´t work and my WordPress can´t read this line.

    Thanks in advance

  37. Permalink to comment#

    This seems like a bad idea duplicating the heading, what if you have attached event listeners to a specific ID? If you duplicate the code this breaks the functionality since it generates two blocks with the same IDs, breaking whatever is in the heading or duplicating event listeners. I just tried to implement and came across this issue, started switching over to classes but still it’s very hacky solution for what you’re trying to achieve in my opinion.

  38. Permalink to comment#

    Hey man, thanks for the code “:).

    But, what if I want to make a column fixed too? For example, if I want the header and the first column of a table to be fixed, but only when looking at them… Then, when scrolling down, they would disappear, just like you did…

    How could I do that?

    Thanks!

  39. Thank you for that nice an simple sollution! works perfectly for my usecase!

  40. Hey Chris I was trying to implement a similar thing. But I wanted that the position of the persistant-header should be below the main header of the web-page. I tried by changing the floating header as
    .floatingHeader{
    position: fixed;
    top: 60px;
    visibility: hidden;
    }
    where 60px is the height of the main header.
    The persistent header timings dont seem to work perfectly. Are there any other changes that I need to make?

This comment thread is closed. If you have important information to share, you can always contact me.

*May or may not contain any actual "CSS" or "Tricks".