Jump to content
Beluga

CSS-only responsive multi-level menu

Recommended Posts

So this is basically a recreation of a menu tutorial from W3Bits, tweaked to include the Advanced checkbox hack.

Demo.

Even the Advanced hack itself was tweaked: apparently this bit is causing issues with Safari, so I removed it:

@-webkit-keyframes bugfix { from {padding:0;} to {padding:0;} }
I found this particular configuration to work quite nicely. A previous menu I tried had a problem with the menu items staying expanded between media query breakpoints, when resizing the browser.

Below is the CSS for the menu. You will notice that it is mobile-first:

/* Menu from http://w3bits.com/css-responsive-nav-menu/ */
/* Note: the tutorial code is slightly different from the demo code */

.cf:after { /* micro clearfix */
  content: "";
  display: table;
  clear: both;
}

body {
  -webkit-animation: bugfix infinite 1s;
   }

#mainMenu {
    margin-bottom: 2em;
}

#mainMenu ul {
  margin: 0;
  padding: 0;
}

#mainMenu .main-menu {
  display: none;
}

#tm:checked + .main-menu {
  display: block;
}

#mainMenu input[type="checkbox"],
#mainMenu ul span.drop-icon {
  display: none;
}

#mainMenu li,
#toggle-menu,
#mainMenu .sub-menu {
  border-style: solid;
  border-color: rgba(0, 0, 0, .05);
}

#mainMenu li,
#toggle-menu {
  border-width: 0 0 1px;
}

#mainMenu .sub-menu {
  background-color: #444;
  border-width: 1px 1px 0;
  margin: 0 1em;
}

#mainMenu .sub-menu li:last-child {
  border-width: 0;
}

#mainMenu li,
#toggle-menu,
#mainMenu a {
  position: relative;
  display: block;
  color: white;
  text-shadow: 1px 1px 0 rgba(0, 0, 0, .125);
}

#mainMenu,
#toggle-menu {
  background-color: #09c;
}

#toggle-menu,
#mainMenu a {
  padding: 1em 1.5em;
}

#mainMenu a {
  transition: all .125s ease-in-out;
  -webkit-transition: all .125s ease-in-out;
}

#mainMenu a:hover {
  background-color: white;
  color: #09c;
}

#mainMenu .sub-menu {
  display: none;
}

#mainMenu input[type="checkbox"]:checked + .sub-menu {
  display: block;
}

#mainMenu .sub-menu a:hover {
  color: #444;
}

#toggle-menu .drop-icon,
#mainMenu li label.drop-icon {
  position: absolute;
  right: 0;
  top: 0;
}

#mainMenu label.drop-icon, #toggle-menu span.drop-icon {
  padding: 1em;
  font-size: 1em;
  text-align: center;
  background-color: rgba(0, 0, 0, .125);
  text-shadow: 0 0 0 transparent;
  color: rgba(255, 255, 255, .75);
}

label {
  cursor: pointer;
  user-select: none;
}

@media only screen and (max-width: 64em) and (min-width: 52.01em) {
  #mainMenu li {
    width: 33.333%;
  }

  #mainMenu .sub-menu li {
    width: auto;
  }
}

@media only screen and (min-width: 52em) {
  #mainMenu .main-menu {
    display: block;
  }

  #toggle-menu,
  #mainMenu label.drop-icon {
    display: none;
  }

  #mainMenu ul span.drop-icon {
    display: inline-block;
  }

  #mainMenu li {
    float: left;
    border-width: 0 1px 0 0;
  }

  #mainMenu .sub-menu li {
    float: none;
  }

  #mainMenu .sub-menu {
    border-width: 0;
    margin: 0;
    position: absolute;
    top: 100%;
    left: 0;
    width: 12em;
    z-index: 3000;
  }

  #mainMenu .sub-menu,
  #mainMenu input[type="checkbox"]:checked + .sub-menu {
    display: none;
  }

  #mainMenu .sub-menu li {
    border-width: 0 0 1px;
  }

  #mainMenu .sub-menu .sub-menu {
    top: 0;
    left: 100%;
  }

  #mainMenu li:hover > input[type="checkbox"] + .sub-menu {
    display: block;
  }
}
Below is the markup outputted using mindplay.dk's method. I found it impossible to output with MarkupSimpleNavigation or MenuBuilder.

