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.

A Shopify mega menu open under a top-level nav item, showing several columns of child links with column headings
Each top-level menu item with children becomes a panel of columns. The columns are the item's child links from the merchant's navigation.

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.links is the array of child links under a menu item.
  • link.links.size tells you how many children there are, so you can decide whether an item is a flat link or a mega-menu trigger.
  • link.active is 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.

sections/mega-menu.liquid
{% 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 menu assignment reads the merchant's picked menu and falls back to main-menu, so the section renders something the moment it is dropped on the page.
  • The link.links.size > 0 guard 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.active and child.active add a class on the current section so the nav reflects where the shopper is.

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.

sections/mega-menu.liquid (schema)
{  "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 actual title and url, so it is announced and focusable. No div pretending 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 a link_list setting so the menu is data, not code.
  • No link.links guard. Without the link.links.size > 0 check, 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 div menus 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 — free

Wrapping 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.

One email when a new module ships, one when v1 launches. No drip sequence, no spam. Unsubscribe anytime.

LiquidNavigationTheme development

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