Terence Eden
Terence Eden

Reputation: 14304

Collapse a horizontal menu for response design

I have a menu which looks like this:

|Home|Options|Settings|Tools|Preferences|Edit|

That's fine when a phone has lots of horizontal space, but when a device with a narrow viewport accesses the page, I want the menu to look like

|Home|Options|Settings|+MORE+|

Where clicking the "MORE" menu displays the other items in a vertical drop down.

I don't want to set manual breakpoints, because I have no idea how wide the individual menu items will be when displayed.

My menu is currently just a set of <li> in a <ul>

The CSS for horizontal layout is

#menu ul, #menu li { margin: 0; padding: 0; list-style: none; }
#menu ul { overflow: auto; }
#menu li { float: left; }
#menu  a { display: block; padding: 0.5em; text-decoration: none; border-right: 1px solid #fff; font-size: 110%; }

I'm reluctant to use something like jQuery - even when minimized, it's still a significant overhead for older mobile browsers. Media Queries are also problematic for some phones, so I would like to avoid relying on those.

Any thoughts on CSS and (simple) JavaScript to automatically hide elements depending on the browser's width?

Upvotes: 5

Views: 4642

Answers (1)

Ana
Ana

Reputation: 37169

Actually, you can do it with no JavaScript at all, just with media queries (which really have excellent support + this solution I am presenting is a mobile first one) and :nth-last-child (which again is even supported by Opera Mini).

demo

(resize to see how it works)

You'll need to have a structure like this:

<nav id='menu'>
    <ul>
        <li><a href='#'>Home</a></li>
        <li><a href='#'>Options</a></li>
        <li><a href='#'>Settings</a></li>
        <li><a href='#'>Tools</a></li>
        <li><a href='#'>Preferences</a></li>
        <li><a href='#'>Edit</a></li>
        <li><a href='#'>+ MORE +</a></li>
    </ul>
</nav>

Then you'll need to select the Tools, Preferences and Edit and set their display to none:

#menu li:nth-last-child(-n+4):not(:last-child) { display: none; }

li:nth-last-child(-n+4) selects only the first four list items from the end. You add the :not(:last-child) condition to that because you want the + MORE + list item to be shown.

In order to better understand structural pseudo-classes, you can play around with this tool.

Finally, you'll need to use a media query to change the display settings for larger screens:

@media (min-width: 30em) {
    #menu li:nth-last-child(-n+4):not(:last-child) { display: block; }
    #menu li:last-child { display: none; }
}

I am using an em based media query and not a px based one for two reasons:

  • one, this article;
  • two, my own site looks like crap on zoom because I used px based media queries on it a year ago;

EDIT: In order to make the menu expand on click and to make the number of menu elements shown vary with screen width, I have changed the structure a bit more:

<nav id='menu'>
    <a tabindex=1 class='ctrl' href='#'>+ MORE +</a>
    <ul>
        <li><a href='#' class='menu-link'>Home</a></li>
        <li><a href='#' class='menu-link'>Options</a></li>
        <li><a href='#' class='menu-link'>Settings</a></li>
        <li><a href='#' class='menu-link'>Tools</a></li>
        <li><a href='#' class='menu-link'>Preferences</a></li>
        <li><a href='#' class='menu-link'>Edit</a></li>
    </ul>
</nav>

And also changed the CSS a bit:

#menu .ctrl { float: right; }
#menu ul, #menu li { margin: 0; padding: 0; list-style: none; }
#menu ul { overflow: auto; }
#menu li { float: left; }
#menu li:nth-last-child(-n+5) { display: none; }
#menu a {
    padding: 0.5em;
    text-decoration: none;
    border-right: 1px solid #fff;
    font-size: 110%;
}
#menu li a { display: block; }
#menu li:first-child a { border-left: 1px solid #fff; }
#menu .ctrl:focus, #menu .ctrl:active { display: none; outline: 0; }
#menu .ctrl:focus ~ ul li, #menu .ctrl:active ~ ul li { display: block; }

@media (min-width: 15em) {
    #menu li:nth-child(2) { display: block; }
}
@media (min-width: 20em) {
    #menu li:nth-child(3) { display: block; }
}
@media (min-width: 25em) {
    #menu li:nth-child(4) { display: block; }
}
@media (min-width: 30em) {
    #menu .ctrl ~ ul li { display: block; }
    #menu .ctrl { display: none; }
}

demo

(I've also added a background with vertical lines at every 5em just to make it clear how wide the screen is when resizing the browser window)

This method should work without JavaScript - tested that in desktop browsers, Opera Mobile, Android browser and iOS Safari. I don't know about Opera Mini though - I'll have to test that.

EDIT#2: No, it doesn't work in Opera Mini for me (the menu is collapsed, but clicking the + MORE + link does not expand it). Tried to make it work with JavaScript (no library), but that also doesn't work in Opera Mini (though it works on desktop browsers).

EDIT#3: Also tried to do the same thing using jQuery. This time it also works in Opera Mini. Really slow (at least for me), but it works. This is what I used:

$('.ctrl').click(function() {
    $(this).css({'display': 'none'}).next().children().css({'display': 'block'});
});

EDIT#4: Now tried the :target method - demo (also CSS-only). Works fine on my laptop using Chrome (haven't tested in another desktop browser), doesn't work in Opera Mini (menu is collapsed, clicking the + MORE + link does not expand it). Works in Opera Mobile though.

Upvotes: 7

Related Questions