Logo
Developer reviewing Shopify Liquid partial layout for soft navigation on a laptop

Shopify Liquid Partials: Swap Main Content Without Reloading Header and Footer

Shopify Liquid Partials: Swap Main Content Without Reloading Header and Footer

Jul 23, 2026

Octavian Contis 8 minutes

Share

Shopify Liquid Partials: Swap Main Content Without Reloading Header and Footer

Merchants feel it before developers name it. Every full page load re-renders the header, re-runs footer scripts, and re-initialises app snippets that live in global chrome. The product grid or policy page in the middle changes, but the browser still pays the cost of the whole document.

Shopify's Liquid July '26 developer preview adds {% partial %} and @shopify/partial-rendering. The use case we cared about first was not micro-updates for fun. It was persistent shell navigation: header and footer load once per visit, and in-theme links refresh only the main content that actually changes.

That aligns with the browser's Declarative Partial Updates direction (named server HTML regions, swapped in place). It is not a mandate to rebuild the theme as a JavaScript app. It is progressive enhancement on top of normal Liquid, the same foundation we argue for in Online Store 2.0 and high-performance theme work.

Introduction

For years, the honest answer to "can we keep the nav and only load new content?" on Shopify was a custom stack: intercept clicks, fetch HTML, guess which nodes to replace, and hope Section Rendering IDs still match after a merchant edits the theme. Every agency theme reinvents the same failure modes: double-fired analytics, cart counts that drift, focus lost after swap, and header scripts that assume a full DOMContentLoaded on each URL.

The July '26 {% partial %} preview does not magically implement routing. It does give you a stable name for the main column in layout Liquid, and a supported fetch / apply / refresh path so the server returns HTML for that name only. That is the missing piece for shell navigation if you are willing to own history, title updates, and accessibility yourself.

We implemented this on block-first skeleton theme work: page-content around content_for_layout, header and footer groups untouched on in-theme link navigation, optional nested refresh for cart chrome when line items change. This article walks through that architecture first. Collection sort partials and multi-region fetches on a single template are the same tool with a smaller boundary, not the headline win.

If you are evaluating preview features for a live merchant roadmap, treat soft nav as enhancement, not dependency. Checkout, account, and external links should remain full loads until you have fallbacks and monitoring in place.

Why we wrapped only the main column

Classic Shopify themes already separate layout from template output via content_for_layout. Partials make that boundary addressable from JavaScript.

On first load, the buyer gets a normal full HTML response: header group, <main>, footer group, analytics, cart bootstrap, the lot.

On subsequent in-theme navigation (when you opt in with soft nav):

  1. Header and footer stay in the DOM. You do not fetch them again unless you choose to.
  2. You partials.fetch('page-content', { url }) for the destination URL.
  3. You partials.apply(update) so only the named region inside <main> swaps.

Less flicker, less repeated work in global chrome, and fewer chances for app scripts in the header to fight each other on every click.

If your theme is already brittle under app load, read common theme mistakes that kill conversion before you add interceptors. Partials fix where HTML lands; they do not fix how much JavaScript you shipped in the shell.

Layout shape on the preview

The preview also ships {% block %} for block-first templates. For soft navigation, the important tag is {% partial %}.

In layout/theme.liquid, the pattern we used looked conceptually like this:

<body>
  {% sections 'header-group' %}

  <main id="MainContent">
    {% partial 'page-content' %}
      {{ content_for_layout }}
    {% endpartial %}
  </main>

  {% sections 'footer-group' %}
</body>

page-content is the contract name. Liquid renders it on first paint. JavaScript uses the same string when the buyer follows an in-theme link.

Preview requirements: enable Liquid July '26 on a development store. Outside preview, Liquid may reject the tag. Always ship a fallback: unmodified links, full page loads, no dependency on soft nav for checkout or core paths.

Soft navigation flow (what you still own)

Shopify does not ship automatic SPA-style link handling. Partials give you the swap mechanism, not the router. Our storefront flow:

  1. Leave header and footer outside the partial you replace on navigation.
  2. Intercept same-origin in-theme links (we used a data-partial-nav convention), preventDefault, history.pushState with the destination URL.
  3. partials.fetch('page-content', { url }) then partials.apply(update).
  4. On popstate, partials.refresh('page-content') so back and forward match window.location.
  5. You still implement document title, meta where relevant, analytics page views, focus management, and any active nav state that must track the new URL (often a follow-up partial or a small DOM update, not a full header re-fetch).

First visit, no-JS buyers, and crawlers keep full document loads. That is the line between soft nav and a fragile app shell.

JavaScript: fetch, apply, and refresh

@shopify/partial-rendering centres on three calls for this pattern:

APIRole in shell navigation
partials.fetch('page-content', { url })Request fresh main-column HTML for the destination URL. Build url from window.location or Liquid routes, not hardcoded paths, so markets and locales keep working.
partials.apply(update)Replace the live page-content region. Optional View Transitions wrapper where supported.
partials.refresh('page-content')Re-sync main content with the current URL after back/forward without rebuilding the request yourself.

Details that matter in production:

  • Pass AbortSignal so a slow click cannot overwrite a newer navigation.
  • Set aria-busy on <main> (or the partial root) while the fetch runs; use a live region when the swap should be announced.
  • After apply(), re-query nodes inside page-content before re-binding animations or listeners. The old subtree is gone.

What we deliberately do not re-fetch on route change

