Shopify Builds>LUS Brands>Variant-aware PDP hero banner driven by a 'heroic' metaobject

Variant-aware PDP hero banner driven by a 'heroic' metaobject

An editorial hero below the buy box that resolves its art, headline and copy from a per-variant metaobject, and refetches its own content through the Section Rendering API when the shopper switches variant.

A section placed in a shared product template renders the same settings for every product, and an image_picker setting cannot hold Liquid. This banner resolves a heroic metaobject from the variant first and the product second, then refetches only its inner content node through the Section Rendering API on variant change. One section instance, editorial art on every product page.

Fact Strip

  • Client: LUS Brands — loveurcurls.com
  • Surface: Product detail page
  • Templates served: four product templates
  • Complexity: High
  • Attribution: Deploi-authored. The section, its refresh script and both parallax paths are ours, written for a theme built on Dawn 15.4.1 and hooked into Dawn's own PUB_SUB_EVENTS variant event rather than replacing it.
  • Status: Live, verified 2026-09-06
  • Data: One metaobject definition, heroic, read through custom.heroic at both variant and product level
  • Primary capability: Variant-aware content and media swapping

The Problem

A theme section renders the settings it was given. Put one banner section into a shared product template and every product using that template gets the same artwork, the same headline and the same copy — that is what "shared template" means. The alternative Shopify offers is a separate template per product, which turns an editorial change into a theme deployment and puts merchandising work inside a code repository.

The 2026 product pages needed the opposite arrangement. Each product, and in some cases each variant, gets its own editorial hero below the buy box: different artwork, different headline, different supporting copy. Nobody wanted a merchandiser adding and configuring a banner section on every product template, one product at a time. Product data had to be the source of truth, not template configuration.

The Constraint

Three platform facts shaped this, and none of them were negotiable.

An image_picker setting cannot hold Liquid. That is the obvious first attempt — point the picker at a metafield — and it does not work: the setting stores a media reference chosen in the editor, not an expression evaluated at render. Product-driven art has to be resolved in the section's own Liquid, downstream of the setting.

Dawn does not re-render arbitrary sections on a variant change. It updates the product information section and leaves the rest of the page alone — correct for most sections, wrong for this one. A banner that changes with the variant has to notice the change itself and fetch its own new markup.

And CSS scroll-linked animation is not universally supported. view-timeline is the right way to run a parallax effect and it is not available to every shopper, so the effect needed a scripted fallback that stands down where the CSS version works.

What We Built

de_image_banner.liquid starts by resolving content, not by rendering it. It looks for a heroic metaobject on variant.metafields.custom.heroic first and product.metafields.custom.heroic second, then uses that metaobject's img, img_mob, title and text fields to override the section's own image, mobile image, heading and body settings. Variant-first resolution is what lets a single product carry different art per variant while a simpler product carries one entry at the product level.

The refresh. The banner renders inside a wrapper keyed to the section — #if_banner_{{section.id}} — with an inner #IfHeroicContent-{{section.id}} holding everything that can change. An inline script subscribes to PUB_SUB_EVENTS.variantChange, fetches {{product.url}}?variant=<id>&section_id=<id> through the Section Rendering API, parses the response with DOMParser, and swaps only the inner content node into place. The outer wrapper — the element the parallax is bound to — is never replaced.

Art direction. The banner emits a <picture> with a separate mobile <source>, so desktop and mobile can carry genuinely different crops rather than one image squeezed into two aspect ratios. Headings support a [[highlight]] token: the delimited span is wrapped and painted in a merchant-set highlight color, which keeps a design flourish inside a plain text field. Per-block typography — size, line height, weight, color, desktop and mobile — is emitted as scoped CSS custom properties rather than as inline styles on each element.

Parallax, twice. The effect is implemented as a pure-CSS view-timeline keyframe animation scoped per section id, and again in de_image_banner.js, which checks CSS.supports('animation-timeline: view()') on load and disables itself when the CSS version is available. Where it is not, the script drives translate3d from getBoundingClientRect().top multiplied by data-parallax-speed, inside a requestAnimationFrame-throttled scroll and resize handler.

Why This Way

Making the metaobject the source of truth means one section instance in the shared product template serves every product page. The content team edits products; nobody edits templates. That is the whole point of the build, and everything else is consequence.

Swapping only the inner content node — rather than replacing the whole section, which is what a naive Section Rendering implementation does — preserves the parallax host element and the scroll listeners already bound to it. Replace the wrapper and you re-bind listeners on every variant click, or you lose the effect.

