Because Craft doesn't formally support one way to do navigation, there is a lot of flexibility in what you can do. Here are some ideas on how to code site navigation in Craft and Twig.
There isn’t an official way to manage navigation in Craft CMS. Unlike other CMSes that offer built-in navigation management, Craft CMS leaves it up to the developer to decide the best implementation for the website.
Let’s review five ways to do navigation in Craft CMS:
Craft CMS is design-agnostic and doesn’t impose any layout or design structure on a project. Craft focuses entirely on the content and not on the presentation. Because Craft doesn’t formally support one way to do navigation, there is a lot of flexibility in what you can do.
Recently on the Craft CMS Discord server, there was a discussion of how to do navigation and the input from different people significantly varied. You had those that mostly always implement the navigation manually via hard-coded nav items right in the Twig template. But, there were also those that did it half-hard-coded navigation, adding in some dynamic items when necessary. And then there were others who chimed in that all navigation should be fully dynamic and manageable from the Craft control panel.
There’s no wrong way, just the way that is best for your project. I’ve primarily used the hard-coded approach for my projects and a mix of everything for client or customer websites.
I don’t want the helpful discussion locked behind a Discord server that is not findable through web searches, so let’s document some of the most popular methods here. At the end of the article, I’ll link to additional ideas and reading on the topic.
Building navigation right in the HTML or Twig code is the simplest of all the implementations we’ll cover in this article. Furthermore, it is the one I most commonly use in my projects because I am both the developer and the client, so I can make this type of trade-off without many downsides.
Static navigation is a set-and-forget approach to navigation, and it comes with the assumption that the navigation will not change very often.
Here’s a snippet of what the CraftQuest navigation looked like until recently:
<a href="/courses" class="{% if craft.app.request.getSegment(1) == 'courses' %}tw-bg-gray-700 tw-text-white
{% else %}tw-text-gray-300 hover:tw-bg-gray-700 hover:tw-text-white{% endif %} tw-px-3 tw-py-2 tw-rounded-md tw-text-sm tw-font-medium">Courses</a>
<a href="/lessons" class="{% if craft.app.request.getSegment(1) == 'lessons' %}tw-bg-gray-700 tw-text-white
{% else %}tw-text-gray-300 hover:tw-bg-gray-700 hover:tw-text-white{% endif %} tw-px-3 tw-py-2 tw-rounded-md tw-text-sm tw-font-medium">Lessons</a>
<a href="/topics" class="{% if craft.app.request.getSegment(1) == 'topics' %}tw-bg-gray-700 tw-text-white
{% else %}tw-text-gray-300 hover:tw-bg-gray-700 hover:tw-text-white{% endif %} tw-px-3 tw-py-2 tw-rounded-md tw-text-sm tw-font-medium">Topics</a>
<a href="/articles" class="{% if craft.app.request.getSegment(1) == 'articles' %}tw-bg-gray-700 tw-text-white
{% else %}tw-text-gray-300 hover:tw-bg-gray-700 hover:tw-text-white{% endif %} tw-px-3 tw-py-2 tw-rounded-md tw-text-sm tw-font-medium">Articles</a>
While the navigation items are hard-coded with the name and the URL, I add in some dynamic checking of the current URL to highlight the navigation item when the visitor is in that section.
Note: In the remaining code examples, I’ll leave out the Tailwind classes in the name of simplicity.
A partially dynamic navigation is one that pulls in some navigation items via Element queries in Craft. A typical example of this is a drop-down that has additional subpages under the main navigation item.
On the CraftQuest site, I recently set this up on the Quests navigation item.
When you click on the navigation item is exposes a drop-down menu with the available Quests. Everything else in the navigation is still static and hard-coded in the Twig template.
In this implementation, I’m using an Element query to fetch all of the Quests, display the title and short description, and link it up with the entry URL. This approach allows me to keep the navigation simple and fast while making it dynamic exactly where it needs to be. So, if I add additional Quests entries to the channel section, those would appear here.
A dynamic section of a navigation can pull from any content source in Craft: a section, categories, tags, or any other element type.
<div class="tw-relative">
<button>Quests</button>
<div id="quests-menu">
<div>
<div>
{% for quest in craft.entries
.section('learningPathways')
.all()
%}
<a href="{{ quest.url }}"
class="{% if craft.app.request.getSegment(2) == quest.slug %}current{% endif %}">
<p>{{ quest.title }}</p>
<span class="tw-text-sm tw-text-gray-500">
{{ quest.pathwayShortDescription }}
</span>
</a>
{% endfor %}
<a href="/quests">
<p class="tw-text-base tw-font-medium tw-text-gray-100">See All Quests</p>
</a>
</div>
</div>
</div>
</div>
<a href="/courses">Courses</a>
<a href="/lessons">Lessons</a>
<a href="/topics">Topics</a>
<a href="/articles">Articles</a>
This simplified code snippet treats just the first navigation item differently, using an element query for the quests (the section handle is learningPathways
) and then listing them out in a drop-down. I’ve removed all of the Tailwind classes to make the code a bit more readable on its own.
After that dynamic navigation item, the remaining items are just the same as in the first part of the article: all hard-coded with a segment check to highlight it when you’re on that page or section of the site.
This implementation doesn’t require anything extra to manage inside of Craft. Instead, it uses existing content structures and data. If you need only some customization to your main navigation, then this is a good choice.
Now we’re getting to the navigation implementations that you can manage from within the Craft control panel. In this example, we create a Global Set in Craft and use it to build and manage the site navigation. The advantage here is that it allows someone, presumably the client or customer, to manage the navigation from the Craft control panel. This is helpful in some scenarios because it doesn’t require the intervention of a developer or technical staff member.
This implementation has the downside of straddling static and fully dynamic navigation, with limited control over child navigation items (like the drop-down we did in the Partially-Dynamic version above).
We can handle this navigation in a few ways because we can use any field or field type in a Global Set. For this version, let’s use a Table field and manage all navigation items as table rows.
The table will have two columns to start: Navigation Label, Navigation URL.
Each row will represent one navigation item. We won’t add any additional features to the table, but you could add columns like HTML classes and IDs, a lightswitch field to enable/disable functionality, a color, and more.
The Twig code to make this happen is a loop over the rows of the table in the Global Set. Since we have global access to it, we can start looping over it without a query.
{% for navItem in mainNavigation.navigation %}
<a href="{{ navItem.navigationUrl }}"
class="{% if craft.app.request.getSegment(1) == navItem.navigationUrl | trim('/') %}tw-bg-gray-700 tw-text-white
{% else %}tw-text-gray-300 hover:tw-bg-gray-700 hover:tw-text-white
{% endif %} tw-px-3 tw-py-2 tw-rounded-md tw-text-sm tw-font-medium">
{{ navItem.navigationLabel }}
</a>
{% endfor %}
If we wanted to add a drop-down, we could check in the loop for the presence of a particular piece of data in the row, like the URL, and then inject some additional code, along with an element query for more content.
The downside to this approach is that it puts a lot of control in the person’s hands updating and maintaining the navigation. One typo could break the navigation.
Another option when tackling navigation is to tie your content structure and navigation closely together using the Structure section to power the navigation.
In this example, each top-level entry in the Structure would be the navigation item. And, optionally, each child item in the Structure would be a drop-down menu item (like we did with Quests earlier in this article).
To use just top-level items in the navigation, we fetch the entries with an element query and specify only the first level using the level
parameter.
{% set pages = craft.entries
.section('siteContent')
.level(1)
.all()
%}
{% for page in pages %}
<a href="{{ page.url }}">{{ page.title }}</a>
{% endfor %}
To manage outputting the child nav items, we’d need to choose between these two options:
nav
TagCraft’s native nav
tag, which has helper functionality to output the child items using the same markup recursively Conditional checks for child nav items and level and then determine if any subnav items are available.
The nav
tag approach is excellent if you have a simple output where the subnav items are just nested copies of the top-level items. However, the’ nav’ tag might feel complex and a little overcooked if you have a more complex layout, like adding additional styling, icons, and a drop-down menu.
{% set pages = craft.entries.section('siteContent').all() %}
{% nav page in page %}
<a href="{{ page.url }}">{{ page.title }}</a>
{% ifchildren %}
<div>
{% children %}
</div>
{% endifchildren %}
{% endnav %}
In my situation with CraftQuest, I want a chevron and some special markup when there are child nav items, so I would take a different approach. Using the nav
tag is totally doable, but the code below is simpler.
{% set pages = craft.entries.section('siteContent').all() %}
{% for page in pages %}
{% if page.children | length %}
{# show top nav item treated with chevron etc #}
<a href="{{page.url}}">{{ page.title }}</a>
{% elseif page.level == 1 %}
{% show normal top nav treatment %}
<a href="{{page.url}}">{{ page.title }}</a>
{% endif %}
{% endfor %}
Re-ordering the pages in the Structure would also re-order them in the navigation, assuming we pull the entries into the Twig template in the default order.
All of the options we’ve covered so far require only native Craft CMs functionality. But if none of the above options work for your project, and you need to give the site administrators ultimate control over the site navigation, then one of the popular navigation plugins is the correct choice.
As of the writing of this article, there are two popular plugins for managing navigation in Craft CMS:
Both plugins offer similar functionality. However, according to the Craft Plugin Store, Verbb’s Navigation plugin is more popular, with about 14x the installations.
They both cost only $19, which is a meager amount to save yourself time and to have a nicer implemenation for your clients or customers.
Using Verbb’s navigation plugin as an example, you can set up multiple navigations and create each “node” of the navigation from entries, categories, assets, products, and custom hard-coded values. Parent-child relationships are possible, too, even between different sources.
For example, the Quests navigation item or node is made up of a parent navigation item based on the entry in the Pages section for the Quests landing page.
The children navigation item or nodes are made up of entries from the Quests section (the same section we pulled from earlier in the article).
The ability to combine different sources makes navigation plugins like Navigation powerful and customizable.
You can output the navigation in a Twig template using either the automated mode via the render()
method or by manually querying for the navigation and then iterating over the results.
{% set navItems = craft.navigation.nodes().handle('mainNavigation').all() %}
If neither of those is suitable for you; you can browse all available plugins related to navigation.
One of the best aspects about Craft is that it doesn’t impose its will on your templates. You can do as you wish. Sometimes that means having a lot of choices, like we do with navigation. Pick the best option for your project and go build great projects!
nav
tag documentation (Craft CMS documentation)