The wrong instinct is to request every named region in the layout response on each navigation, including header cart markup. That recreates full reload costs in smaller pieces.

Persistent shell:

  • Header group, footer group, and global snippets: first load only for route changes.
  • page-content: every soft navigation.

Targeted exceptions (same page or event-driven, not every link):

  • A nested header-cart partial can refresh on shopify:cart:lines-update so the badge stays accurate without reloading the whole header or the main column.
  • Collection sort or filter can fetch product-grid, product-count, and toolbar together on that template without touching layout chrome. That is the same partial machinery, different boundary.

Cart mutations stay on Shopify.actions and shopify:cart:* events. Partials re-render markup; they do not replace Shopify's event model. That separation matters on stores where checkout abandonment already traces back to script churn in global chrome.

Keep {% sections 'header-group' %} as a direct child of <body> for theme editor discovery, not buried inside block wrappers that confuse the editor.

Not partial hydration (and not a free Turbo clone)

Developers often mislabel this as partial hydration. That term usually means attaching JavaScript to islands while most HTML stays static. Here, the server still owns HTML generation; the client replaces a named region with fresh server output. apply() preserves focus, selection, and form values where the runtime allows.

Lower-level Declarative Partial Updates platform APIs (streaming markers, <template for>, insertion APIs) sit underneath this story. For theme work, @shopify/partial-rendering is the practical layer.

Theme editor: the sharp edge

Partials are a storefront preview feature. request.design_mode follows a different path. Unconditional partial wrappers can yield Unknown tag 'partial' and break header group mounting in the theme editor.

What worked:

  • {% if request.design_mode %} branches: plain Liquid in the editor, partial wrappers on the storefront.
  • Disable soft-navigation scripts in design mode so they do not fight editor clicks.

If your team lives in the theme editor daily, budget time for this split before you promise merchants shell navigation on production timelines.

Checklist before you ship shell navigation

  1. Name parity: {% partial 'page-content' %} and partials.fetch('page-content') must match exactly.
  2. Chrome outside the partial: header and footer are not in the region you replace on link navigation.
  3. Fallback: failed fetch or missing runtime falls back to window.location or normal link behaviour.
  4. Stale requests: abort or ignore superseded fetches.
  5. Loading and announcements: aria-busy, live regions where swaps are silent.
  6. Editor vs storefront: request.design_mode branches for tags and nav scripts.
  7. Ownership: title, analytics, focus, and nav active state have an explicit owner on each navigation.

Conclusion

Liquid partials gave us a documented name for the boundary merchants already imagine: global chrome stays put, the main column updates. That is the game-changing day-to-day story on heavy DTC themes, more than yet another Ajax patch for a sort dropdown.

Start on the preview with page-content and one class of in-theme links before you expand to collection partials or nested cart regions. If soft nav intersects checkout extensions, app conflicts, or a catalogue model that already fights structured data and filters, scope it inside a broader theme roadmap.

For layout boundaries and partial strategy on an established store, see our Shopify theme development capability or book a stack assessment to map preview features against what your stack can maintain long term.

Frequently Asked Questions

The {% partial %} tag marks a named region of server-rendered HTML inside a Liquid template or layout. JavaScript can request fresh HTML for that name later and swap it into the live page without a full reload. The partial name is the contract between Liquid and @shopify/partial-rendering. Content inside the tag is ordinary Liquid that renders on first load like any other template output.

Yes, when you draw boundaries in the layout. Leave header and footer markup outside the partial you replace on navigation. Wrap content_for_layout (or your main column) in a partial such as page-content. On in-theme link clicks, call partials.fetch('page-content', { url }) and partials.apply(update) so only that region updates. Header and footer stay in the DOM from the first full load until you intentionally refresh them, for example after a cart event on a nested partial.

The Section Rendering API returns HTML for a section ID through Ajax endpoints you wire up in theme JavaScript. Partials name inline regions in template or layout markup and use partials.fetch() and partials.apply() to replace matching regions. Section Rendering remains strong for section-scoped updates and apps. Partials fit when the unit of change is a named subtree you already authored in Liquid, such as the main content shell on soft navigation.

No. Partial hydration usually means static HTML with JavaScript attached to specific islands, common in RSC or islands architectures. Liquid partials are region replacement: the server renders HTML, the client fetches updated HTML for named regions, and apply() swaps content while preserving focus, selection, and form values where possible. First paint stays normal server-side rendering.

You need a development store with the Liquid July '26 feature preview enabled. The {% partial %} tag and helpers ship in developer preview, not every production storefront yet. Outside preview context, Liquid may treat partial as an unknown tag, so plan fallbacks: normal links, full page loads, and no soft navigation when the runtime is unavailable.

The theme editor uses a different rendering path than the buyer-facing storefront. Unconditional {% partial %} wrappers can trigger unknown tag errors and prevent section groups from mounting in the editor. Gate partial markup with request.design_mode: plain Liquid in the editor, partial wrappers on the storefront. Keep section groups such as {% sections 'header-group' %} as direct children of <body> in the layout when the theme editor must discover them.

Bring in theme engineering when soft navigation must stay accessible, market-aware, and compatible with checkout, apps, and analytics, or when partial boundaries cross merchant workflows. Partials reduce full reloads but they do not replace information architecture. A structured Shopify stack audit helps decide whether a persistent shell belongs in your roadmap or whether full loads are enough for now.

Related Articles