Page 1 of 1

Exploring Responsive Multi-Level Horizontal Menu Rate Topic: -----

#1 andrewsw  Icon User is offline

  • lashings of ginger beer
  • member icon

Reputation: 6340
  • View blog
  • Posts: 25,569
  • Joined: 12-December 12

Posted 23 May 2015 - 01:43 PM

I recently used some code I found that displays a horizontal navbar but, at smaller resolution, converts it to a burger/ collapsible menu, also called an "accordion". I was happy with the code but wanted to explore it more. Unfortunately, it didn't come with much information. I did have a look at the CSS and Javascript, but when confronted with lots of 'ul ul li' selectors it is hard to break down. So I decided to build a version myself and see how difficult it is, and what issues arise.

I am fairly happy with the results, see the screenshots. Apologies for the colours! Obviously, I was more concerned about getting it to work, and very little about the look of it. You will see that I use a limited amount of jQuery, and a single media query.

If you aren't already using the jQuery library then I recommend that you rewrite the code in plain Javascript, it is unnecessary to add the library just for the small section of code needed here.

Attached Image

Attached Image

I am not suggesting that it is a professional piece of work, but I do think that there is value in discussing how it was built, and what issues arose. You might then decide to take this forward - making it look more presentable/professional. Even if you, or I, decide to go with plug-ins for this kind of functionality, it is still useful, and interesting, to understand how it would have been built.

I haven't tested extensively. I won't be upset if you point out some failing(s) in a response below, particularly if it raises an interesting discussion point.

The HTML is a standard ul/li/a combination so is tucked away in the spoiler. This is the HTML content between the body tags:

Spoiler

(The full page is given at the end of the tutorial.)

I am not in favour of more than three levels for a navbar. However, after working with just three levels, I realised that, with the way I had written the code, it would work with a fourth level as well (perhaps a fifth..). I add a fourth level just to show that it works.

We are encouraged to take a mobile first approach to responsive web design. Personally, I'm more comfortable to work desktop-first, building for a desktop but always bearing in mind what could happen, or what I could make to happen, at smaller resolution. However, in this case, I started with the accordion menu because it was the feature I had no experience with.
<style>
#menu {
    margin: 2px;
}
nav {
    display: none;
}


At small resolution the navigation bar is not shown and clicking the menu-button will show or hide it.



I had forgotten that it is preferable to use CSS like this to show or hide content (rather than using display: none):
    {
    position: absolute;
    left: -9999em;
}
/* to show it.. */
    {
    left: 0;
}


This is better for accessibility. I did attempt to rewrite it but there was a bit of work involved. I put it on the back-burner. This is something that you might want to consider.



nav ul {
    list-style-type: none;
    padding-left: 0;
    font-size: 0;
    background-color: lightblue;
}


By default a UL has a left-padding of 40px, which I remove.

I will use inline-block for the LI when they are displayed as a horizontal menu (at larger resolution). When there is whitespace between the LI, in the HTML, this causes a small gap to appear between them. One way to remove this gap is to set the font-size to 0 on the UL, then re-instate the font-size on the LI elements. Other ways to resolve this are to put all the LI on a single line (Bleugh!) or to add a comment after each LI (not good..). Note that the font-size settings are not relevant for the acordion (lower resolution) menu, but they are added here as they have no other impact.

I prefer to put as little CSS as possible in media rules. I only want them to contain CSS rules that are necessary at different resolutions.

I'm using colour-names (lightblue). It is still preferable (I understand) to use color-numbers, even though modern browsers recognise a large collection of colour-names. Personally, I'm happy to use standard colour-names (white, blue, red, etc.) but perhaps be a little wary if using a more exotic colour-name.
nav ul ul {
    display: none;
    z-index: 10;
}
.showIt {
    display: block;
}


The nested ULs aren't shown initially. The class-name 'showIt' will be added to show and hide these elements when each accordion button, or a sub-menu button on the full navbar, is clicked on. The z-index is for the full horizontal navbar, to ensure that it doesn't overlap other content, or other elements within the navbar. This isn't necessary for the accordion version, as the buttons are block-level elements and their content will be expanded, and push down the remaining content on the page. Again, I'm happy to put this declaration here as it doesn't impact the accordion version.

nav ul ul Note that this does not just target two-levels of UL within the nav element, it will apply to any deeper levels as well, as each UL will have another UL as a containing element.
nav li {
    display: block;
    font-size: 16px;
}
nav a {
    display: block;
    padding: 5px 10px;
    text-decoration: none;
    color: inherit;
}


The LI elements can occupy the full width of the viewport at lower resolution. The font-size is re-instating the size after setting it to 0 on the UL.

