Theme development
How to build a Shopify mega menu without an app (Liquid)
Build a multi-column Shopify mega menu in Liquid with no app, using the real navigation object: linklists, nested link.links, and link.active. Includes a section schema with a link_list setting and accessible markup.
Bas Lefeber
Founder, learnshopify.dev · June 19, 2026 · 6 min read
A mega menu is the wide, multi-column dropdown that opens under a top-level nav item: think "Shop" expanding into Men, Women, Sale, and New In, each with its own list of child links. Stores reach for an app to build one, but Shopify already gives you everything you need. The navigation a merchant edits under Online Store, Navigation is exposed to your theme as the linklists object, and any menu item can hold a nested set of child links. Render those children as columns and you have a mega menu, no app, no monthly fee, no third-party script slowing the page down.
The thing that makes this maintainable is that the structure lives in the merchant's menu, not in your Liquid. They add a child link in the admin, a new column appears. You never touch the theme again. Here is how to build it properly.
TL;DR
Loop linklists['main-menu'].links for the top row. For each top-level link, check link.links.size > 0 and render its link.links as columns inside a <details> dropdown. Let the merchant pick the menu via a link_list section setting instead of hardcoding the handle. Use link.active to mark the current section.
How Shopify navigation maps to a mega menu
Every menu in the store is reachable through linklists, keyed by handle. The default header menu is usually main-menu. Each entry in linklists['main-menu'].links is a link object with a title, a url, and (this is the part that matters) its own links array of child links. Shopify menus support two levels of nesting in the admin, which is exactly the depth a mega menu needs: top-level items as triggers, their children grouped into columns.
link.linksis the array of child links under a menu item.link.links.sizetells you how many children there are, so you can decide whether an item is a flat link or a mega-menu trigger.link.activeis true when the current page is under that link, which lets you highlight the section the shopper is browsing.
Build it as a section with a menu picker
Do not hardcode main-menu in the Liquid. Build the navigation as a section and expose a link_list setting so the merchant chooses which menu drives it. That is the difference between a header a developer has to edit and one a merchant can repoint in the theme editor. Read the chosen menu off section.settings, and fall back to the default handle if nothing is picked yet.
{% assign menu = linklists[section.settings.menu] | default: linklists['main-menu'] %} <nav class="mega-nav" aria-label="Primary"> <ul class="mega-nav__row"> {% for link in menu.links %} <li class="mega-nav__item"> {% if link.links.size > 0 %} <details class="mega-nav__details"> <summary class="mega-nav__top {% if link.active %}mega-nav__top--active{% endif %}" > {{ link.title }} </summary> <div class="mega-nav__panel"> <div class="mega-nav__columns"> {% for child in link.links %} <div class="mega-nav__col"> <a href="{{ child.url }}" class="mega-nav__col-head {% if child.active %}mega-nav__col-head--active{% endif %}" > {{ child.title }} </a> {% if child.links.size > 0 %} <ul class="mega-nav__sublist"> {% for grandchild in child.links %} <li> <a href="{{ grandchild.url }}" class="mega-nav__sublink"> {{ grandchild.title }} </a> </li> {% endfor %} </ul> {% endif %} </div> {% endfor %} </div> </div> </details> {% else %} <a href="{{ link.url }}" class="mega-nav__top {% if link.active %}mega-nav__top--active{% endif %}" > {{ link.title }} </a> {% endif %} </li> {% endfor %} </ul></nav>- The
menuassignment reads the merchant's picked menu and falls back tomain-menu, so the section renders something the moment it is dropped on the page. - The
link.links.size > 0guard is what splits triggers from flat links. An item with children renders a<details>panel; an item without children renders a plain<a>. Skip this guard and you get empty dropdowns on "Contact" and "Home". - Each child link becomes a column heading, and any grandchildren (the deeper level Shopify allows) become the list under it. That two-level structure is the whole mega menu.
link.activeandchild.activeadd a class on the current section so the nav reflects where the shopper is.
The schema: a link_list setting
The link_list setting type is the native way to let a merchant pick a menu. It renders a dropdown of every menu in the store and returns the chosen menu's handle, which is exactly what linklists[...] expects. Here is the minimum schema. The default maps to the handle of the menu the merchant most likely wants.
{ "name": "Mega menu", "settings": [ { "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu", "info": "Edit items and columns under Online Store, Navigation." } ], "presets": [ { "name": "Mega menu" } ]}Where the columns come from
There is no "add a column" control in the theme editor here, and that is by design. The columns are the child links of each top-level item in the chosen menu. The merchant builds them under Online Store, Navigation by nesting items. This keeps one source of truth for the store's navigation instead of a second copy living in theme settings.
Accessibility: do not skip this part
A mega menu is a keyboard and screen-reader minefield if you build it as a pile of divs with hover-only behaviour. The native <details> and <summary> elements give you a disclosure widget that already works with the keyboard, exposes its expanded state to assistive tech, and toggles on click without a line of JavaScript. That is why the markup above leads with them rather than a custom hover menu.
- Real links, real text. Every item is an
<a>with the link's actualtitleandurl, so it is announced and focusable. Nodivpretending to be a link. - A labelled landmark. The wrapping
<nav aria-label="Primary">gives screen-reader users a named region to jump to. - Hover is an enhancement, not the mechanism. If you add hover-to-open with CSS, keep the click and keyboard toggle from
<details>working underneath it. Hover alone excludes keyboard and touch users.
Heads up
If you open the panel on hover with CSS, set the open behaviour so focus is not trapped and the menu still closes on Escape. The simplest correct version is to let <details> own open and close, and treat hover purely as a visual nicety on pointer devices.
What AI tools get wrong here
- Hardcoding the navigation. A generated mega menu often spells out "Men", "Women", "Sale" as literal Liquid or pulls from
linklists['main-menu']with the handle baked in. The moment the merchant renames the menu or wants a different one in the footer, it breaks. Use alink_listsetting so the menu is data, not code. - No link.links guard. Without the
link.links.size > 0check, flat items like "Home" and "Contact" render empty dropdown panels, and a single missing guard turns a clean nav into a row of broken triggers. - Skipping accessibility. Hover-only
divmenus with no keyboard support, no focusable links, and no expanded-state semantics are common in AI output. They look fine on a mouse and fail an audit. The<details>element exists precisely so you do not have to hand-roll any of that.
If you want the broader picture of how sections and blocks compose a theme, the sections and blocks explainer walks through where a section like this fits. And if you are working on a recent build, the Spring 2026 edition for developers covers what changed in the platform around theme architecture.
Learn this properly · free lesson
The shape of a theme: where everything lives
Free: learn the Shopify theme structure in the browser, where sections, snippets, and the navigation object live, before you wire up a mega menu against a real store.
Try this lesson — freeWrapping up
A mega menu is a loop over linklists, a guard on link.links.size, and a <details> panel that renders the children as columns. Drive it with a link_list setting so the merchant owns the structure, mark the current section with link.active, and lean on the native disclosure element for accessibility. That is a production-grade mega menu with no app, no recurring cost, and nothing third-party loading on every page.
Frequently asked questions
How do I build a Shopify mega menu without an app?
Build a section that loops over linklists['main-menu'].links. For each top-level item, check link.links.size > 0 and render its child links as columns inside a native <details> dropdown. Expose a link_list setting so the merchant picks the menu. It is plain Liquid, CSS, and the built-in navigation object, no app needed.
Where do the mega menu columns come from?
From the child links of each top-level menu item. In the Shopify admin under Online Store, Navigation, the merchant nests items under a parent. Those nested items become link.links in Liquid, which you render as the columns of the dropdown.
How many levels of nesting does a Shopify menu support?
Shopify navigation supports two levels of nesting in the admin: top-level items and their children. That is enough for a mega menu, where top-level items are the triggers and their children are grouped into columns. A grandchild level is available in Liquid via child.links when the menu provides it.
How do I highlight the current section in the menu?
Use the link.active property. It returns true when the current URL path matches or sits under a link's URL, so you can add an active class to the top-level item or column the shopper is currently browsing without any JavaScript.
On the launch list
Get updates on the platform.
Same waitlist as the homepage. New posts plus a heads-up when v1 launches. No drip, no spam.
About the author
Bas Lefeber, Founder, learnshopify.dev
Bas builds learnshopify.dev, where developers learn production-grade Shopify theme development against a live storefront. He writes about Liquid, theme architecture, and the parts of the job that still matter now that AI writes the code.
Keep going in the curriculum
