Free code · No app required
A mega menu on Shopify is one section file and a nested menu in the admin. No app, no subscription, no external library. This section reads your existing navigation through Shopify’s linklist object, renders all three levels Shopify allows, and gives the merchant a column count and a promo panel in the theme editor. It opens with a native <details> element, so it works before any JavaScript loads. Every file below passes Shopify’s own Theme Check.
Menus are one of the few places where a free build genuinely covers most stores. The limits below are Shopify's, not this code's.
| Handled here | Needs a different build |
|---|---|
| Three levels of nesting, the maximum Shopify supports | A fourth level — restructure instead, Shopify caps linklist.levels at 3 |
| Merchant edits links in Online Store → Navigation | Menus that change per customer segment or login state |
| A promo image, heading and link in the dropdown | Live product cards or stock counts inside the menu |
| Server-rendered anchors, so crawlers follow every link | Menus assembled client-side after page load |
| Keyboard and Escape handling, WCAG-oriented markup | Full multi-column keyboard roving tabindex for very large menus |
If you need the right-hand column to show real products, or the menu to differ for wholesale customers, that is app or metaobject territory and it is the point where paying someone starts to make sense.
One file. Shopify bundles the stylesheet and javascript blocks for you, so there is nothing to add to theme.liquid.
sections/mega-menu.liquidThe whole thing in one file — markup, styles and behaviour. Shopify bundles the {% stylesheet %} and {% javascript %} blocks for you, so there are no extra asset files to create.
<nav class="mm" aria-label="{{ section.settings.aria_label | escape }}">
<ul class="mm-root">
{%- for link in section.settings.menu.links -%}
{%- if link.links.size > 0 -%}
<li class="mm-item mm-has-children{% if link.child_active %} mm-active{% endif %}">
<details class="mm-details">
<summary class="mm-summary">
<span>{{ link.title }}</span>
<svg class="mm-caret" viewBox="0 0 12 8" width="12" height="8" aria-hidden="true" focusable="false">
<path d="M1 1l5 5 5-5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
</svg>
</summary>
<div class="mm-panel">
<div class="mm-panel-inner" style="--mm-cols: {{ section.settings.columns }}">
{%- for child in link.links -%}
<div class="mm-col">
<a class="mm-col-title{% if child.active %} mm-current{% endif %}" href="{{ child.url }}">
{{ child.title }}
</a>
{%- if child.links.size > 0 -%}
<ul class="mm-sub">
{%- for grandchild in child.links -%}
<li>
<a class="mm-sub-link{% if grandchild.current %} mm-current{% endif %}" href="{{ grandchild.url }}">
{{ grandchild.title }}
</a>
</li>
{%- endfor -%}
</ul>
{%- endif -%}
</div>
{%- endfor -%}
{%- if section.settings.promo_image != blank -%}
<div class="mm-col mm-promo">
{%- if section.settings.promo_link != blank -%}
<a href="{{ section.settings.promo_link }}">
{{ section.settings.promo_image | image_url: width: 480 | image_tag: loading: 'lazy', alt: section.settings.promo_heading }}
</a>
{%- else -%}
{{ section.settings.promo_image | image_url: width: 480 | image_tag: loading: 'lazy', alt: section.settings.promo_heading }}
{%- endif -%}
{%- if section.settings.promo_heading != blank -%}
<p class="mm-promo-heading">{{ section.settings.promo_heading }}</p>
{%- endif -%}
</div>
{%- endif -%}
</div>
</div>
</details>
</li>
{%- else -%}
<li class="mm-item">
<a class="mm-top-link{% if link.active %} mm-current{% endif %}" href="{{ link.url }}">
{{ link.title }}
</a>
</li>
{%- endif -%}
{%- endfor -%}
</ul>
</nav>
{% stylesheet %}
.mm-root { display: flex; flex-wrap: wrap; gap: 1.5rem; list-style: none; margin: 0; padding: 0; }
.mm-item { position: relative; }
.mm-summary,
.mm-top-link {
display: inline-flex; align-items: center; gap: .35rem;
padding: .65rem 0; cursor: pointer; list-style: none;
color: inherit; text-decoration: none; font: inherit;
}
.mm-summary::-webkit-details-marker { display: none; }
.mm-caret { transition: transform .18s ease; }
.mm-details[open] .mm-caret { transform: rotate(180deg); }
.mm-current { text-decoration: underline; text-underline-offset: .25em; }
.mm-panel {
position: absolute; left: 0; top: 100%; z-index: 40;
min-width: min(760px, 92vw);
background: Canvas; color: CanvasText;
border: 1px solid rgba(128,128,128,.25);
border-radius: .5rem;
box-shadow: 0 12px 32px rgba(0,0,0,.14);
}
.mm-panel-inner {
display: grid;
grid-template-columns: repeat(var(--mm-cols, 4), minmax(0, 1fr));
gap: 1.5rem; padding: 1.5rem;
}
.mm-col-title { display: block; font-weight: 600; margin-bottom: .5rem; color: inherit; text-decoration: none; }
.mm-sub { list-style: none; margin: 0; padding: 0; display: grid; gap: .35rem; }
.mm-sub-link { color: inherit; text-decoration: none; opacity: .85; }
.mm-sub-link:hover { opacity: 1; text-decoration: underline; }
.mm-promo img { width: 100%; height: auto; border-radius: .4rem; }
.mm-promo-heading { margin: .5rem 0 0; font-size: .9rem; }
@media (max-width: 989px) {
.mm-root { flex-direction: column; gap: 0; }
.mm-panel { position: static; min-width: 0; border: 0; box-shadow: none; }
.mm-panel-inner { grid-template-columns: 1fr; padding: .5rem 0 1rem 1rem; }
}
{% endstylesheet %}
{% javascript %}
document.addEventListener('click', (e) => {
document.querySelectorAll('.mm-details[open]').forEach((d) => {
if (!d.contains(e.target)) d.open = false;
});
});
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
document.querySelectorAll('.mm-details[open]').forEach((d) => {
d.open = false;
const s = d.querySelector('.mm-summary');
if (s) s.focus();
});
});
{% endjavascript %}
{% schema %}
{
"name": "Mega menu",
"settings": [
{ "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu" },
{ "type": "range", "id": "columns", "label": "Columns", "min": 2, "max": 6, "step": 1, "default": 4 },
{ "type": "text", "id": "aria_label", "label": "Accessible label", "default": "Main navigation" },
{ "type": "header", "content": "Promo panel" },
{ "type": "image_picker", "id": "promo_image", "label": "Promo image" },
{ "type": "text", "id": "promo_heading", "label": "Promo heading" },
{ "type": "url", "id": "promo_link", "label": "Promo link" }
],
"presets": [{ "name": "Mega menu" }]
}
{% endschema %}
Online Store → Themes → … → Edit code → Sections → Add a new section, name it mega-menu. Delete whatever Shopify puts in it and paste the code above.
Online Store → Navigation. Nest your links: a top-level item, its children, and their children. Shopify allows three levels and no more — the section renders all three. Nothing here needs you to name the menu 'main-menu'; you pick it in step 4.
In the theme editor, open the header group and add the Mega menu section. If your theme does not allow sections in the header, render it directly in sections/header.liquid instead:
{% section 'mega-menu' %}Select the section in the theme editor. Choose the menu, set how many columns the dropdown uses (2–6), and optionally add a promo image with a heading and link — that panel is what turns a navigation menu into a merchandising surface.
Below 990px the panel stops floating and stacks inline, so it behaves like an accordion instead of a dropdown that runs off the screen. That breakpoint is in the stylesheet block; change 989px if your theme uses a different one.
A Shopify mega menu needs one section file and a nested menu in the admin — no app. Shopify's own linklist object already carries the structure: linklists gives you every menu in the store, link.links gives you a link's children, and Shopify supports three levels of nesting (linklist.levels caps at 3). The section above loops those three levels into a CSS grid and uses a native <details> element for the dropdown, which means it opens and closes with no JavaScript at all. The small script only adds two conveniences: closing an open panel when you click elsewhere, and closing it on Escape with focus returned to the trigger.
Yes, and on any Online Store 2.0 theme. Nothing in the section reads Dawn-specific or Horizon-specific markup — it renders its own list from the linklist object and styles it with its own scoped classes. The only theme-dependent step is where you place it. In most 2.0 themes you can add it to the header group from the theme editor; in themes that lock the header, you render it in sections/header.liquid with {% section 'mega-menu' %}. Colours inherit from your theme because the panel uses the Canvas and CanvasText system colours rather than hard-coded values.
Three, and that is a platform limit rather than a theme one. The linklist object exposes a levels property that Shopify documents as having a maximum of 3. In practice that means a top-level item (Clothing), a second level that becomes the column headings in the dropdown (Men, Women, Kids), and a third level that becomes the links inside each column (T-shirts, Jackets). If you need a fourth level, the usual answer is not a deeper menu but a different structure — a promoted collection panel, or a landing page that does the fourth level as content.
Neither, if it is rendered server-side, which this one is. Every link is in the initial HTML as a real anchor, so crawlers follow them and the internal linking benefit is real — a mega menu is one of the few components that genuinely improves how link equity reaches deep collection pages. On performance it adds no network requests and no external library. The pattern that does cause problems is a JavaScript menu that fetches its own contents after load: crawlers see an empty nav, and the fetch competes with your LCP image.
Yes, entirely, and that is the point of building it as a section rather than hard-coding it. The links come from Online Store → Navigation, so a merchant restructures the menu without touching code. The column count, the accessible label, and the promo image, heading and link are all schema settings, so they appear in the theme editor with live preview. Nothing in the file needs a developer once it is installed.
The section above is a normal mega menu and it will serve most stores unchanged. Teams come to us when the menu has to do more than list links: showing live products or stock inside a panel, swapping the whole structure for wholesale or B2B customers, driving it from metaobjects so marketing can edit panels without touching navigation, or rebuilding a menu that has grown past what three levels can express.
We build Shopify themes and apps, and we have shipped three of our own to the Shopify App Store. Send your store URL and what the menu needs to do — you get a straight answer on whether it needs custom work at all, and a fixed price if it does. Small, well-defined jobs start at $1,000–$2,000.
Reply within a few hours · Fixed price before any work starts