I recently set myself the challenge of building a CSS-only menu (including sub-menu dropdowns) for a WordPress site. Why? Partly as an academic exercise, but partly in an attempt to reduce what appeared to be a too-busy combination of CSS, JS and PHP in the site and theme as it stood.
All this of course in contravention of the well-established KISS principle, but hey … we’re in lockdown and it’s good to have something to occupy the little grey cells.
One thing I soon discovered is that if you need a dropdown menu, you’ll be relying on the so-called “CSS Checkbox Hack”. The term “hack” isn’t quite fair, because the HTML and CSS used are perfectly legitimate and have valid use cases of their own. It’s just that there’s a no-doubt unintended way to use this particular feature of CSS.
There are scores of explanations of the checkbox hack on the web, so I’ll be brief. The idea is that you set up your HTML like this:
<label for="menu-toggle" class="menu-toggle-label">Menu</label>
<input type="checkbox" id="menu-toggle" class="menu-toggle">
<ul class="menu">
<li>Blah</li>
</ul>
The label will be visible. The checkbox has to be hidden because it’s only there to trigger the action we need (and that’s the “hack”). The <ul> will initially be invisible.
Now, in HTML, if you click the label for a checkbox, it will toggle the checkbox’s “checked” state. And we can use that as a selector in CSS to drive a change in another element – for example to change the visibility of that <ul>. Here’s the relevant part of the CSS:
.menu, .menu-toggle {
display: none;
}
.menu-toggle:checked + ul {
display: block;
}
That second rule says that when menu-toggle is checked, find an adjacent <ul> and make it visible. Simple enough. So using the checkbox’s label effectively as a button causes the <ul> to appear and disappear.
The next challenge was to find a way to arrange WP’s menu HTML with the necessary labels and checkboxes. I think the only way to do this is to generate the HTML yourself, and the usual suggestion is to create an extension to the Walker_Nav_Menu class, where you can set up your own start/end HTML for the items and levels in the menu.
But that seemed like a tad too much complexity, so I went for a custom approach based on WP’s wp_get_nav_menu_items function. This brings back a sorted list of the menu items, each with a value defining its parent item, so you can work out what the submenu hierarchy is. By the way, I’d not realised until using this that the menu items are saved as posts in WP’s database.
The first bit of code here is where we grab the menu items and create an array of objects that reflect the hierarchy.
$menu_items = wp_get_nav_menu_items('primary-menu');
// Construct menu hierarchy
$menu = [];
foreach ($menu_items as $index=>$menu_item) {
$parent = $menu_item->menu_item_parent;
$menu_obj = new stdClass();
$menu_obj->url = $menu_item->url;
$menu_obj->title = $menu_item->title;
if ($parent) { // Add submenu to parent object
$menu[$lvl_index]->submenus[] = $menu_obj;
} else { // Make new parent object
$menu[$index] = $menu_obj;
$menu[$index]->submenus = [];
$lvl_index = $index;
}
}
We end up with an array of objects for the top-level menu items, each with an array of its submenu objects. Armed with this, we can generate the HTML. In my case, it looked like this. It’s a pretty straightforward iteration through the array and sub-menu arrays, shoving those labels and checkboxes in where necessary.
echo '<nav class="nav-primary">';
echo '<label for="menu-toggle" class="menu-toggle-label top-menu-toggle-label">Menu</label>';
echo '<input type="checkbox" id="menu-toggle" class="menu-toggle">';
echo '<ul class="menu menu-primary">';
foreach ($menu as $index=>$menu_item) {
$has_submenu = count($menu_item->submenus);
$classes = 'menu-item '.'menu-item'.$index;
if ($has_submenu) $classes .= ' menu-item-has-children';
echo '<li class="',$classes,'">';
echo '<a class="menu-link" href="',$menu_item->url,'">';
echo '<span>',$menu_item->title,'</span>';
echo '</a>';
if ($has_submenu) {
echo '<label for="menu-toggle-',$index,'" class="menu-toggle-label">+</label>';
echo '<input type="checkbox" id="menu-toggle-',$index,'" class="menu-toggle">';
echo '<ul class="sub-menu">';
foreach ($menu_item->submenus as $submenu_item) {
echo '<li class="menu-item submenu-item">';
echo '<a class="menu-link submenu-link" href="',$submenu_item->url,'">';
echo '<span>',$submenu_item->title,'</span>';
echo '</a>';
echo '</li>';
}
echo '</ul>';
}
echo '</li>';
}
echo '</ul>';
echo '</nav>';
And hey, it works. But I decided against using it.
A few reasons for that.
First – it uses an unintended consequence of a CSS feature, and it puts checkboxes into a page that no user will ever see. That’s not what checkboxes are for.
Second – I still hold to the principle that CSS is for styling and JS is for functionality. This approach blurs that line.
Third – a JS approach allows me to do more with the HTML, like adding classes to dropdowns when they are visible … I can’t see a way to do this with CSS alone.
So all-in-all, a good exercise with some useful lessons.