Headless Trees
craft.navigation.tree() returns a nested array tree — useful for JSON APIs, JavaScript menus, and static site generators.
Basic Usage
{% set tree = craft.navigation.tree('mainMenu') %}
{{ tree|json_encode }}You can pass criteria like a node query:
{% set tree = craft.navigation.tree({ handle: 'mainMenu', level: 1 }) %}Array Shape
Each node array includes keys such as:
| Key | Description |
|---|---|
title, url, nodeUri | Link data |
current, active, hasActiveChild | Active flags (resolved for the current request) |
target | Link target |
children | Nested arrays |
element | Linked element array when present — null by default; populated when withLinkedElements is enabled |
Example Output
A small mainMenu on /products/widget might serialise like this:
[
{
"id": 101,
"title": "Products",
"url": "https://example.com/products",
"nodeUri": "products",
"level": 1,
"active": true,
"current": false,
"hasActiveChild": true,
"target": "",
"element": null,
"children": [
{
"id": 102,
"title": "Widget",
"url": "https://example.com/products/widget",
"nodeUri": "products/widget",
"level": 2,
"active": true,
"current": true,
"hasActiveChild": false,
"target": "",
"element": null
}
]
},
{
"id": 103,
"title": "About",
"url": "https://example.com/about",
"nodeUri": "about",
"level": 1,
"active": false,
"current": false,
"hasActiveChild": false,
"target": "",
"element": null
}
]Each node also includes Craft element keys from toArray() — type, menuId, custom fields, and so on. Omitted here for brevity.
With withLinkedElements: true, element holds the linked entry (or category, etc.) as an array instead of null. Projected Dynamic children include "isProjected": true and a slimmer key set — see Projected Node.
Options
{% set tree = craft.navigation.tree('mainMenu', {
withLinkedElements: true,
}) %}Navigation loads linked elements as needed to build their URLs, but the tree’s element key is null for both stored and projected nodes unless you enable the withLinkedElements output option. This option includes linked element data after the underlying node fetch, which can still use the tree cache. Setting withLinkedElements in the query criteria instead bypasses that cache for the node fetch.
tree() runs through the same read pipeline as nodes().all() — caching and active-state rules apply to the underlying fetch. See Performance & Caching.
Dynamic Projection
Projected Dynamic children appear in children with isProjected: true unless you opt out with withProjectedChildren(false) on the underlying query criteria.
For GraphQL headless access, see Query Nodes.