2026 Community Survey results are here! See how the Craft CMS community works. results are live!

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.

Image

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 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 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"> 
	<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 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 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 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
                    .section('siteContent')
                    .level(1)
                    .all() 
%}  
{% 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 %}
		<div>
			{% children %}
		</div>
	{% 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 #