display: block on the A-tags, and padding, expands its clickable area to the full area of its containing LI, otherwise, by default, it is only the text of the link that is clickable.
nav ul ul li {
    min-width: 100px;
}
nav ul ul a {
    background-color: cornflowerblue;
    color: white;
}


Recall that these settings will not just apply to second-level A and LI tags, but to any level of UL within the nav.

I set a min-width of 100px as, firstly, this is a reasonable value, but also because without it, buttons with shorter text display both the colour of the A element and some of the colour of the UL. There are other ways to solve this but min-width works well, and suits my purpose. If you have wider menus increase this value to something like 150px, but increase the same value that occurs in the media query, so that a sub-menu will appear correctly offset from its parent menu.



In the first screenshot it might look to the casual observer that 'PD Courses' is the active sub-menu, when it is in fact 'Microsoft Certified'. Still, the user clicked the menu, so they know where they are. Nevertheless, you might want to consider the colour-combinations used, or take some approach to indicate which sub-menu is active. (Personally, this doesn't concern me greatly.)
nav a[href='#'] {
    background-color: blue;
    color: white;
    /*position: relative;*/
}
/*nav a[href='#']:after {
    content: '\27B2';
    position: absolute;
    right: 3px;
    bottom: 0;
    font-size: small;
}*/


I like this first rule. It allows me to target all buttons that just reveal a sub-menu, those where the href is just '#'.

The commented code demonstrates how to add a little arrow to the right of all the sub-menus, indicating their purpose. I decided to stick with just the colour. Using both is overkill, and partly accounts for the slight confusion just mentioned, about which menu is active.

(Note that relative positioning would be necessary on the A element in order to position the arrow absolutely to its right.)

This is all the CSS that is required for the accordion menu. Some jQuery is then required to enable clicking of the main menu items to expand the sub-menus. This is added at the end of the page, before the closing body-tag. Don't forget to include the jQuery library!!
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>

<script>
    $("nav").on('click', "a[href='#']", function () {
        var $thisUl = $(this).next('ul');
        $('nav ul').not($thisUl).each(function () {
            if (!($(this)[0].contains($thisUl[0])))
                $(this).removeClass('showIt');
        });
        $thisUl.toggleClass("showIt");
    });

    $('#menu').on('click', function () {
        if ($('nav').is(':visible'))
            $('nav').hide();
        else
            $('nav').show();
    });
</script>


The second code block is easy enough, display or hide the navbar on clicking the menu button.

The first code block took a little effort. I can target clicking the sub-menu buttons, those with href of '#'. However, it does not just need to show or hide its adjacent sub-menu (next()), by toggling the 'showIt' class-name, it also needs to collapse any other sub-menu that is open. I needed to collapse all sub-menus (by removing the class-name) EXCEPT the menu, or sub-menu, that contains the currently clicked item. The code reads as "collapse all UL menus by removing the class-name 'showIt', except any that contain the currently clicked UL item", then toggle 'showIt' on the clicked item.

[What held me up for a while is that the contains() method works with DOM elements, not jQuery objects, which is why you see [0] used in the code.]



Having got the accordion menu working for small screens, I saved a second version of the file and then started to modify the CSS to expand to a full horizontal menu. However, I realised that what I should do is add a new section to the bottom of the CSS, modifying the already applied rules. Taking this approach I would then be able to take this new CSS and move it within a media-query, so that both the accordion and full menu would work, at different resolutions.

Also, as I added new CSS rules, I considered whether they would impact the accordion menu. If they didn't then I moved them into the existing CSS. This way, as I mentioned earlier, I only include CSS rules in the media query that are necessary to apply at a different resolution.

An approach that is sometimes taken is just to have two completely separate menus, and show or hide whichever one is appropriate. This is an easier approach, and the two menus can be completely different. However, we'd prefer to avoid doubling-up on the code, and maintenance, if possible.

Here is the full media query that completes the CSS:
@media screen and (min-width: 800px) {
    #menu {
        display: none;
    }
    nav {
        display: block !important;
    }
    nav li {
        display: inline-block;
        position: relative;
    }
    .showIt {
        position: absolute;
        width: 100px;
    }
    nav ul ul ul {
        left: 100px;
        top: 0;
    }
}


Twenty lines! I chose 800px as a suitable breakpoint as it allows me to add some more main menu items. I believe an iPad has a width of 768px, and you will often see this value in media queries (or 767px). However, you do not want to get in the habit of trying to target specific devices. The breakpoints should be based on your content, and testing frequently on different devices. There are many emulators that can help. Use more than one emulator, and test on real devices as well and, of course, different browsers.. particularly IE.

The first two rules are straightforward: hide the menu button and display the full navbar. I rarely use !important but I added it because, if you reduce the browser width, hide the menu, and then maximize the browser, the navbar doesn't reappear automatically. !important resolves this.
    nav li {
        display: inline-block;
        position: relative;
    }


