Shopify Builds>LUS Brands>Alt-text driven variant media gallery with tiered matching and full-screen lightbox

Alt-text driven variant media gallery with tiered matching and full-screen lightbox

The product gallery that consumes the alt-text match tiers: it builds the main slider, the thumbnail rail and a full-screen lightbox out of server-side verdicts, then re-filters the same nodes in the browser when the shopper changes variant.

Shopify attaches at most one variant to an image, so a product sold as size by formula cannot show a full set per variant. This gallery classifies every media item against the selected variant in Liquid, builds the Swiper track, thumbnail rail and full-screen lightbox from the verdicts, and parks hidden slides off-DOM so a variant change re-filters without a refetch.

Fact Strip

  • Client: LUS Brands — loveurcurls.com
  • Surface: Product detail page
  • Templates served: four product templates
  • Complexity: High
  • Attribution: Deploi-authored. The gallery, the lightbox and the video controls are ours. It runs in a theme built on Dawn 15.4.1 and deliberately reuses Dawn's product-thumbnail.liquid and product-media.liquid unchanged, including their responsive sizes math.
  • Status: Live, verified 2026-09-06
  • Evidence: Five Liquid snippets read from the theme, plus client working-session records from April and May 2026
  • Primary capability: Variant-aware content and media swapping

The Problem

Shopify attaches at most one variant to an image. For a catalog whose variants are a size axis crossed with a formula axis, that ceiling is the whole problem: a variant cannot own a set of images, only a single one. Stores work around it two ways, and both are bad. Either the same lifestyle and education imagery is uploaded again under every variant, which multiplies the media library by the variant count and makes every future swap a bulk edit; or the gallery shows everything to everyone, and a shopper looking at the 3oz scrolls past three shots of a bottle they are not buying.

The 2026 rebrand asked for the version the platform does not offer. Each variant shows its own hero image and its own set. Shared media — lifestyle, education, ingredient art — stays visible on every variant, uploaded once.

The Constraint

The association data had to come from something the content team can already edit in the Shopify admin, which meant image alt text: there is no per-variant media list in Liquid to read instead, and no new admin surface was on the table.

That decision has a second half that is easy to miss. Everything is computed in Liquid at render time, with no Storefront API call — and then computed again, identically, in JavaScript. The lightbox is assembled client-side and re-orders and re-filters slides after a variant change without going back to the server, so the same match rules have to exist on both sides of the paint. Two languages, one convention, nothing enforcing agreement between them.

The theme underneath is Dawn. Dawn has its own opinion about product media and updates the gallery itself on variant change. Any custom gallery has to tell Dawn's product-info.js to stand down, or the two fight over the same DOM.

What We Built

de_product_media_gallery.liquid — roughly 3,700 lines, the largest snippet in the theme — is the renderer. It calls the two classification snippets documented on the alt-text convention page for every media item, and everything below is built from the verdicts they return.

Server-rendered structure. The gallery builds the main Swiper track, the thumbnail rail and the slide order from the tiers, marks the slide whose alt contains main as that variant's hero, and emits a VariantAltKeys-<section> JSON island listing every variant's match key and visible-media count. That island is the contract between the Liquid pass and the JavaScript pass: the browser applies keys rather than re-deriving them. The gallery also advertises data-use-alt-variant-media, so Dawn's product-info.js stands down from its own media updating and leaves the slides alone.

The lightbox. A full-screen <dialog> holds a second pair of Swipers — a main track and a thumbnail track — with arrows, an overflow-aware thumb rail that switches between centered and scrollable depending on measured width, and slide and thumb "parks": off-DOM containers that hidden nodes are moved into rather than deleted. Parking matters because a variant change is a re-filter, not a re-render. Nodes that come back on the next variant are already built, already have their images decoded, and only need moving.

Video. Video slides get a hand-built control bar — play/pause, scrub, mute, elapsed time — that reveals on pointer or keyboard interaction and is wired through Dawn's deferred-media:loaded event, so a video only gains controls once its player actually exists. Video and 3D slides are forced to full width; 3D models keep the Shopify XR button and the model-viewer stylesheets.

Reuse. Slide and thumbnail rendering still goes through Dawn's product-thumbnail.liquid and product-media.liquid, unchanged. The custom code is the matching, the ordering and the chrome around them.

SERVER · LIQUID, RENDER TIMEBROWSER · ON VARIANT CHANGE Media library alt: "3oz + fragrance-free" Selected variant Match tiers perfect · partial · blank · hidden Gallery markup slider · thumb rail · lightbox Variant change event Apply keys applied, never re-derived Re-filter the same nodes Park — off-DOM kept built, images already decoded VariantAltKeys JSON island classify againstbuilds reads keyshiddenreturns no server round-trip
The same match rules exist twice, in two languages. Liquid classifies at render time so the first paint is already correct; the JSON island carries the verdicts across so the browser applies keys instead of re-deriving them. Hidden slides are parked, not deleted — which is why a variant change needs no second fetch.

Why This Way

Alt text was the only per-image field the merchandising team could already edit, and it doubles as accessibility copy, so the convention added no new admin surface and no new field to keep in step with the images.

