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.
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 horizontallysubmenu
identifies item lists below menu container itemsmenuitem
is used to mark the clickable items of the menuexpandable
provides right- and down-arrows for container itemsThis 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 ...
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?
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.
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.
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 validThe 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.
/* 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.
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 alldiv
containers that have aul
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 | <!DOCTYPE html> |
ɔ⃝ Fritz Ritzberger, 2015-07-11