Blog-Archiv

Samstag, 11. Juli 2015

Pure CSS Menu

Is the "Pure CSS Menu" a myth? Obviously not, when you look at the web. Pure CSS menus are those that open when you hover some navicon , or move the mouse over some menu bar. No intentional click needed, it is there and jumps into your eye everytime you move over by chance.

CSS is a rule-based language. CSS provides no variables, no constants, no functions, no arithmetic operations, not even means for structured programming to avoid code duplications. And you can not receive events in CSS like you can in JavaScript. So how can a menu be implemented using CSS only, without JavaScript?

You can set rules about states, and then change styles when these states are met. CSS pseudo-classes are the means for such. For example, the pseudo-class :focus gets active when you click on a button, or :hover when you move the mouse over some element. That's the way you receive events in CSS.


This Blog is about my experience with "Pure CSS" solutions. I like the CSS-Tricks page a lot, not because of CSS, but because things are explained there in a way that you can understand. Programming CSS means sitting hours and hours. Especially behavioural applications like menus result in code that can be understood by experts only. I will try to expose it also for non-experts here.


Menu as a List

Here is the HTML part of the "Pure CSS Menu", with no CSS yet.

<!DOCTYPE html>

<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  
  <title>Pure CSS Menu</title>
  
</head>

<body>

  <ul class="menubar">
    <li><a class="menuitem expandable" href="#">File</a>
      <ul class="submenu">
        <li>
          <a class="menuitem expandable" href="#">Open</a>
          <ul class="submenu">
            <li><a class="menuitem" href="#">In This Window</a></li>
            <li><a class="menuitem" href="#">In New Window</a>
          </ul>
        </li>
        <li>
          <a class="menuitem expandable" href="#">Save</a>
          <ul class="submenu">
            <li>
              <a class="menuitem expandable" href="#">As</a>
              <ul class="submenu">
                <li><a class="menuitem" href="#">Draft</a></li>
                <li><a class="menuitem" href="#">Published</a></li>
                <li><a class="menuitem" href="#">Final</a></li>
              </ul>
            </li>
            <li>
              <a class="menuitem expandable" href="#">To Server ...</a>
              <ul class="submenu">
                <li><a class="menuitem" href="#">Tick</a></li>
                <li><a class="menuitem" href="#">Trick</a></li>
                <li><a class="menuitem" href="#">Track</a></li>
              </ul>
            </li>
          </ul>
        </li>
        <li><a class="menuitem" href="#">Close</a></li>
      </ul>
    </li>
    <li>
      <a class="menuitem expandable" href="#">Edit</a>
      <ul class="submenu">
        <li><a class="menuitem" href="#">Cut</a></li>
        <li><a class="menuitem" href="#">Copy</a></li>
        <li><a class="menuitem" href="#">Paste</a></li>
      </ul>
    </li>
    <li>
      <a class="menuitem" href="#">Search</a>
    </li>
    <li>
      <a class="menuitem" href="#">View</a>
    </li>
    <li>
      <a class="menuitem expandable" href="#">Help</a>
      <ul class="submenu">
        <li><a class="menuitem" href="#">Content</a></li>
        <li><a class="menuitem" href="#">Index</a></li>
      </ul>
    </li>
  </ul>

</body>
</html>

I have put CSS classes onto all elements that play a role in controlling this menu. That hopefully will avoid wasting other ul elements on the page.

  • menubar will serve to make the top-level menu horizontally
  • submenu identifies item lists below menu container items
  • menuitem is used to mark the clickable items of the menu
  • expandable provides right- and down-arrows for container items

This now should become a menu-bar on top of the page, looking like the thing below. Try hovering the menu bar, and hover into the opening sub-menus.

If you look through this, you see that ...

  • the sub-menu in "File" - "Save" - "As" covers parts of the menu item below it
  • a click on a menu item won't close the menu