The homepage is added as the first top-level item. Notice the onclicks that make it work on iOS < 6.0. The clearfix class cf for the top ul is important. Otherwise the element will have no height (got bitten by this..).

<nav id="mainMenu">
    <label for='tm' id='toggle-menu' onclick>Navigation <span class='drop-icon'>▼</span></label> <input id='tm' type='checkbox'>

    <ul class='main-menu cf'>
      <?php
        /**
         * Recursive traverse and visit every child in a sub-tree of Pages.
         *
         * @param Page $parent root Page from which to traverse
         * @param callable $enter function to call upon visiting a child Page
         * @param callable|null $exit function to call after visiting a child Page (and all of it's children)
         *
         * From mindplay.dk 
         */
        echo '<li><a href="' . $pages->get(1)->url . '">Home</a></li>';
        function visit(Page $parent, $enter, $exit=null)
        {
            foreach ($parent->children() as $child) {
                call_user_func($enter, $child);
                
                if ($child->numChildren > 0) {
                    visit($child, $enter, $exit);
                }
                
                if ($exit) {
                    call_user_func($exit, $child);
                }
            }
        }
        visit(
          $pages->get(1)
          ,
          function(Page $page) {
              echo '<li><a href="' . $page->url . '">' . $page->title;
              if ($page->numChildren > 0) {
                  echo '<span class="drop-icon">▼</span>
                        <label title="Toggle Drop-down" class="drop-icon" for="' . $page->name . '" onclick>▼</label>
                        </a>
                        <input type="checkbox" id="' . $page->name . '"><ul class="sub-menu">';
              } else {
                echo '</a>';
              }
          }
          ,
          function(Page $page) {
              if ($page->numChildren > 0) {
                  echo '</ul>';
              }
              echo '</li>';
          }
        );
        ?>
    </ul>
  </nav>
Edit: fixed the end part, thanks er314.
  • Like 12

Share this post


Link to post
Share on other sites

Thanks a lot, this menu system is really performing well.

I think there's a small mistake at the end of the template code ; it is auto-fixed by most browsers, but not by all. The very last block should be :
 

          function(Page $page) {
              if ($page->numChildren > 0) {
                  echo '</ul>';
              }
              echo '</li>';
          }
  • Like 1

Share this post


Link to post
Share on other sites

Thank you!

I've implemented it very recently.

Apparently, the only thing that is missing - but this is often missing (I had noticed it on the Foundation profile) - is the possibility (perhaps with a javascript/jquery script...) to change, in my case, from

#mainMenu .sub-menu .sub-menu {left: 100%; top: -1px;}
to
#mainMenu .sub-menu .sub-menu {left: -101%; top: -1px;}

to revert the direction when the third-level ul touches the right side (edge) of the browser window, otherwise, in some cases, a (big) part of it can be hidden.

Share this post


Link to post
Share on other sites

Nothing is impossible. This is quite easy with MarkupSimpleNavigation:

$tree = $modules->MarkupSimpleNavigation;

$treeOptions = array(
    "show_root" => true,
    "outer_tpl" => "<ul class='main-menu cf'>||</ul>",
    "inner_tpl" => "<ul class='sub-menu'>||</ul>",
    );

$tree->addHookAfter("getItemString", null, function ($event){
    $child = $event->arguments("page");
    if($child->id != 1 && $child->numChildren(true)){
       $itemStr = "<a href='{$child->url}'>{$child->title}
                    <span class='drop-icon'>▼</span>
                    <label title='Toggle Drop-down' class='drop-icon' for='tab_{$child->name}' onclick>▼</label>
                </a><input type='checkbox' id='tab_{$child->name}'>";
       $event->return = $itemStr;
    }
});

$menuStr = $tree->render($treeOptions);

