You are viewing beta documentation for Navigation 4.x.
Templates

Active State & Context

Navigation resolves current, active, and hasActiveChild states for each node after the menu loads. Use these for highlighting, open dropdowns, and contextual sub-navigation.

Active state is always computed after cache hits and is skipped in the CP, console, and preview requests.

States

StateMethodTypical use
Currentnode.getCurrent()aria-current="page", primary highlight
Activenode.getActive()Open branch / .active on parent
Has active childnode.hasActiveChild()Open submenu, chevron icon

Current is an exact URL match (including urlSuffix). Active is true when the node is current, when any descendant nav node is current, or when path-section matching applies. Has active child is true when a descendant nav node — not just a URL under the parent path — is current.

Path-section matching covers URLs that sit under a node’s path when no child nav item exists for that page. The /news node is active on /news/article-slug even if the article is not in the menu.

<li class="{{ node.getActive() ? 'is-active' }}{{ node.getCurrent() ? ' is-current' }}">

Query helpers

{# Deepest exact-match (current page) node #}
{% set currentNode = craft.navigation.getActiveNode({ handle: 'mainMenu' }) %}

{# Branch fallback when no exact node exists for the URL #}
{% set sectionNode = craft.navigation.getActiveNode({ handle: 'mainMenu' }, true) %}

{# All exact matches, deepest first #}
{% set currentNodes = craft.navigation.getCurrentNodes({ handle: 'mainMenu' }) %}

{# All branch-active nodes #}
{% set activeNodes = craft.navigation.getActiveNodes({ handle: 'mainMenu' }) %}

Default behaviour

getActiveNode() (default) returns the deepest node whose URL exactly matches the current page — not the first branch-active ancestor.

On /products/widget with nodes for /products and /products/widget, getActiveNode() returns Widget. Both Products (getActive()) and Widget (getCurrent()) are highlighted appropriately.

Pass true as the second argument when you need branch/path fallback for contextual sub-nav.

Context API

For sidebars and section navigation, use craft.navigation.context() instead of hand-rolling parent/sibling queries:

{% set ctx = craft.navigation.context('mainMenu') %}

<ul>
    {% for node in ctx.siblings() %}
        <li class="{{ node.getCurrent() ? 'is-current' }}">
            <a href="{{ node.url }}">{{ node.title }}</a>
        </li>
    {% endfor %}
</ul>
MethodReturns
current()Deepest exact-match node
currentNodes()All exact matches, deepest first
activeNodes()All branch-active nodes
parent()Parent of deepest current node
ancestors()Root → parent of current
siblings()Same-level nodes under shared parent
children()Direct children of deepest current node
branch()Ancestors + current + siblings (common sidebar set)

See Context & Breadcrumbs for breadcrumb trails.

Dynamic children

Projected entries from Dynamic nodes participate in active resolution. getActiveNode() and getCurrentNodes() can return a ProjectedNode when the current URL matches a projected entry.

Override active state

Branch active state can be modified via Node::EVENT_NODE_ACTIVE when getActive() runs. See Events.