Classifying in Liquid is what keeps the first paint correct. The gallery in the initial HTML is the gallery for the selected variant, which matters for crawlers, for shoppers with JavaScript disabled or slow, and for anyone who sees the page before a script has run. The JavaScript mirror exists for one reason only: the lightbox re-orders nodes client-side, and it can't ask the server to do that for it.

You pay for that twice, in two languages. The same rules are implemented once in Liquid and once in JavaScript, and the two only stay equivalent because someone moves them together. We took that on knowingly rather than give up the server-rendered first paint, which was the more valuable half.

Why Not an App

Variant-image-grouping apps solve exactly this problem, and if you want the behavior and don't want the maintenance, buying one is a reasonable call. The usual mechanism is to hold the mapping in the app's database and rewrite the gallery client-side after load.

That shape has two consequences we didn't want here. The images in the server-rendered HTML are the wrong ones until the app's script runs, so your shopper can watch a re-shuffle on a page whose hero image is the reason they came. And the mapping lives outside Shopify, edited separately from the media it describes. Keeping the association in the alt field makes the label and the image one object: rename it once, with no second record to update.

Implementation Notes

  • The alt convention is <option> + <option> [-- free text], with main marking that variant's hero image. Matching is case- and whitespace-normalized and handles non-breaking spaces pasted from spreadsheets.
  • Alt matching only engages when at least one media/variant pair classifies as perfect. Otherwise the gallery falls back to Dawn's own attached_to_variant? behavior, so an unlabeled product still works.
  • see_all_media_count > 5 gates both the See all thumbnail tile and the entire lightbox <dialog> markup, so short galleries never ship the dialog's DOM at all.
  • Swiper is self-hosted as a theme asset (swiper-custom.min.js, enqueued from theme.liquid) rather than loaded from a CDN, which keeps the slider on the store's own origin and out of a third party's release schedule.
  • Dawn's product-thumbnail.liquid and product-media.liquid are reused unchanged for slide and thumbnail rendering, including their responsive sizes math and zoom classes, so a theme update touches one less custom file.
  • Hidden slides and thumbs are moved into off-DOM park containers rather than removed, so re-showing them after a later variant change needs no re-render and no second fetch.

Edge Cases

  • Images with no alt text are treated as shared and shown for every variant, which is the safe default while a catalog is only partly labeled.
  • Media claimed perfectly by another variant is hidden rather than shown as a partial match, so a size-specific shot cannot leak into a sibling variant's gallery.
  • Single-media and no-media products skip the slider chrome entirely through the single_media_visible and hide_mobile_slider flags, so a one-image product does not render dead arrows and an empty thumbnail rail.
  • 3D models still get the Shopify XR button and the model-viewer stylesheets; video and model slides are forced to full width regardless of the layout the images use.
  • The lightbox thumbnail rail switches between centered and scrollable depending on measured overflow, so a four-thumb product and a twenty-thumb product both look deliberate.
  • A VariantAltKeys JSON parse failure degrades to showing every media item rather than throwing, so the worst case is an unfiltered gallery instead of an empty one.

Platform Primitives Used

  • section-rendering-api — Dawn's variant change runs through it; the gallery opts out of the media half of that update with data-use-alt-variant-media and re-filters the slides it already holds.
  • custom-elements — the video control bar hangs off Dawn's deferred-media custom element, listening for deferred-media:loaded rather than polling for a player.
  • metafields — the product page around the gallery is assembled from product metafields. The gallery itself deliberately takes none: its inputs are the media library and its section settings.

Where It Runs

Product detail page only, mounted by four product templates: the default product template, the 2026 redesign template, the kids redesign template and the waiting-list template. The same snippet serves all four, so a change to the match behavior lands on every product page at once rather than being configured per template.

What This Demonstrates

  • Variant-aware content and media swapping — the primary capability, and the largest single implementation of it in the corpus. Media, hero selection and slide order all resolve from the selected variant.

In the taxonomy this build is also indexed under PDP media gallery and zoom, and under bundle pricing, since the bundle templates share the gallery.

How We Know

Five Liquid snippets read directly from the theme, roughly 4,000 lines across them, plus client working-session records from April and May 2026. Three separate documented requirements meet in this one component: an aligned decision that product-page images must update with the selected variant, a requirement for an image zoom and magnifier on the PDP, and a later aligned decision to swap image positions on the product page to match the design.

The alt-text mechanism itself appears in none of them. The requirements are documented; the convention that satisfies them was a code-side decision, and this page reconstructs it from the snippets.

Related Builds

The Buy-vs-Build Question

An app buys the behavior for a monthly fee and a client-side repaint. Building it bought a correct first paint and a mapping held in the store's own data, and cost a large custom snippet plus a JavaScript mirror of its rules. That trade goes the other way for plenty of catalogs. The test: Variant limits and complex options: buy or build?

Ready to Build a PDP That Keeps Up With Your Catalog?

You dream it. We build it. If your product page is working around Shopify's variant model instead of with it, Contact us today and we'll walk you through the options — the build, the app, and where the line sits.

More builds