Bugs I could not solve. You spend hours and hours solving such issues. For example, moving the "Help" menu item to the right of the menu-bar seems to be impossible :-(
Mind that I do not belive in such solutions. I just present the results of my CSS experiments.

So how was this done?

Menubar Layout

This menu uses primarily display and position properties for layout.
I will evolve the style element now step by step. First is the menubar. This top-level list we need to make horizontal, because an HTML ul element is vertical by default.

The following style element should be placed in the head of the HTML page.

  <style type="text/css">
  
    /* Menu layout */
    
    ul.menubar, ul.submenu {
      list-style-type: none; /* no bullets on list menu-items */
      padding: 0;            /* no indentation of menu-items */
      position: absolute;    /* don't let opening submenus push down parts of the menubar */
    }
    ul.menubar {             /* locate the menubar to top of its relative parent */
      top: 0;
    }
    ul.menubar > li {        /* top-level menu-items are horizontal */
      display: inline-block;
    }

The position: absolute property, together with top: 0, places the menu-bar to the top of its parent element. The parent will be the one that has no position: static (this is default), falling back to the document's body. So when you want to place the menu-bar into some parent element (like I did for the live-demo above), you need to put it into a div with position: relative.

The position: absolute also lifts it in z-direction above other content, without use of the z-index property. But the other content will not know it at all, so you need to add some space after the menu bar to avoid it overlapping content below.

The ul.menubar > li selects li elements directly below the menu-bar. The display: inline-block directive flattens the menu-bar ul items horizontally.

All selectors are headed by ul.menubar to be as specific as possible, and not mix into non-menu elements in the HTML page.

Submenu Layout


    ul.menubar .menuitem {   /* basic styling of menuitem */
      display: block;        /* keeps opening sub-menus below trigger item */
      white-space: nowrap;   /* do not allow line breaks in menuitem labels */
    }
    ul.menubar .submenu .menuitem { /* override for sub-menu items */
      display: inline-block; /* make sub-menu display to the right, not below */
    }

Here the top-level menu-items are styled to display their successor submenu below (display: block). This selector is then over-ruled by the next selector (because it is more specific = has more selector parts). This over-rule expresses that a menu-item within a sub-menu makes its successor submenu display to the right (display: inline-block).

Mind that the HTML default display: block for li items is valid here, it was set to display: inline-block just for the li items directly below the menu-bar. So it should be sure that items on same level always display below each other.

This was the most complicated part: the layout. Now I need to find an "CSS event" to set the sub-menus visible.

How to Receive Events in CSS


    ul.menubar .submenu {
      display: none;         /* hide menu items in sub-menus ... */
    }
    ul.menubar .menuitem:hover + .submenu, ul.menubar .submenu:hover {
      display: inline-block; /* ... but display when their trigger item is hovered */
    }

So here I define the same selector for two different states. The first state is valid when the mouse is not over some item. The second is when it is hovers one. This is the way how to receive events in CSS. Pseudo-classes make it possible.

  • :hover takes effect on an element when the mouse is over it
  • :active is true for an element and all its parents while the mouse is pressed on the element (applies both focusable and other elements)
  • :focus is true when a focusable element was clicked, and gets false as soon as another element is clicked
  • :target gets valid on the link-target element when a link element (a) was clicked, and lost as soon as another link was targeted
  • :checked is there when a checkbox was toggled to true, and absent when it is false
  • :valid strikes when an input field's value becomes valid

The selector ul.menubar .submenu declares that sub-menus are basically invisible: display: none. The selector ul.menubar .menuitem:hover + .submenu, ul.menubar .submenu:hover then declares that a sub-menu element should be visible when it is successor of a hovered menuitem element, or when itself is hovered. It sets the visibility to inline-block to display the menu to the right of its HTML predecessor (that also must have inline-block). For the top-level menus this is prevented by display: block of the triggering menu-item, so these are displayed below their triggers.

That was about the visibility of the menu. The remaining things are decorative. The hard work was the layout.

Colors, Borders, Paddings, Arrows ...


    /* Expand control arrows */
    
    ul.menubar .expandable::after {
      content: '\000A0\000A0\025BE'; /* two spaces and arrow down */
    }
    ul.menubar .submenu .expandable::after {
      content: '\000A0\000A0\025B8'; /* two spaces and arrow right */
    }
    
    /* Menu colors, borders ... */
    
    .menubar {
      width: 100%;
    }
    .menubar, .submenu {
      background-color: gray;
      border-top: 1px solid black;
      border-left: 1px solid black;
    }
    .menuitem {
      color: white;
      text-decoration: none;
      padding: 0.3em;
      /* width: 8em;            avoid open sub-menus cover parts of the menu below */
    }
  </style>

The arrows are set using pseudo-elements. These are written with two colons, ::after. They generate artificial elements before or after an selected element, in this case any element with class expandable.

One last remark about the menu-item width:

/* width: 8em;            avoid open sub-menus cover parts of the menu below */

This can workaround the fact that sub-menus might obscure items below. When you define a width for all menu-items, this won't occur anymore. But mind that you must choose a width that all items fit into! (Might be a PITA :-)

So if you now assemble all four CSS fragments into the head-element of above HTML, you can hack on the menu until it satifies your needs. You can find the full source on bottom of this page, or go to my homepage to visit its current state.

Résumé

As you might have noticed, I am not a big fan of CSS. I prefer JavaScript solutions. That does not mean I do not use CSS (could anyone afford that?). But I use it from JavaScript, for the sake of once-and-only-once.

There are lots of CSS weaknesses. Have you ever tried to express

"put a red border onto all div containers that have a ul within them"

in CSS? You can't. A CSS selector will always target the last, rightmost element. So you can set a style property onto a selected element below some parent, but not onto the parent above a selected child.

CSS is not suited for layout tasks. In layout you need to define the relationships between the components inhabiting a container. How do this without variables and assignments?

Browsers were built to display text flows with images, not imitate desktop applications. But this is what they need to do nowdays. The well-known layouts of desktop-applications are not present in a CSS engine.


Click to see complete source.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<!DOCTYPE html>

<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  
  <title>Pure CSS Menu</title>
  
  <style type="text/css">
  
    /* Menu layout */
    
    ul.menubar, ul.submenu {
      list-style-type: none; /* no bullets on list menu-items */
      padding: 0;            /* no indentation of menu-items */
      position: absolute;    /* don't let opening submenus push down parts of the menubar */
    }
    ul.menubar {             /* locate the menubar to top of its relative parent */
      top: 0;
    }
    ul.menubar > li {        /* top-level menu-items are horizontal */
      display: inline-block;
    }
    
    ul.menubar .menuitem {   /* basic styling of menuitem */
      display: block;        /* keeps opening top-menus below menu-bar */
      white-space: nowrap;   /* do not allow line breaks in menuitem labels */
    }
    ul.menubar .submenu .menuitem { /* override for sub-menu items */
      display: inline-block; /* make sub-menu display to the right, not below */
    }
    
    ul.menubar .submenu {
      display: none;         /* hide menu items in sub-menus ... */
    }
    ul.menubar .menuitem:hover + .submenu, ul.menubar .submenu:hover {
      display: inline-block; /* ... but display when their trigger item is hovered */
    }
    
    /* Expand control arrows */
    
    ul.menubar .expandable::after {
      content: '\000A0\000A0\025BE'; /* two spaces and arrow down */
    }
    ul.menubar .submenu .expandable::after {
      content: '\000A0\000A0\025B8'; /* two spaces and arrow right */
    }
    
    /* Menu colors, borders ... */
    
    .menubar {
      width: 100%;
    }
    .menubar, .submenu {
      background-color: gray;
      border-top: 1px solid black;
      border-left: 1px solid black;
    }
    .menuitem {
      color: white;
      text-decoration: none;
      padding: 0.3em;
      /* width: 8em;            avoid open sub-menus cover parts of the menu below */
    }
  </style>
</head>

<body>

  <ul class="menubar">
    <li><a class="menuitem expandable" href="#">File</a>
      <ul class="submenu">
        <li>
          <a class="menuitem expandable" href="#">Open</a>
          <ul class="submenu">
            <li><a class="menuitem" href="#">In This Window</a></li>
            <li><a class="menuitem" href="#">In New Window</a>
          </ul>
        </li>
        <li>
          <a class="menuitem expandable" href="#">Save</a>
          <ul class="submenu">
            <li>
              <a class="menuitem expandable" href="#">As</a>
              <ul class="submenu">
                <li><a class="menuitem" href="#">Draft</a></li>
                <li><a class="menuitem" href="#">Published</a></li>
                <li><a class="menuitem" href="#">Final</a></li>
              </ul>
            </li>
            <li>
              <a class="menuitem expandable" href="#">To Server ...</a>
              <ul class="submenu">
                <li><a class="menuitem" href="#">Tick</a></li>
                <li><a class="menuitem" href="#">Trick</a></li>
                <li><a class="menuitem" href="#">Track</a></li>
              </ul>
            </li>
          </ul>
        </li>
        <li><a class="menuitem" href="#">Close</a></li>
      </ul>
    </li>
    <li>
      <a class="menuitem expandable" href="#">Edit</a>
      <ul class="submenu">
        <li><a class="menuitem" href="#">Cut</a></li>
        <li><a class="menuitem" href="#">Copy</a></li>
        <li><a class="menuitem" href="#">Paste</a></li>
      </ul>
    </li>
    <li>
      <a class="menuitem" href="#">Search</a>
    </li>
    <li>
      <a class="menuitem" href="#">View</a>
    </li>
    <li>
      <a class="menuitem expandable" href="#">Help</a>
      <ul class="submenu">
        <li><a class="menuitem" href="#">Content</a></li>
        <li><a class="menuitem" href="#">Index</a></li>
      </ul>
    </li>
  </ul>

</body>

</html>


















Keine Kommentare: