Free code · No app required

Shopify mega menu without an app — the complete section

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.

What this covers, and where it stops

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 hereNeeds a different build
Three levels of nesting, the maximum Shopify supportsA fourth level — restructure instead, Shopify caps linklist.levels at 3
Merchant edits links in Online Store → NavigationMenus that change per customer segment or login state
A promo image, heading and link in the dropdownLive product cards or stock counts inside the menu
Server-rendered anchors, so crawlers follow every linkMenus assembled client-side after page load
Keyboard and Escape handling, WCAG-oriented markupFull 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.

The code

One file. Shopify bundles the stylesheet and javascript blocks for you, so there is nothing to add to theme.liquid.

sections/mega-menu.liquid

The 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 %}

Installing it, step by step

  1. 1Create the section file

    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.

  2. 2Build the menu in the admin

    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.

  3. 3Add the section to your header

    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' %}
  4. 4Point it at your menu and set the columns

    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.

  5. 5Check it on mobile

    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.

Questions people actually ask

How do you build a Shopify mega menu without an app?

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.

Will this work on Dawn and Horizon?

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.

How many levels can a Shopify menu have?

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.

Is a mega menu bad for SEO or Core Web Vitals?

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.

Can merchants edit the mega menu themselves?

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.

When a menu becomes a project

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.

  • Live products or stock inside a menu panel
  • A different menu for wholesale or B2B customers
  • Panels driven by metaobjects, editable without touching navigation
  • A menu that has outgrown three levels
Or book a 15-min call

Reply within a few hours · Fixed price before any work starts