All the LI elements move inline to display horizontally. position: relative is added to give context to the absolute positioning used on the contained UL elements.
    .showIt {
        position: absolute;
        width: 100px;
    }
    nav ul ul ul {
        left: 100px;
        top: 0;
    }


Compare this code with the earlier rules:
nav ul ul {
    display: none;
    z-index: 10;
}
.showIt {
    display: block;
}


The inner ULs are not visible by default. The jQuery code that we already have will apply or remove the class 'showIt' on ULs. With this class applied to a UL (adjacent to a clicked A-tag with href '#') it will be displayed as a block-level element but, at larger resolution, it will also be positioned absolutely (relative to the LI that contains it), and moved 100px to the right of this LI. (We have to move it otherwise it sits above the menu that is already displayed.)

There is a further piece of jQuery that I added:
    $("nav").on('mouseleave', function () {
        $('.showIt').removeClass('showIt');
    });


Leaving the main navbar causes all the sub-menus to collapse, otherwise the user would have to click on a main menu-item to achieve this.

I haven't been able to test this on a device, but I believe tablets, etc., will only run the mouseleave code when tapping away from the menu area, so this code shouldn't interfere.

There may be a small issue here that I should point out. Added: corrected in the first comment below. On a small device, if someone expands a tall sub-menu, then clicks on a different sub-menu further down, then the collapse of the earlier menu may trigger the mouseleave code, resulting in the menu collapsing to its default state. If this were the case then it is something that should be addressed. But I wouldn't want to just remove the mouseleave code, as it is useful with the full navbar.

If you do encounter this then let me know and I'll investigate further. I think I would probably just add a little bit of code in the mouseleave event to check whether the full navbar is currently in use or not. In fact, just checking whether the menu button is currently visible should sufice to distinguish between the accordion and full menu, not removing the class-name 'showIt' if the accordion is currently in use.

Here is the full code, and I hope that you found this discussion useful and/or interesting.

Spoiler

If you want the three-line, burger, button then various ways to create this are outlined here at css-tricks.

This post has been edited by andrewsw: 07 June 2015 - 02:03 PM


Is This A Good Question/Topic? 1
  • +

Replies To: Exploring Responsive Multi-Level Horizontal Menu

#2 andrewsw  Icon User is offline

  • lashings of ginger beer
  • member icon

Reputation: 6340
  • View blog
  • Posts: 25,569
  • Joined: 12-December 12

Posted 23 May 2015 - 02:19 PM

Yes, this slight adjustment to the code fixes the potential, slight issue I mentioned towards the end of the tutorial:
    $("nav").on('mouseleave', function () {
        if (!$('#menu').is(':visible'))
            $('.showIt').removeClass('showIt');
    });

The mouseleave code does nothing if the accordion menu is in use.
Was This Post Helpful? 0
  • +
  • -

#3 andrewsw  Icon User is offline

  • lashings of ginger beer
  • member icon

Reputation: 6340
  • View blog
  • Posts: 25,569
  • Joined: 12-December 12

Posted 23 May 2015 - 04:17 PM

I added the little arrow to indicate the active menu:

Attached Image

It looks kinda cute but I don't consider it particularly necessary.
nav a[href='#'] {
    background-color: blue;
    color: white;
    position: relative;
}
.active_sub:after {
    content: '\27B2';
    position: absolute;
    right: 0;
    font-size: small;
}

    $("nav").on('click', "a[href='#']", function () {
        var $thisUl = $(this).next('ul');
        $('nav ul').not($thisUl).each(function () {
            if (!($(this)[0].contains($thisUl[0])))
                $(this).removeClass('showIt');
        });
        $thisUl.toggleClass("showIt");
        // mark as active..
        $('.active_sub').removeClass("active_sub");
        $(this).addClass("active_sub");
    });
    $("nav").on('mouseleave', function () {
        if (!$('#menu').is(':visible')) {
            $('.active_sub').removeClass("active_sub");
            $('.showIt').removeClass('showIt');
        }
    });

This post has been edited by andrewsw: 23 May 2015 - 04:18 PM

Was This Post Helpful? 0
  • +
  • -

#4 chozen  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 1
  • Joined: 04-June 15

Posted 04 June 2015 - 03:43 PM

Thank you, i need this code^
Was This Post Helpful? 0
  • +
  • -

#5 andrewsw  Icon User is offline

  • lashings of ginger beer
  • member icon

Reputation: 6340
  • View blog
  • Posts: 25,569
  • Joined: 12-December 12

Posted 04 June 2015 - 04:34 PM

Thank you ;)

Of course, it requires a bit of work to make it look attractive. Here is my first lame attempt:

Attached Image

I've just added some borders, changed the font and given it a little more room.

Spoiler

Was This Post Helpful? 0
  • +
  • -

Page 1 of 1