Implementing a Custom Header Navigation with Liquid and JSON

Table of Contents
Big thanks to our contributors those make our blogs possible.

Our growing community of contributors bring their unique insights from around the world to power our blog. 

Introduction

Your store’s header navigation is more than just links—it’s the roadmap guiding visitors through your brand, products, and content. By leveraging Liquid and a JSON-driven menu configuration, you can build a fully dynamic, easily maintainable header that adapts as your catalog grows. In this guide, you’ll learn how to: store your menu structure in a JSON asset, parse it with Liquid, render nested menus, and add responsive behavior with minimal JavaScript. Along the way, we’ll share best practices, real-world analogies, and expert tips so you can empower merchants to update navigation without touching code. Let’s transform your header from static links into a robust, data-driven navigation system.

Understanding the Architecture

Before writing code, let’s outline the pieces:

  • JSON Menu File: A structured asset (e.g., assets/header-menu.json) defining labels, URLs, and submenus.
  • Liquid Parsing: Use asset_url and json filters to load and traverse menu items.
  • Section Markup: A sections/header.liquid file that loops through parsed JSON and outputs <nav> markup.
  • Styling & Responsiveness: CSS Grid or Flexbox for layout; a simple toggle script for mobile menus.
  • Merchant-Friendly: Updating the JSON file (via Theme Editor’s file uploader or Git) instantly reflects in the navigation.

Analogy: Think of your JSON file as a train’s timetable—once updated, all stations (pages) automatically follow the new schedule without rewiring tracks.

Prerequisites and Setup

  1. Shopify Theme (2.0+): Ensure your theme supports JSON templates and custom sections.
  2. Shopify CLI (optional): For local editing and live preview (shopify theme serve).
  3. Code Editor: VS Code or your preference.
  4. Basic JS/CSS: Familiarity with vanilla JavaScript and CSS Grid/Flexbox.

Step 1: Defining Your JSON Menu

Create a new asset at assets/header-menu.json:

jsonCopyEdit{
  "items": [
    {
      "label": "Home",
      "url": "/"
    },
    {
      "label": "Shop",
      "url": "/collections/all",
      "children": [
        { "label": "New Arrivals", "url": "/collections/new" },
        { "label": "Best Sellers", "url": "/collections/best-sellers" }
      ]
    },
    {
      "label": "About Us",
      "url": "/pages/about"
    },
    {
      "label": "Blog",
      "url": "/blogs/news"
    },
    {
      "label": "Contact",
      "url": "/pages/contact"
    }
  ]
}
  • items array: Top-level menu entries.
  • Optional children: Defines nested submenus for dropdowns.

Expert Tip: Keep labels and URLs in sync with your store’s structure. You can export existing menus manually or via API if needed.

Step 2: Loading JSON in Liquid

In sections/header.liquid, at the top:

liquidCopyEdit{% assign menu_json = 'header-menu.json' | asset_url | fetch %}
{% assign menu = menu_json | parse_json %}
  • asset_url: Generates the URL to your JSON file.
  • fetch (Shopify 2.0+): Retrieves the file’s contents.
  • parse_json: Converts the string into a Liquid object.

Note: If your theme doesn’t support fetch, use the json filter on a static string or load via include.

Step 3: Rendering the Navigation Markup

Below your assignments, add:

liquidCopyEdit<nav class="site-header__nav">
  <ul class="nav-menu">
    {% for item in menu.items %}
      <li class="nav-item{% if item.children %} has-children{% endif %}">
        <a href="{{ item.url }}" class="nav-link">{{ item.label }}</a>
        {% if item.children %}
          <ul class="sub-menu">
            {% for child in item.children %}
              <li class="sub-item">
                <a href="{{ child.url }}" class="sub-link">{{ child.label }}</a>
              </li>
            {% endfor %}
          </ul>
        {% endif %}
      </li>
    {% endfor %}
  </ul>
  <button class="nav-toggle" aria-expanded="false" aria-label="Toggle navigation">
    ☰
  </button>
</nav>
  • .nav-menu contains top-level items.
  • .has-children marks items with dropdowns.
  • .nav-toggle button for mobile view.

Accessibility Note: Always include aria attributes on toggles and dropdowns for screen-reader compatibility.

Step 4: Styling the Menu

In assets/header.css (or your main CSS file):

cssCopyEdit.site-header__nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: relative;
}
.nav-menu {
  display: flex;
  gap: 2rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.nav-item { position: relative; }
.nav-link {
  text-decoration: none;
  font-weight: 500;
}
.sub-menu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  list-style: none;
  background: white;
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  margin: 0;
  padding: 1rem 0;
}
.has-children:hover .sub-menu {
  display: block;
}
.sub-item { padding: 0.5rem 1rem; }
.sub-link { text-decoration: none; color: #333; }

/* Mobile Styles */
@media (max-width: 768px) {
  .nav-menu { 
    flex-direction: column;
    display: none;
    background: white;
    width: 100%;
    position: absolute;
    top: 100%;
    left: 0;
  }
  .nav-item { margin: 0; }
  .nav-toggle { display: block; }
  .nav-menu.active { display: flex; }
}
  • Desktop: Flex row, hover-triggered dropdowns.
  • Mobile: Hidden by default, toggled via an active class.

Best Practice: Use CSS custom properties (e.g., --nav-gap: 2rem;) to let merchants adjust spacing via theme settings.

Step 5: Adding Toggle Behavior with JavaScript

In assets/header.js:

jsCopyEditdocument.addEventListener('DOMContentLoaded', () => {
  const toggle = document.querySelector('.nav-toggle');
  const menu = document.querySelector('.nav-menu');

  toggle.addEventListener('click', () => {
    const expanded = toggle.getAttribute('aria-expanded') === 'true';
    toggle.setAttribute('aria-expanded', !expanded);
    menu.classList.toggle('active');
  });
});

Then include this script in your layout:

liquidCopyEdit{{ 'header.js' | asset_url | script_tag }}
  • ARIA updates: Keep aria-expanded in sync.
  • active class: Triggers mobile menu visibility.

Expert Insight: Debounce resize events if you add more complex behaviors to avoid performance hits.

Step 6: Making It Merchant-Friendly

  1. Expose JSON Filename in Settings: Let merchants switch to a different menu without code edits: jsonCopyEdit// config/settings_schema.json { "name": "Header Menu", "settings": [ { "type": "text", "id": "menu_json", "label": "Menu JSON Filename", "default": "header-menu.json" } ] } Then in header.liquid: liquidCopyEdit{% assign filename = settings.menu_json | default: 'header-menu.json' %} {% assign menu_json = filename | asset_url | fetch %}
  2. Use Presets: Provide sample JSON files (e.g., header-menu-alt.json) with different menu structures.

Pro Tip: Validate JSON syntax with a linter before uploading to prevent Liquid parsing errors.

Conclusion

By centralizing your navigation in a JSON asset and harnessing Liquid’s parsing capabilities, you gain a flexible, maintainable header that scales with your store. Merchants can update links, labels, and dropdowns simply by editing or swapping JSON files—no developer intervention required. Pair this with responsive CSS and a lightweight toggle script, and you have a modern, accessible header navigation that delights users on any device. Start small with a basic menu, then layer on advanced features like mega menus or icon-based links to elevate the shopping experience.

Let's connect on TikTok

Join our newsletter to stay updated

Sydney Based Software Solutions Professional who is crafting exceptional systems and applications to solve a diverse range of problems for the past 10 years.

Share the Post

Related Posts