Published Jul 29. 2016 - 5 years ago
Updated or edited Oct 8. 2020

The fixed top menu

A lot of sites feature a part of the page – typically a top menu – that is fixed to the top of the screen when you scroll down. Here are some thoughts about such a feature and how to do it.

Nerd alert

This is NOT about fly fishing or fly tying, but about site development and nerdy stuff.

Sticky is your friend

All the good advice below is quite OK, but the real solution to this issue is the CSS tag position:sticky. Sticky is precisely what you need to make a menu... eh... sticky.
Add that bit of CSS to the menu container - in my case #menu-bar, and maybe tell it where to stick. Default is top:0, but I have set it to top:-1px. This has the desired effect: the menu sits where it sits until it hits its top position and then it sticks and stays. You may need to play with some z-index values to get it on top of everything, but it sure beats lots of code as sketched below.
Mission accomplished.

DIY Photography
Google Analytics
Fixed headers block a lot of content on these two pages
Martin Joergensen

I generally dislike fixed elements on a screen – for several reasons:
- They fill the screen space with permanent elements that I generally don't need.
- It breaks the user's feeling of seeing one page and having a “physical” sense of the position of things.
- The way some developers add dynamic transitions like zooms, swoops, fades and whatnot, doesn't appeal much to me. And it's often done technically clumsily, which also bothers me.
- It often comes with fairly complex code, sometimes even two copies of the same element – the dynamic and the fixed.

Screen estate

The first issue is my main argument against using fixed top bars or headers. A lot of sites simply set the header region of the page to be fixed and let the rest scroll. On my medium resolution laptop, that means that up to a third or more of the screen height is taken by static elements, leaving me way too little space to read and especially to see images.
From the top of the screen I may see the browser top bar and menu, the browser tabs, the browser's location entry field, my personal toolbox icons (like Firebug in Firefox) and then the fixed part of the page.
Sometimes I can add a fixed bottom on the page to that, like social media links, newsletter signup or some other footer content plus whatever the browser decides to put there and my Windows task bar. All that leaves preciously little space in the middle for actual page content.

High or low

The technical side of fixed content can be pretty simple. Just add the CSS style position:fixed to any element, and it stays where it was first rendered.
The most recent incarnation of Google Analytics uses this approach. It renders a page top, fixes it and simply leaves it there, scrolling the rest of the page under it. Many sites use this approach which is simple, but also the one that potentially uses up most space, because the header can often contain a lot of stuff like a logo, links, a menu, a search form and much more.
Other sites render the page with a normal header with all these elements, but once the user scrolls this header off the page, a smaller subset of the header fixes itself to the top of the page. Typically what remains is just a single line menu or a simplified header, and that takes up way less space than the full header.

Double up

Unfortunately this approach is mostly done by simply adding an extra element to the page. The fixed menu is rendered as one entity and the usual header as another – mostly these two contain something, which is close to identical or completely identical. Having these two elements allows the system to change the visibility of the fixed one when the normal header scrolls off screen.
But if the menu is large – like the menu on this site – containing many submenus, images, lists and much more, the user gets two almost identical copies adding load time and complexity.

Same, same but different

The solution to this is of course to have one menu only and fixing that as the user scrolls. This means that there's only one copy of the possibly complex and large menu, and, as it's the the case on this site, it also reliefs the developer from having to juggle identical id's and much more.
Luckily all the menu HTML for the Global FlyFisher is contained in a single element – a div with the id #menubar. This is simply rendered as part of the header like it would be on any site and is positioned just beneath the top header of the page with the logo, search form and other elements. In order to fix it at the right time, we need to look at some element that scrolls on and off the page as the user moves down through the content. We can't look at the menu bar itself, since it will scroll off, but then be fixed onscreen. Again we're lucky, because almost on the same level as the menu bar, there's a search form, which rolls off screen as the user scrolls.

JavaSccript and CSS

Using jQuery to check the visibility of this form, we can change the class of the menu bar to something else – like fixed.
This is the script that does it:

