[Horizon] Adding a Banner Image to the Drawer Menu

"I want to show a campaign banner in the drawer menu."

When a visitor opens the mobile hamburger menu, displaying a banner image below the navigation is an effective way to promote coupons or announce a sale. There's a guide for Dawn/Rise themes, but how does Horizon handle this?

The short answer: it's achievable in Horizon, but because of how its files are structured, editing vendor files is unavoidable. This article covers the full process — from tracing the relevant files to making image and link configurable through the theme editor.

Note: this article deliberately avoids creating new custom.* files, which is the approach recommended in Horizon's customization guidelines. Instead, it edits vendor files directly — and explains in detail why.

Final Result

A banner image appears at the bottom of the drawer menu's navigation, and the image and its link can be freely changed through the menu block settings in the theme editor. If no image is set, nothing is displayed — the default appearance is completely unchanged.

Horizon theme — drawer menu banner example

How Dawn Handles This

In Dawn/Rise themes, the header section and drawer menu live in the same file.

// Dawn/Rise

sections/header.liquid    ← schema definitions and drawer rendering code, all here

You simply add an image_picker and a url setting to header.liquid's schema, then add the banner rendering code to the drawer section in the same file. The entire change is contained in one file.

Horizon's Drawer Menu: File Separation

In Horizon, the drawer menu functionality is split across three files.

// Horizon

sections/header.liquid                 ← section container
  └─ blocks/_header-menu.liquid          ← menu block (schema defined here)
       └─ snippets/header-drawer.liquid   ← drawer HTML structure (rendering happens here)

What Dawn keeps in a single file — the schema definition and the drawer rendering code — Horizon splits into separate files.

Why is it structured this way?

Horizon uses a "theme block architecture." Menu settings — link list, font, accordion behavior, etc. — are managed in the block's schema, while HTML rendering is delegated to a snippet. This separation allows the same _header-menu block to be reused across three contexts: the desktop menu, the mobile drawer, and the navigation bar.

Tracing the Files

Let's walk through how you'd actually trace the files to add a banner to the drawer menu.

Step 1: header.liquid — Follow the block delegation

Opening sections/header.liquid, you'll find that drawer rendering is delegated to the _header-menu block.

// _header-menu invocations in header.liquid (3 places)

{% content_for 'block', type: '_header-menu', id: 'header-menu' %}                                        ← desktop menu
{% content_for 'block', type: '_header-menu', id: 'header-menu', variant: 'mobile' %}             ← mobile drawer
{% content_for 'block', type: '_header-menu', id: 'header-menu', variant: 'navigation_bar' %}    ← navigation bar

The key point is that the block file name is hardcoded in the type parameter of content_for 'block'. This is what leads to the "custom.* approach not being viable" situation explained later.

Checking header.liquid's schema, there is no blocks definition — the header section only makes static block calls and has no mechanism to accept custom blocks.

Step 2: _header-menu.liquid — Follow the drawer snippet delegation

Opening blocks/_header-menu.liquid, you'll see it renders differently depending on the variant value. When variant: 'mobile', drawer rendering is delegated to the header-drawer snippet.

// _header-menu.liquid (top section)

{% case variant %}
  {% when 'mobile' %}
    <div class="header__drawer">
      {% render 'header-drawer',
        linklist: block_settings.menu,
        data_header_drawer_type: 'mobile-drawer' %}
    </div>
  {% when 'navigation_bar' %}
    ...
{% endcase %}

At the bottom of this file is the schema, which defines all configurable settings for the menu block — everything in the "Menu" block settings panel in the theme editor. This is where you add the banner settings.

However, the banner rendering (HTML output) must happen in the called snippet, not here.

Step 3: header-drawer.liquid — Identify where to insert the banner

Opening snippets/header-drawer.liquid reveals the HTML structure inside the drawer.

// Inner structure of header-drawer.liquid

<div class="menu-drawer">
  <button class="menu-drawer__close-button">...</button>
  <nav class="menu-drawer__navigation">
    <ul>...menu items...</ul>
  </nav>
                                                  ← ★ banner insertion point
  <div class="menu-drawer__utility-links">...language/currency selectors...</div>
  <div class="menu-drawer__featured-content">...collection/product images...</div>
</div>

The natural insertion point is right after the navigation (</nav>), just before the utility links — so the banner appears as users scroll past the menu items.

This snippet cannot have its own schema, but it can access the parent block's settings via block_settings (= block.settings). This works because Shopify snippets inherit the scope of their caller.

Snippet scope

At the top of header-drawer.liquid, you'll find assign block_settings = block.settings, which stores the parent block's settings in a variable. Any settings added to _header-menu.liquid's schema can be accessed inside this snippet as block_settings.drawer_banner_image, etc.

What to Change

The approach is the same as in the previous article on badge customization: rather than removing existing code, add new settings to the schema to expose controls in the theme editor, using default values to preserve the original behavior.

Change 1: Add banner settings to _header-menu.liquid's schema

Add a header, an image_picker, and a url field at the end of the settings array (after drawer_dividers).

// Add to the end of the settings array in _header-menu.liquid's schema

    {
      "type": "header",
      "content": "t:content.drawer_banner"
    },
    {
      "type": "image_picker",
      "id": "drawer_banner_image",
      "label": "t:settings.drawer_banner_image"
    },
    {
      "type": "url",
      "id": "drawer_banner_link",
      "label": "t:settings.drawer_banner_link"
    }

image_picker lets merchants upload or select an image from the admin. url provides a link picker for pages, collections, products, or external URLs. Neither has a default, so both start empty — meaning no banner is shown by default.

Change 2: Add banner rendering code to header-drawer.liquid

