CSS fixed-position headers
I began this post three months ago, got stuck, and put it in the too hard basket. I wanted to devise a workable solution to my stumbling block before publishing this information. I'm getting ahead of myself, though. First, the background.
As I began writing this post, I had just completed a redesign of this site. The new design removed unnecessary distractions to allow readers to focus on the clearly presented content. I moved site navigation from the sidebar (which I axed altogether) to the header. I decided to fix the header in place so that the navigation and search form would always be visible. This required very little effort, but overcoming the problem posed by fixed-position headers took a great deal of trial and error. To save others from going through this tortuous process I'll describe my various approaches, and list the benefits and drawbacks of each.
Requirements
- An additional vertical scrollbar must not be introduced.
- Appending #example to a URL should result in the element whose id is "example" being displayed at the top of the content area (not hidden behind the header).
- Additional markup may be used only if the requirements above cannot be met without extra markup.
The CSS for the header initially looked like this:
#header
{
position: fixed;
top: 0;
left: 0;
height: 160px;
}
Approach 1: positive top padding + negative bottom margins
Here's the CSS:
h1, h2, h3, h4, h5, h6, p
{
padding-top: 160px;
margin-bottom: -160px;
}
This approach adds top padding (equal to the height of the header) to each of the block-level elements in the content area. This ensures that elements are in the correct position when jumped to using #id. An equal and opposite bottom margin is also applied to prevent the padding from adding unwanted white space between elements.
- No additional markup is required.
- Straightforward CSS.
- The CSS selector needs to contain all block-level elements that may have ids in some instances. This is likely to include elements such as divs and forms. Since it is hard to foresee all the situations in which a link may direct a user to a uniquely identified element, it is difficult to ensure that this approach will work in all cases.
- The negative bottom margin causes each block-level element to overlap the preceding element, making "overlapped" links unclickable!
Approach 2: preceding divs
Again, the CSS:
div.id
{
position: relative;
top: -160px;
}
This approach introduces meaningless markup. Where previously we may have had something like this:
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc faucibus volutpat risus nec mollis. Integer dapibus dictum ultrices. Aenean vel lectus odio. Nam a mi ligula. Nam in dolor quis metus pretium imperdiet sit amet sed elit.</p>
<h3 id="example">Example</h3>
<p>Suspendisse potenti. Proin convallis lacinia nibh, nec auctor ligula mattis consectetur. Mauris vel elit sit amet nibh volutpat varius id vel sem. Pellentesque id purus ligula. Vivamus vel nulla vel justo tempor ultricies.</p>
We now have the following:
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc faucibus volutpat risus nec mollis. Integer dapibus dictum ultrices. Aenean vel lectus odio. Nam a mi ligula. Nam in dolor quis metus pretium imperdiet sit amet sed elit.</p>
<div id="example" class="id"></div>
<h3>Example</h3>
<p>Suspendisse potenti. Proin convallis lacinia nibh, nec auctor ligula mattis consectetur. Mauris vel elit sit amet nibh volutpat varius id vel sem. Pellentesque id purus ligula. Vivamus vel nulla vel justo tempor ultricies.</p>
With this approach, each uniquely identified block-level element within the content area gives its id to a div which appears immediately before it in the HTML. Each of these divs is offset by the height of the header, ensuring that the element of interest is not obscured by the header.
- Straightforward CSS.
- Links are always clickable!
- Additional markup is required, necessitating that existing content be updated (or JavaScript used to insert the additional elements dynamically).
- Not only is this extra markup meaningless, but it actually
removes ids from the elements to which they were additionally assigned.
Any existing CSS selectors that refer to one of these elements will need
to be updated. (For example,
h3#commentswould need to change todiv#comments + h3.
This approach does not have any fatal flaws, but it may require template files, static HTML files, style sheets, and database records to be updated. Additionally, it is inelegant. In other words, it is as option, but not a good one.
Approach 3: JavaScript trickery
The previous approach got the job done, but introduced meaningless elements. This task is best performed with JavaScript.
// accommodate fixed-position header
document.observe('dom:loaded', function () {
$$('h2[id]', 'h3[id]', 'h4[id]', 'h5[id]', 'h6[id]').each(function (e) {
var div = new Element('div', { id: e.id });
e.writeAttribute({ id: null });
e.addClassName('unidentified');
e.insert({ 'top': div });
})
});
The above snippet locates all the h2, h3, h4, h5, and h6 elements on the page that have an id attribute. It then loops through this collection of elements and inserts an empty div element into each one. This div "steals" its parent's id.
CSS can be used to position these empty divs in such a way that headings are visible when jumped to:
h2.unidentified div, h3.unidentified div
{
float: left;
margin: -160px 0 0 0;
}
- No additional markup is required.
- Straightforward CSS.
- Links are always clickable.
- JavaScript (and in this case Prototype) required.
- Association between an id and the element it identifies is broken.
Summary
I have implemented the JavaScript approach, and it works nicely. I am still hopeful that there exists a simpler and/or more universal solution to the problem posed by fixed-position headers. Please let me know if you have any ideas or suggestions.
Possibly related posts
- Prototype image slider
- Coda theme for SyntaxHighlighter
- Captions over images
- Sticky footers
- Valid XHTML alternative to
<strike>
Comments
Hello David,
I myself have been contemplating this same element and even with my lack of experience I won't give up. While I like your approach(s) I have come across this site > http://dev-for-fun.blogspot.com/2008/04/gotcha-why-positionfixed-header-scroll.html
which turn points to this site > http://archive.visitmix.com/2008/
Perhaps it will inspire you even more so or perhaps I am not reading deep enough into how it was in fact accomplished. Best of luck to you. :D
Hey David,
Just wanted to say thanks. Approach #2 is exactly what I was looking for!
:) az
Approach 2 doesn't seem to work in IE7 or IE8. It does in IE6 though…
Thanks for the additional info, Chris. I'm often guilty of not testing things in IE.
Hi Chris,
I'm dealing with the problem of anchors from a long article disappearing under a CSS fixed-position header, on a WordPress theme that I am customizing (www.obamatheconservative.com — username: chris — password: chris).
I am trying to implement your Approach 3 to fixing the issue. I have put the following code in the page template that displays the article (at the end of the same div as the content):
document.observe('dom:loaded', function () {
$$('p[id]').each(function (e) {
var div = new Element('div', { id: e.id });
e.writeAttribute({ id: null });
e.addClassName('unidentified');
e.insert({ 'top': div });
});
});
(I used "p" instead of h1, h2, etc… since all of my ids are in <p> tags)
This does not seem to be inserting the empty divs that it is supposed to. The developer tools don't show anything other than the original <p id="...> tags. Just to be sure, I tried the CSS styling:
p.unidentified div { float: left; margin: -160px 0 0 0; }
Predictably, there was no result.
Do you have any idea what I might be missing? (I'm a total beginner with JavaScript)
Thanks a bundle! Tim
I visited your site, Tim, and noticed an error appear in my browser's JavaScript console:
TypeError: Result of expression 'document.observe' [undefined] is not a function.
document.observe is one of the conveniences Prototype provides, and your site is not using Prototype. You'll either need to rewrite the snippet in vanilla JavaScript, or use a JavaScript library such as Prototype or jQuery to make things easier. The equivalent jQuery code may be of interest.
Hi David (don't know why I thought you were Chris…sorry!)
Thanks for your quick response. I got the Prototype version of approach 3 working like a charm! Prototype.js is included with WordPress, and all the wisdom on the web said I should put the command <?php wp_enqueue_script('prototype'); ?> in the header.php template before <?php wp_head(); ?> to load the Prototype library.
That's what I did originally, and it didn't work. So I tried putting the call to the library right next to the call to your script (named fixedposheaderproto.js) , both in the page.php template that displays the article:
This ended up working fine, but many people have warned that loading the Prototype library that way can cause problems with other plugins that I might add in the future. Any thoughts?
Incidentally, I tried the JQuery version of the script, and it produced throughout the document a bunch of weird paragraph-symbols that were active links, and it didn't fix the disappearing anchor problem.
thanks!! Tim
This ended up working fine, but many people have warned that loading the Prototype library that way can cause problems with other plugins that I might add in the future.
One possible reason for these warnings is that many JavaScript libraries
introduce a global object named $. If one were to add Prototype to a page
featuring jQuery and jQuery code, some of that jQuery code may break (because
it assumes that $ is jQuery but it's now Prototype's dollar function).
Note that jQuery code which adheres to best practice would not be affected,
since it will only assume the presence of jQuery and not its alias, $.
This discussion is moot, though, since one should not be including both Prototype and jQuery on the same page for performance reasons. These hefty scripts provide similar functionality – requiring users to download both files is not cool.
I'm no WordPress expert, but if I were to hazard a guess I'd say that using
wp_enqueue_script allows WordPress to ensure that scripts are not included
twice.
Incidentally, I tried the JQuery version of the script, and it produced throughout the document a bunch of weird paragraph-symbols that were active links, and it didn't fix the disappearing anchor problem.
That's the code used on this site to account for its fixed-position header. Yes, it also adds a permalink to each of these headings while it's at it (hover over the "Respond" heading below to see the behaviour).