Fully Dynamic Hierarchical Nav Menu Template

Problem

You want to output a navigation menu that automatically lists a Structure Section’s page hierarchy, with various CSS classes that indicate current page, current section, has children, etc.

Solution

The Craft docs have a basic example of a hierarchical nav menu, but usually you want to add various CSS classes that indicate certain things about each page in the menu so you can style them appropriately. Here is a template that shows you how to add such classes.

<ul class="nav-list nav-list--level-1">
    {% set homeIsCurrent = (entry.section.handle == 'homepage') %}
    <li class="nav-item nav-item--home nav-item--level-1 {% if homeIsCurrent %}nav-item--current{% endif %}">
        <a href="{{ siteUrl }}">Home</a>
    </li>
    
    {% nav page in craft.entries.section('pages') %}
        {% set pageIsCurrent = (page.id == entry.id) %}
        {% set pageIsTopParent = (craft.request.firstSegment == page.uri) %}
        {% set pageIsInPath = (entry.uri matches '{^' ~ page.uri ~ '}') %}
        
        {% set classes = ['nav-item--level-' ~ page.level] %}
        {% set classes = classes|merge([pageIsCurrent ? 'nav-item--is-current' : '']) %}
        {% set classes = classes|merge([pageIsTopParent ? 'nav-item--is-top-parent' : '']) %}
        {% set classes = classes|merge([pageIsInPath ? 'nav-item--is-in-path' : '']) %}
        {% set classes = classes|merge([page.hasDescendants ? 'nav-item--has-children' : '']) %}
        {% set classes = classes|join(' ') %}

        <li class="{{ classes }}">
            <a href="{{ page.url }}">
                {{ page.title }}
                {% if page.hasDescendants %}<span class="nav-toggle nav-toggle--level-{{ page.level }}">&#9660;</span>{% endif %}
            </a>
            {% ifchildren %}
                <ul class="nav-list nav-list--level-{{ page.level + 1 }}">
                    {% children %}
                </ul>
            {% endifchildren %}
        </li>
    {% endnav %}
</ul>

Some things to note:

  • Make sure you replace the ‘pages’ argument in craft.entries.section('pages') with the handle of your own structure section.
  • You can of course set whatever CSS classes you want, but I am using nav-list, nav-item prefixes to denote the ul and li elements.
  • You can change the actual CSS class names that get output, by altering the strings in the various {% set classes = ... %} lines.
  • pageIsCurrent means that the nav item is the page currently being viewed.
  • pageIsTopParent means that the nav item is a top-level page in the given section that is “above” the page currently being viewed (that is, it is the top-level menu item that the currently-viewed page lives under).
  • pageIsInPath means that the nav item is a parent at any level (direct parent, grandparent, great-grandparent, etc.) of the page currently being viewed. Note that it will also be true for the page currently being viewed itself.
  • All that “set classes” junk is kind of ugly, but it does keep a lot of logic out of the html itself, so in my opinion it is slightly cleaner than littering the actual css attribute of the html tags with a bunch of twig if statements.
  • If you don’t want the homepage link in your nav menu, just delete that first <li> for it (along with the isHome variable setting line above it).