Navigation Options in Craft CMS

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 offi­cial way to man­age nav­i­ga­tion in Craft CMS. Unlike oth­er CMSes that offer built-in nav­i­ga­tion man­age­ment, Craft CMS leaves it up to the devel­op­er to decide the best imple­men­ta­tion for the website. 

Let’s review five ways to do nav­i­ga­tion in Craft CMS:

Craft CMS is design-agnos­tic and doesn’t impose any lay­out or design struc­ture on a project. Craft focus­es entire­ly on the con­tent and not on the pre­sen­ta­tion. Because Craft doesn’t for­mal­ly sup­port one way to do nav­i­ga­tion, there is a lot of flex­i­bil­i­ty in what you can do.

Recent­ly on the Craft CMS Dis­cord serv­er, there was a dis­cus­sion of how to do nav­i­ga­tion and the input from dif­fer­ent peo­ple sig­nif­i­cant­ly var­ied. You had those that most­ly always imple­ment the nav­i­ga­tion man­u­al­ly via hard-cod­ed nav items right in the Twig tem­plate. But, there were also those that did it half-hard-cod­ed nav­i­ga­tion, adding in some dynam­ic items when nec­es­sary. And then there were oth­ers who chimed in that all nav­i­ga­tion should be ful­ly dynam­ic and man­age­able from the Craft con­trol panel.

There’s no wrong way, just the way that is best for your project. I’ve pri­mar­i­ly used the hard-cod­ed approach for my projects and a mix of every­thing for client or cus­tomer websites.

I don’t want the help­ful dis­cus­sion locked behind a Dis­cord serv­er that is not find­able through web search­es, so let’s doc­u­ment some of the most pop­u­lar meth­ods here. At the end of the arti­cle, I’ll link to addi­tion­al ideas and read­ing on the topic.

Sta­t­ic Nav­i­ga­tion #

  • Rea­sons: Sim­plic­i­ty, Speed
  • Ide­al for: small web­sites, brochure sites

Build­ing nav­i­ga­tion right in the HTML or Twig code is the sim­plest of all the imple­men­ta­tions we’ll cov­er in this arti­cle. Fur­ther­more, it is the one I most com­mon­ly use in my projects because I am both the devel­op­er and the client, so I can make this type of trade-off with­out many downsides. 

Sta­t­ic nav­i­ga­tion is a set-and-for­get approach to nav­i­ga­tion, and it comes with the assump­tion that the nav­i­ga­tion will not change very often. 

Here’s a snip­pet of what the CraftQuest nav­i­ga­tion looked like until recently:

<a href="/courses" class="{% if == '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 == '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 == '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 == '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 nav­i­ga­tion items are hard-cod­ed with the name and the URL, I add in some dynam­ic check­ing of the cur­rent URL to high­light the nav­i­ga­tion item when the vis­i­tor is in that section. 

Note: In the remain­ing code exam­ples, I’ll leave out the Tail­wind class­es in the name of simplicity.

Par­tial­ly-Dynam­ic Nav­i­ga­tion #

  • Rea­sons: Sim­plic­i­ty, Some Dynam­ic Items Needed
  • Ide­al for: small web­sites, brochure sites

A par­tial­ly dynam­ic nav­i­ga­tion is one that pulls in some nav­i­ga­tion items via Ele­ment queries in Craft. A typ­i­cal exam­ple of this is a drop-down that has addi­tion­al sub­pages under the main nav­i­ga­tion item.

On the CraftQuest site, I recent­ly set this up on the Quests nav­i­ga­tion item. 

When you click on the nav­i­ga­tion item is expos­es a drop-down menu with the avail­able Quests. Every­thing else in the nav­i­ga­tion is still sta­t­ic and hard-cod­ed in the Twig template.

In this imple­men­ta­tion, I’m using an Ele­ment query to fetch all of the Quests, dis­play the title and short descrip­tion, and link it up with the entry URL. This approach allows me to keep the nav­i­ga­tion sim­ple and fast while mak­ing it dynam­ic exact­ly where it needs to be. So, if I add addi­tion­al Quests entries to the chan­nel sec­tion, those would appear here. 

A dynam­ic sec­tion of a nav­i­ga­tion can pull from any con­tent source in Craft: a sec­tion, cat­e­gories, tags, or any oth­er ele­ment type. 