(function($) { $(window).scroll(function(){
    var itemOffset = $('#block-search-form').offset().top;
    var scrollTop = $(window).scrollTop();
    if (scrollTop >= itemOffset) {
    else {
Global FlyFisher
Global FlyFisher
Martin Joergensen

This is loaded into the html.tpl.php file, meaning that it will be loaded on all pages.
So normally the menu bar has no class, but when the search form moves off screen, it gets the class fixed.
In the CSS, .fixed has the property position:fixed, top:0 and a high z-index, which will staple it to the top edge of the screen and make sure it's visible. In the case of this site the rounded corners are also changed, the height is adjusted slightly and it's given a small shadow to add a 3D kind of effect.

The result

The result is a menu bar containing code for a large and complex menu, which appears only once on the page and is fixed using a very simple script and CSS or a faily simple Drupal module. No major DOM changes are made, only a class is added or removed. And it's only done when the condition is triggered, which is just once or a few times as the user scrolls up and down on a page.
The downturn is the jQuery scroll() function, which is called all the time and which will potentially call our little function thousands of times during a normal page visit.
But I haven't experienced any performance problems with this solution, which works fine in all browsers and has been running flawlessly for a year now.

Updated: A better - more correct - alternative

After having implemented this and having it up and running for the above mentioned year, I realized that it's not recommended to use the scroll() function for triggering events - which actually makes good sense, because it fires with very small intervals, and adds a risk of overloading the system and causing jagged movements. Firefox actually fires a warning when it detects the scroll() function.

This site appears to use a scroll-linked positioning effect. This may not work well with asynchronous panning; see for further details and to join the discussion on related tools and features!

Since I need to detect the position of the triggering element (the search form in my case), I need something else to trigger the detection. I opted for a simple interval timer.
The timer fires every half second and if the search form is off screen, it sets the class on the menu div to .fixed, which is set to be fixed at the top of the screen. When the search box enters the screen again, the class is removed again, and the menu pops back in its usual place below the head.
I wanted the code a little more manageable, and decided to develop a small module - sticky_menu - which handles everything. This module offers configurable ids for the trigger and menu as well as a setting for the class and timer interval.
I transfer the variables between Drupal and jQuery using the drupal_add_js() function and the Drupal.settings.construct in JavaScript. It all added a whole lot of complexity, but also gave a cleaner and more correct way of doing things. To the end user the half second delay is probably insignificant, and you can always set the delay even shorter in the configuration.

 * @file sticky_menu.js javascript file
 * */

(function($) {
  $(document).ready(function() {

    // Get trigger, menu id and class from Drupal's js settings
    var stickyMenuTrigger = '#' + Drupal.settings.sticky_menu.stickyMenuTrigger;
    var stickyMenuId = '#' + Drupal.settings.sticky_menu.stickyMenuId;
    var stickyMenuClass = Drupal.settings.sticky_menu.stickyMenuClass;

     * Check whether the trigger is out of the screen and change menu class accordingly
     * */
    stickyMenuCheck = function() {
      if (!$(stickyMenuTrigger).length || !$(stickyMenuId).length) {
        // One of the elements is missing
      // Get trigger position
      var itemOffset = $(stickyMenuTrigger).offset().top;
      // Get item position
      var scrollTop = $(window).scrollTop();
      // Compare and add or remove class
      if (scrollTop >= itemOffset) {
      else {

    // Get interval from Drupal's js settings
    var stickyMenuTime = Drupal.settings.sticky_menu.stickyMenuTime;
    // Check every stickyMenuTime milliseconds
    var stickyMenuTimer = setInterval(stickyMenuCheck, stickyMenuTime);

} (jQuery));

I add the settings in the module file, where they are made available for the jQuery script using hook_preprocess_html()

 * Implements hook_preprocess_html
 * There to make variables available to jQuery
 * */
function sticky_menu_preprocess_html(&$vars) {
  $variables = array(
    'stickyMenuTrigger' => variable_get('sticky_menu_trigger', ''), // '#block-search-form',
    'stickyMenuId' => variable_get('sticky_menu_id', ''), // '#menubar'
    'stickyMenuClass' => variable_get('sticky_menu_class', ''), // 'fixed'
    'stickyMenuTime' => variable_get('sticky_menu_time', 500),
  drupal_add_js(array('sticky_menu' => $variables), 'setting');

Log in or register to pre-fill name on comments, add videos, user pictures and more.
Read more about why you should register.

Since you got this far …

The GFF money box

… I have a small favor to ask.

Long story short

Support the Global FlyFisher through several different channels, including PayPal.

Long story longer

The Global FlyFisher has been online since the mid-90's and has been free to access for everybody since day one – and will stay free for as long as I run it.
But that doesn't mean that it's free to run.
It costs money to drive a large site like this.

See more details about what you can do to help in this blog post.