Insert the banner rendering code immediately after the navigation (</nav>) in the drawer's HTML structure.

// Add immediately after </nav> in header-drawer.liquid

      {%- if block_settings.drawer_banner_image -%}
        <div class="menu-drawer__banner menu-drawer__animated-element"
          style="--menu-drawer-animation-index: {{ linklist.links.size }};">
          {%- if block_settings.drawer_banner_link != blank -%}
            <a href="{{ block_settings.drawer_banner_link }}">
              {{ block_settings.drawer_banner_image | image_url: width: 800 | image_tag: class: 'menu-drawer__banner-image', loading: 'lazy', widths: '400, 600, 800' }}
            </a>
          {%- else -%}
            {{ block_settings.drawer_banner_image | image_url: width: 800 | image_tag: class: 'menu-drawer__banner-image', loading: 'lazy', widths: '400, 600, 800' }}
          {%- endif -%}
        </div>
      {%- endif -%}

Adding the menu-drawer__animated-element class integrates the banner into the slide-in animation. The --menu-drawer-animation-index CSS variable controls animation delay order, set to match the number of menu items.

The image_url and image_tag filters automatically generate CDN-resized images with srcset attributes for optimal delivery across devices.

Change 3: Add labels to locale files

To display labels in the theme editor, add translation keys to the locale files.

// en.default.schema.json — add to "content" object:
"drawer_banner": "Drawer banner"

// add to "settings" object:
"drawer_banner_image": "Banner image",
"drawer_banner_link": "Banner link"
// ja.schema.json — add to "content" object:
"drawer_banner": "ドロワーバナー"

// add to "settings" object:
"drawer_banner_image": "バナー画像",
"drawer_banner_link": "バナーのリンク先"

Change 4: Add CSS

Add CSS to style the banner in assets/custom.cssno vendor file changes required here.

.menu-drawer__banner {
  padding: 16px 20px 0;
}

.menu-drawer__banner-image {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--style-border-radius-card, 8px);
}

Using Horizon's --style-border-radius-card CSS variable automatically inherits the corner radius from theme settings.

Drawer banner settings in the theme editor

Summary of Changed Files

File Type Change
blocks/_header-menu.liquid Vendor file edit Add banner settings (image_picker + url) to schema
snippets/header-drawer.liquid Vendor file edit Add banner rendering HTML
locales/en.default.schema.json Vendor file edit Add English labels
locales/ja.schema.json Vendor file edit Add Japanese labels
assets/custom.css Custom file Add banner styles

Additionally, the following files needed to be read and understood before making changes.

File Why it needed to be understood
sections/header.liquid Starting point. Confirms block delegation and schema constraints (no blocks definition).
sections/header-group.json Confirms the header group composition and runtime block settings for _header-menu.

The "Don't Touch Vendor Files" Guideline vs. Reality

The safe customization guide recommends keeping all changes in custom.*-prefixed files and never touching vendor files.

What would happen if we tried the custom approach here?

What happens when you try the custom approach?

Say you copy _header-menu.liquid and create custom._header-menu.liquid. To use this custom block, you'd need to update all three content_for type references in header.liquid.

// Three places in header.liquid that would need to change

{% content_for 'block', type: '_header-menu', id: 'header-menu' %}
{% content_for 'block', type: 'custom._header-menu', id: 'header-menu' %}
                        ↑ × 3 places

The header-group.json (the theme editor's saved state) would also need its block type references updated.

Approach Vendor files touched
Edit _header-menu.liquid + header-drawer.liquid directly 4 files (block + snippet + 2 locale files)
Create custom._header-menu.liquid Still requires: 3 places in header.liquid + header-group.json + header-drawer.liquid

The previous article showed a case where the custom approach cascaded to 11 files. This case is smaller, but the root cause is the same: the content_for type parameter is a hardcoded reference — renaming a block file inevitably requires updating every caller.

For this customization, the approach taken is to directly edit _header-menu.liquid and header-drawer.liquid. Since the change is purely additive — only adding settings, no deletions or rewrites — default values preserve the original behavior. Additive schema changes are also less likely to cause conflicts during a theme update merge.

Using Git for version control

When customizations touch vendor files, version control with GitHub (or Bitbucket, etc.) is strongly recommended. Track the original theme on an upstream/horizon branch and manage customizations on main. When a theme update arrives, git merge upstream/horizon lets you review and merge the diff. The Horizon safe customization guide covers this in detail.

Same Customization, Different Path: Dawn vs. Horizon

Even for the exact same requirement — "add a banner to the drawer menu" — the approach differs significantly between Dawn and Horizon.

Dawn / Rise Horizon
Schema definition header.liquid blocks/_header-menu.liquid
Rendering code header.liquid (same file) snippets/header-drawer.liquid (separate file)
Files changed 1 file 4 files (+ 1 CSS)
Files to understand 1 file 4 files (header.liquid → _header-menu.liquid → header-drawer.liquid + header-group.json)

Horizon separates schema definitions from HTML rendering into different files so the same menu block can be reused across desktop, mobile, and navigation bar contexts. In the no-code theme editor, a single setting change applies to all three — a rational design. But when customizing through code, schema and rendering must be changed in separate files, which increases tracing overhead.

Keeping the following in mind when customizing Horizon will help:

  1. First, check if the theme editor can handle it. Horizon's block system is more flexible than expected — many requirements can be met through editor settings alone.
  2. If CSS alone is enough, put it in custom.css. Keep vendor file touches to a minimum.
  3. When vendor files must be edited, use additive changes only. Adding to the schema — not removing or rewriting — lets default values preserve the original behavior.
  4. Always use Git when touching vendor files. Document what you changed and where.

Related Articles

Back to blog