<div class="tw-relative"> 
     <div id="quests-menu"> 
		     {% for quest in craft.entries
     			<a href="{{ quest.url }}" 
     class="{% if == quest.slug %}current{% endif %}"> 
     				<p>{{ quest.title }}</p> 
			     	<span class="tw-text-sm tw-text-gray-500"> 
			     		{{ quest.pathwayShortDescription }} 
     		{% endfor %}
     <a href="/quests"> 
     <p class="tw-text-base tw-font-medium tw-text-gray-100">See All Quests</p> 

<a href="/courses">Courses</a> 

<a href="/lessons">Lessons</a>

<a href="/topics">Topics</a> 

<a href="/articles">Articles</a>

This sim­pli­fied code snip­pet treats just the first nav­i­ga­tion item dif­fer­ent­ly, using an ele­ment query for the quests (the sec­tion han­dle is learningPathways) and then list­ing them out in a drop-down. I’ve removed all of the Tail­wind class­es to make the code a bit more read­able on its own.

After that dynam­ic nav­i­ga­tion item, the remain­ing items are just the same as in the first part of the arti­cle: all hard-cod­ed with a seg­ment check to high­light it when you’re on that page or sec­tion of the site.

This imple­men­ta­tion doesn’t require any­thing extra to man­age inside of Craft. Instead, it uses exist­ing con­tent struc­tures and data. If you need only some cus­tomiza­tion to your main nav­i­ga­tion, then this is a good choice. 

Glob­al Set Pow­ered Nav­i­ga­tion #

  • Rea­sons: Fin­er con­trol, Reg­u­lar­ly chang­ing nav­i­ga­tion, No devel­op­er available
  • Ide­al for: larg­er con­tent sites, teams with­out tech­ni­cal sup­port, prod­uct mar­ket­ing sites

Now we’re get­ting to the nav­i­ga­tion imple­men­ta­tions that you can man­age from with­in the Craft con­trol pan­el. In this exam­ple, we cre­ate a Glob­al Set in Craft and use it to build and man­age the site nav­i­ga­tion. The advan­tage here is that it allows some­one, pre­sum­ably the client or cus­tomer, to man­age the nav­i­ga­tion from the Craft con­trol pan­el. This is help­ful in some sce­nar­ios because it doesn’t require the inter­ven­tion of a devel­op­er or tech­ni­cal staff member.

This imple­men­ta­tion has the down­side of strad­dling sta­t­ic and ful­ly dynam­ic nav­i­ga­tion, with lim­it­ed con­trol over child nav­i­ga­tion items (like the drop-down we did in the Par­tial­ly-Dynam­ic ver­sion above).

We can han­dle this nav­i­ga­tion in a few ways because we can use any field or field type in a Glob­al Set. For this ver­sion, let’s use a Table field and man­age all nav­i­ga­tion items as table rows. 

The table will have two columns to start: Nav­i­ga­tion Label, Nav­i­ga­tion URL.

Each row will rep­re­sent one nav­i­ga­tion item. We won’t add any addi­tion­al fea­tures to the table, but you could add columns like HTML class­es and IDs, a lightswitch field to enable/​disable func­tion­al­i­ty, a col­or, and more.

The Twig code to make this hap­pen is a loop over the rows of the table in the Glob­al Set. Since we have glob­al access to it, we can start loop­ing over it with­out a query.

{% for navItem in mainNavigation.navigation %}
	<a href="{{ navItem.navigationUrl }}" 
		class="{% if == 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 }}
{% endfor %}

If we want­ed to add a drop-down, we could check in the loop for the pres­ence of a par­tic­u­lar piece of data in the row, like the URL, and then inject some addi­tion­al code, along with an ele­ment query for more content. 

The down­side to this approach is that it puts a lot of con­trol in the person’s hands updat­ing and main­tain­ing the nav­i­ga­tion. One typo could break the navigation.

Struc­ture-Pow­ered Nav­i­ga­tion #

  • Rea­sons: Want nav­i­ga­tion tight­ly bound to con­tent struc­ture, No devel­op­er available
  • Ide­al for: Larg­er con­tent sites, teams with­out tech­ni­cal sup­port, prod­uct mar­ket­ing sites

Anoth­er option when tack­ling nav­i­ga­tion is to tie your con­tent struc­ture and nav­i­ga­tion close­ly togeth­er using the Struc­ture sec­tion to pow­er the navigation.

In this exam­ple, each top-lev­el entry in the Struc­ture would be the nav­i­ga­tion item. And, option­al­ly, each child item in the Struc­ture would be a drop-down menu item (like we did with Quests ear­li­er in this article).

To use just top-lev­el items in the nav­i­ga­tion, we fetch the entries with an ele­ment query and spec­i­fy only the first lev­el using the level parameter.

{% set pages = craft.entries
{% for page in pages %}  
 	<a href="{{ page.url }}">{{ page.title }}</a>  
{% endfor %}

To man­age out­putting the child nav items, we’d need to choose between these two options:

nav Tag

Craft’s native nav tag, which has helper func­tion­al­i­ty to out­put the child items using the same markup recur­sive­ly Con­di­tion­al checks for child nav items and lev­el and then deter­mine if any sub­nav items are available.

The nav tag approach is excel­lent if you have a sim­ple out­put where the sub­nav items are just nest­ed copies of the top-lev­el items. How­ev­er, the’ nav’ tag might feel com­plex and a lit­tle over­cooked if you have a more com­plex lay­out, like adding addi­tion­al 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 %}
			{% children %}
	{% endifchildren %}
{% endnav %}
Check­ing for Page Children

In my sit­u­a­tion with CraftQuest, I want a chevron and some spe­cial markup when there are child nav items, so I would take a dif­fer­ent approach. Using the nav tag is total­ly 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-order­ing the pages in the Struc­ture would also re-order them in the nav­i­ga­tion, assum­ing we pull the entries into the Twig tem­plate in the default order.

Plu­g­in-Pow­ered Nav­i­ga­tion #

  • Rea­sons: Ulti­mate flex­i­bil­i­ty and control
  • Ide­al for: Larg­er con­tent site, Sites need­ing reg­u­lar updates to navigation

All of the options we’ve cov­ered so far require only native Craft CMs func­tion­al­i­ty. But if none of the above options work for your project, and you need to give the site admin­is­tra­tors ulti­mate con­trol over the site nav­i­ga­tion, then one of the pop­u­lar nav­i­ga­tion plu­g­ins is the cor­rect choice. 

As of the writ­ing of this arti­cle, there are two pop­u­lar plu­g­ins for man­ag­ing nav­i­ga­tion in Craft CMS:

Both plu­g­ins offer sim­i­lar func­tion­al­i­ty. How­ev­er, accord­ing to the Craft Plu­g­in Store, Verbb’s Nav­i­ga­tion plu­g­in is more pop­u­lar, with about 14x the installations. 

They both cost only $19, which is a mea­ger amount to save your­self time and to have a nicer imple­me­na­tion for your clients or customers.

Look­ing at Verbb Navigation

Using Verbb’s nav­i­ga­tion plu­g­in as an exam­ple, you can set up mul­ti­ple nav­i­ga­tions and cre­ate each node” of the nav­i­ga­tion from entries, cat­e­gories, assets, prod­ucts, and cus­tom hard-cod­ed val­ues. Par­ent-child rela­tion­ships are pos­si­ble, too, even between dif­fer­ent sources. 

For exam­ple, the Quests nav­i­ga­tion item or node is made up of a par­ent nav­i­ga­tion item based on the entry in the Pages sec­tion for the Quests land­ing page. 

The chil­dren nav­i­ga­tion item or nodes are made up of entries from the Quests sec­tion (the same sec­tion we pulled from ear­li­er in the article). 

The abil­i­ty to com­bine dif­fer­ent sources makes nav­i­ga­tion plu­g­ins like Nav­i­ga­tion pow­er­ful and customizable.

You can out­put the nav­i­ga­tion in a Twig tem­plate using either the auto­mat­ed mode via the render() method or by man­u­al­ly query­ing for the nav­i­ga­tion and then iter­at­ing over the results.

{% set navItems = craft.navigation.nodes().handle('mainNavigation').all() %}

If nei­ther of those is suit­able for you; you can browse all avail­able plu­g­ins relat­ed to nav­i­ga­tion.

One of the best aspects about Craft is that it doesn’t impose its will on your tem­plates. You can do as you wish. Some­times that means hav­ing a lot of choic­es, like we do with nav­i­ga­tion. Pick the best option for your project and go build great projects!

Addi­tion­al Resources and Read­ing #