<nav id="mainMenu">
    <label for='tm' id='toggle-menu' onclick>Navigation <span class='drop-icon'>▼</span></label> <input id='tm' type='checkbox'>
    <?php echo $menuStr;?>
</nav>
  • Like 6

Share this post


Link to post
Share on other sites

Has anyone been able to make this work with kongondo's MenuBuilder? With MarkupSimpleNavigation it works like a charm but I want the client to have more control of the menu from the admin.

Share this post


Link to post
Share on other sites

Has anyone been able to make this work with kongondo's MenuBuilder? With MarkupSimpleNavigation it works like a charm but I want the client to have more control of the menu from the admin.

MenuBuilder menu's are created from a JSON string that has info about each menu item: title, page_id, parent_id, etc...Easiest way to achieve this is to grab that JSON string, convert it into an array and use whatever recursive method tickles your fancy :-). Now that I think about it, I'll create a method in MB that just returns the JSON string or its array depending on the arguments you supply. That way you can have your cake and eat it too...like this guy did... :)

  • Like 2

Share this post


Link to post
Share on other sites

Thanks, but using JSON how can I pass the information that the inner ul should have a class "sub-menu"?

<?php
	
$json =  $pages->get(1022)->menu_items;
$items = json_decode($json, true);

if( count($items) > 0 ){
  $out .= '<ul class="class="main-menu cf">';
  foreach($items as $item){
    $url = ($item['url']) ? $item['url'] : $pages->get($item['pages_id'])->url;
    $target = (1 == $item['newtab']) ? 'target="_blank"':'';
    $out .= '<li class="list-item"><a href="' . $url . '" class="list-item-link" ' . $target . '>' . $item['title'] . '</a></li>';
  }
  $out .= '</ul>';
}
?>

<nav id="mainMenu">
    <label for='tm' id='toggle-menu' onclick>Navigation <span class='drop-icon'>▼</span></label> <input id='tm' type='checkbox'>
    <?php echo $out;?>
</nav>

Share this post


Link to post
Share on other sites

Below is the markup outputted using mindplay.dk's method. I found it impossible to output with MarkupSimpleNavigation or MenuBuilder.

Thanks, but using JSON how can I pass the information that the inner ul should have a class "sub-menu"?

I have now added a method to MarkupMenuBuilder that makes this quite easy, thanks to your challenge :-).

@Beluga, I have used your example to make a demo using Menu Builder, thanks.