Two costs came with it. The parallax logic exists in two forms, CSS and JavaScript, and both have to stay true to the same motion. And a variant change now costs a network round trip for this section, on top of the one Dawn already makes for product information. We took the round trip because the alternative — shipping every variant's banner markup up front and toggling it — scales badly on products with many variants and puts unused artwork in the initial payload.

Why Not an App

This sits where a per-product hero app or a page-builder block would normally go. Both are reasonable products, and both make the same structural choice: the artwork and copy live in the vendor's own store, attached to a page, not to a product.

That is precisely what made the content unmaintainable before. Content attached to a page has to be recreated when a product is added, and it has no idea what a variant is — so per-variant art either is not possible or is faked with visibility rules. Content attached to product data arrives with the product, gets exported with the product, and answers a variant question by looking at the variant. The metaobject also gives the fields names and types, so a merchandiser filling in img_mob knows what it is for without being told twice.

Implementation Notes

  • The variant-level metafield wins over the product-level one, so a single product can carry per-variant art while products that do not need it define custom.heroic once at product level.
  • The section returns nothing at all when no image resolves from either the metaobject or its own setting, so an unconfigured product renders no empty banner shell and no reserved whitespace.
  • The refresh script binds once per element via a dataset.ifHeroicRefreshBound flag, and no-ops entirely when data-product-url is absent — which is how it stays inert off the product page.
  • If subscribe or PUB_SUB_EVENTS are not yet defined when the script runs, binding is deferred to DOMContentLoaded with { once: true } rather than assuming theme script order.
  • The parallax script guards against double-binding globally through a window.__ifImageBannerParallaxListeners flag, so two banners on the same page share a single set of scroll listeners instead of stacking two handlers.
  • de_image_banner.js is enqueued only when the section's image_behavior setting is set to parallax-y, so a page using a static banner never downloads the fallback logic at all.
  • Per-block typography settings are emitted as scoped CSS custom properties, which is what allows desktop and mobile values to differ without a second block or a media-query-per-block.

Edge Cases

  • A prefers-reduced-motion preference clears the transform outright instead of animating it, rather than running the same motion at a slower speed and calling that a concession.
  • Non-product placements skip the variant-refresh script entirely, because product is blank there and the script has nothing to subscribe to.
  • A product with no heroic metaobject gets no banner rather than a fallback one: with nothing to resolve an image from, the section returns nothing and the page simply has no hero in that slot. On the four product templates it runs on, the metaobject is the only source of banner content.
  • An AbortError from the section fetch is swallowed — that is a superseded request, not a failure — while other errors are logged rather than silently discarded.
  • Mobile artwork falls back to the desktop image when img_mob is unset, so a half-filled metaobject still renders correctly on a phone.
  • The separate mobile <source> keeps the aspect-ratio padding correct on small screens, which is what stops the banner reflowing as the image decodes.

Platform Primitives Used

  • metaobjects — a heroic definition holds the art, mobile art, title and text as named typed fields, so the content has a shape rather than being four loose metafields.
  • metafieldscustom.heroic references that metaobject at both variant and product level, which is what makes variant-first resolution possible.
  • section-rendering-api — the banner refetches itself by section id on variant change and swaps only its inner content node.
  • liquid-schema-settings — the metaobject overrides settings that still exist in the schema, so the same section can be mounted as a plain configured banner in placements that fill those settings in directly.
  • theme-blocks — heading, text and button are blocks with their own typography settings, emitted as scoped custom properties.

Where It Runs

Product detail page, on four product templates: the default product template, the 2026 redesign template, the kids redesign template and the waiting-list template. All four mount it with image_behavior set to parallax-y. One section instance per template, and the product decides what it says.

What This Demonstrates

In the taxonomy this build is also indexed under art-directed hero and banner systems.

How We Know

One section file and two script assets read directly from the theme, roughly 1,000 lines in total, plus a metafield export that carries custom.heroic at both product and variant level, and client working-session records from May 2026.

This is the clearest evidence chain in the corpus. A working session recorded that hero images were cut off on desktop because of aspect-ratio differences with mobile; the aligned decision was to create separate mobile and desktop hero fields. The metafield export then shows custom.heroic defined at both levels, and the code reads it variant-first with separate desktop and mobile art. Requirement, data model and implementation line up.

Related Builds

The Buy-vs-Build Question

The build-side argument here is not the banner. It is that the content ends up inside Shopify's own data model, where it exports, versions and travels with the product instead of living in a vendor's page store. The cost is that somebody has to maintain the metaobject entries. We work through where that trade lands in Variant limits and complex options: buy or build?

Ready to Put Your Product Pages in Your Merchandisers' Hands?

You dream it. We build it. If your product pages need to say something different on every product — and your team is editing templates to do it — contact us today and we'll map out what it takes to move that content into product data.

More builds