@Peter, you will find full examples in Menu Builder's support forum here.

  • Like 5

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By louisstephens
      Is it possible to use count() to return a number of repeater items don't have a checkbox checked? In my current set up, I have a repeater on the page "dev_repeater" with a checkbox called "dev_repeater_exclude". I need to get a count of the current items that do not have it checked so I can pass it to my css grid to alter the column width.
    • By Chris Bennett
      Not sure where I originally saw it while lurking around the forums, but someone, somewhere at some time was asking about styling Uikit checkboxes as toggle-style switches, much like the ones at the bottom of this post asking me if I want to be notified of replies.
      So here is my humble offering, rough and ready,  which can be thrown in at the bottom of your Uikit css as a starting point.
      Everything is based on ems and rems, so you can increase size as you desire by altering the single instance of font-size.
      It only targets single instances of labels within a specific field to a) try to limit unintended consequences and b) because in those cases it often seems more user-friendly to have an on/off binary switch rather than a checkbox. It's still totally a checkbox, just styled differently.
       
      .uk-form-controls-text label:only-of-type input.uk-checkbox { font-size:.8rem; margin-top:0; position:relative; -webkit-appearance:none; outline:none; width:4em; height:2.4em; border:2px solid #D6D6D6; border-radius: 3em; box-shadow:inset 5em 0 0 0 #DDD; flex-shrink: 0; } .uk-form-controls-text label:only-of-type input.uk-checkbox:after { content:""; position:absolute; top:.25em; left:.25em; background:#FFF; width:1.6em; height:1.6em; border-radius:50%; transition:all 250ms ease 20ms; box-shadow:.05em .25em .5em rgba(0,0,0,0.2); } .uk-form-controls-text label:only-of-type input.uk-checkbox:checked { background-color: transparent; box-shadow:inset 5em 0 0 0 #4ed164; border-color:#67bba5; } .uk-form-controls-text label:only-of-type input.uk-checkbox:checked:after { left:1.85em; box-shadow:0 0 1em rgba(0,0,0,0.2); } label:only-of-type input.uk-checkbox:checked + span { color:#008a00; transition:all 250ms ease 20ms; } .InputfieldCheckbox .InputfieldContent label:only-of-type {display:flex;} label:only-of-type input.uk-checkbox + span { color:#c3c3c3; display:flex; align-items:center; line-height:1.1; } /* Below is only necessary if you want the optional "tick" after items when selected */ label:only-of-type input.uk-checkbox + span:after { flex-shrink:0; margin-left:.5em;width:2em; opacity:0; content:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 250 250'%3E%3Ccircle cx='125' cy='125' r='125' fill='%23231F20' opacity='.1'/%3E%3Cpath fill='%230B8B44' d='M95.823 139.432l-32.492-32.56-31.872 31.883-.008-.008 63.72 63.732L218.549 79.116 187.494 47.52z'/%3E%3C/svg%3E"); } label:only-of-type input.uk-checkbox:checked + span:after { opacity:1; transition: opacity 250ms ease 150ms; }  

    • By Robin S
      Checkbox Reversed
      Modifies InputfieldCheckbox so that it shows the reverse of its true value. The checkbox will be unchecked when the field value is 1 and checked when the field value is not 1.
      Background
      The core FieldtypeCheckbox does not have a setting that allows a checkbox to be checked by default. One reason for this is that only a checked field saves a value to the database. An unchecked field does not save "0" to the database, but rather does not save any value for the field at all. Therefore there is no way to distinguish between a new field that has not yet been saved (and therefore could potentially get a default checked state) and a field that has deliberately been saved as unchecked.
      Because of this you sometimes have to use a checkbox in the opposite way than you would like. Suppose your client has requested a checkbox labelled "Bootylicious" that will be checked by default. This isn't possible with FieldtypeCheckbox so instead you have to convince them that a checkbox labelled "Not bootylicious" that is unchecked by default is just as good. This alternative will achieve the same thing, but it's not ideal.
      A solution
      This module doesn't change the limitations of the core checkbox field, but it provides a workaround that allows you to show the checkbox with the desired default state and label. So in the example above you would still name the field "not_bootylicious" (otherwise it could get confusing in your template files) but you can label the field "Bootylicious" and the checkbox will appear checked when its true value is actually unchecked, and vice versa. This allows new pages to show the checkbox checked by default. Clear as mud?
      Usage
      Install the Checkbox Reversed module.
      For any Checkbox field where you want the inputfield to show the reverse of its true value, activate the "Reverse the checked state of this inputfield?" option in the field settings.

       
      https://github.com/Toutouwai/CheckboxReversed
      http://modules.processwire.com/modules/checkbox-reversed/
    • By suntrop
      Hi all! I want to sort child-pages by two fields but I don't know if it is possible within one selector or if I have to make two separate queries
      <?php foreach ($page->children('sort=checkbox, sort=-number') as $child): ?> I want to keep 'checked' pages on top and sort them and all following by the 'number' field. The 'checkbox' field is a 
    • By jploch
      Hi! 
      this should be easy, but I can't get it to work.
      I have a repeater with events.
      Every event has an option-field with multiple checkboxes.
      To filter the events on the frontend I have a script, that uses the class names to filter the results (an event can have multiple categories).

      Now I just want to add every checked option (title) to use as my class name.
      This is what I have so far, wich only gets the first title.
      foreach($page->events as $event) { $tags = $event->options->title; echo "<div class='size1of2 {$tags}'>"; echo "<img src='{$event->image->url}'>"; echo "{$event->text_editor}"; echo "</div>"; }
      I know there is an example with a foreach, but how would I use it in this context?
      foreach($page->countries as $country) { echo "<li>$country->title</li>"; }
       
×
×
  • Create New...