Shopify Builds>LUS Brands>Alt-text convention that maps product media to variants without metafields

Alt-text convention that maps product media to variants without metafields

Two Liquid snippets turn the alt text merchandisers already type in the Shopify media library into a variant match key and a four-tier match verdict, so a product gallery can filter media per variant at render time.

Shopify associates media with a variant through one 'variant image' field, which cannot express 'these four shots belong to the 8.5oz fragrance-free version.' Two Liquid snippets turn the alt text merchandisers already type into a variant match key and a four-tier match verdict, so the product gallery filters media per variant with no metafield, no app and no client-side repaint.

Fact Strip

  • Client: LUS Brands — loveurcurls.com
  • Surface: Product detail page
  • Templates served: four product templates
  • Complexity: High
  • Attribution: Deploi-authored. Both snippets are ours. They run inside a theme built on Dawn 15.4.1, and the gallery that calls them reuses Dawn's own thumbnail and media snippets unchanged.
  • Status: Live, verified 2026-09-06
  • Evidence: Two snippets, about 140 lines between them — plus client records that document the requirement and never mention the mechanism
  • Primary capability: Variant-aware content and media swapping

The Problem

Shopify associates a media item with a variant through a single field: the variant image. One image, one variant. That field cannot express these four shots belong to the 8.5oz fragrance-free version, and Liquid offers no per-variant media list to ask instead — product.media is one flat array for the whole product.

This catalog needs the thing that field cannot say. Products sell in size and fragrance combinations, and a shopper moving from the 3oz to the 8.5oz should get that size's own set of images, not one hero shot and a shared remainder. Other media is universal — lifestyle photography, education panels, ingredient art — and stays visible for every variant rather than being duplicated onto each.

The merchandising team had already done half the work without being asked. They were labeling uploads in the media library's alt field as they went. The mapping existed. Nothing read it.

The Constraint

No new metafield was to be introduced. The source of truth had to stay the alt text merchandisers already type — the one field they edit without a definition, a migration or a developer in the loop.

That puts the whole problem inside Liquid, with Liquid's string filters and nothing else. No regular expressions. No case-insensitive compare. No sets, no dictionaries. Comparing two strings means normalizing both by hand first.

And the input is human. Alt text arrives with commas on one product and plus signs on the next, mixed case throughout, and non-breaking spaces pasted out of spreadsheets that look exactly like real spaces and are not. Some entries carry a free-text note after the option list, written for the next person rather than for a parser. All of it had to count as valid input. Rejecting it would mean asking merchandisers to type differently, and their workflow is what the constraint protects.

What We Built

Two composable snippets, each doing one job, both callable from anywhere in the theme.

de_variant_media_alt_key.liquid builds the canonical key for a variant: it joins option1, option2 and option3 with +, falls back to variant.title when the options are empty, then downcases and collapses runs of whitespace. Every comparison on the page is made against that string.

de_variant_media_alt_classify.liquid does the matching. It normalizes the media's alt text the same way: replaces the U+00A0 non-breaking space, downcases, converts , and , to +, and keeps only the portion before a -- comment suffix, so a merchandiser's note never reaches the matcher. It splits the remainder on + and returns one of four verdicts:

  • blank — the media has no alt text, so it is shared and shows for every variant.
  • perfect — every populated option of the selected variant appears in the alt.
  • partial — some options match, and no other variant claims this media perfectly.
  • hidden — another variant owns it.

The partial rule is where most of the code lives. Before demoting a media item to partial, the snippet loops every other variant on the product and checks whether that variant matches the alt perfectly. If one does, the verdict flips to hidden. That check is what stops a shot labeled for the 8.5oz from leaking into the 3oz gallery, while still letting a shot labeled only with a fragrance appear under every size of it.

The verdict comes back as bare text, because a Liquid snippet has no return value. Callers wrap the render in {% capture %} and pipe it through strip. de_product_media_gallery.liquid consumes it in about twenty places: to add the if-media-gallery__variant-hidden class to a slide, to count the media actually visible for the selected variant, and to decide whether the product falls back to a single-media layout. That gallery — the Swiper track, the thumbnail rail, the lightbox — is documented separately. This page is the classifier underneath it.

Why This Way

Encoding the mapping in alt text keeps the workflow inside the media library, where the merchandising team already works. No metafield definitions, no app, no migration, no second copy of the truth about an image.

Four tiers rather than a boolean, because real catalogs are mixed: some shots are universal, some belong to one exact variant, and some name a single axis such as fragrance and should show across every size of it. A two-state match would have made merchandisers label every image against every variant — the manual work the convention was meant to remove.

Three costs come with that, and the third is the one to weigh hardest before you copy this. All the matching lands in Liquid: the classifier loops every other variant, splitting and comparing strings, inside the gallery's own loops over product.media. The alt text also still has to be alt text — it is read by screen readers and by crawlers, so the convention constrains copy that was never only ours to shape. And the convention is undocumented in the admin. Nothing tells a merchandiser what the alt field is now doing, and nothing validates what they type. A mistyped option value does not error; it silently drops a shot out of one variant's gallery, or leaves one visible to variants that should not have it, and the only way anyone finds out is by looking at the page. A blank alt is safe by design. A wrong alt is not.

Why Not an App

Variant-image-grouping apps do exactly this, and the full case against renting one is made on the gallery page, where the shopper-visible consequence lands. We are not going to run it twice.

What is specific to the classifier is smaller and harder to buy. This is two snippets that return a verdict as text, callable from any Liquid file in the theme. The gallery calls it in about twenty places today, and so could a collection card, a quick view or a structured-data snippet tomorrow. An app owns a gallery; it does not hand you a verdict you can ask for anywhere else. If you want the variant-to-media mapping available to code you have not written yet, it has to be yours.

Implementation Notes

  • Normalization replaces the non-breaking space explicitly before downcasing, then runs split: ' ' followed by join: ' ' to collapse internal whitespace. Liquid has no filter that trims inside a string.
  • Alt text may carry a -- suffix holding a human note. Only the part before it is used for matching, so editorial comments and match keys can share one field.
  • Both , and + are accepted as separators, so merchandisers are not held to one syntax and existing labels did not need rewriting before launch.
  • The "does another variant claim this perfectly" check is an O(variants × alt-parts) inner loop, and the gallery calls the classifier inside its own loops over product.media.
  • The classify snippet returns text rather than setting a variable, so every caller wraps it in {% capture %} and pipes the result through strip before comparing it.
  • The key snippet counts non-blank options rather than assuming three, so a one-option product and a three-option product build their keys through the same code path with no special casing.

Edge Cases

  • Media with no alt text is tiered blank and stays visible for every variant. That is the safe default for a catalog that is only partly labeled.
  • Variants with fewer than three options are handled by counting the populated ones rather than assuming a fixed three, so two-option products match on both axes.
  • A duplicated option value inside one alt string is matched at most once, using per-option matched flags, so repetition cannot inflate a partial into a perfect.
  • The self-comparison is skipped when looping the other variants: a variant matching its own alt perfectly must not hide its own media.
  • A product whose media is entirely unlabeled classifies as blank throughout and behaves exactly like a standard Shopify gallery, which makes adoption per product rather than all at once.

Platform Primitives Used

  • liquid-schema-settings — the consuming gallery exposes its behavior through section settings, so the convention can be switched on per section rather than theme-wide.
  • liquid-snippets — the whole build is two snippets with no section, no asset and no JavaScript, so any template in the theme can call the classifier.

Where It Runs

Product detail page only, and never directly — these snippets run wherever the gallery runs, which today means four product templates: default, 2026 redesign, kids redesign and waiting list. Nothing is configured per template. The convention is the configuration.

What This Demonstrates

How We Know

The requirement is on paper; the mechanism is not. Client working sessions in April and May 2026 aligned on product-page images updating with the selected variant, and separately flagged variant image filtering as a requirement missing from the design mockups. Both snippets — about 140 lines between them — were then read straight out of the live theme.

The mechanism is documented nowhere but in the code. No working-session record proposes alt text as the carrier — that was a code-side decision, and this page reconstructs it from the two snippets. We'd rather say so than imply a paper trail that does not exist.

Related Builds

The Buy-vs-Build Question

This is about the cheapest build in the variant-limits category and one of the stronger arguments for building: two snippets, no recurring cost, no vendor dependency, a mapping held in a field the store already owns. It is also the least portable, because the convention is editorial. Where that line falls: Variant limits and complex options: buy or build?

Ready to Make Your Variants Behave?

You dream it. We build it. If your product page is fighting the platform's variant model, Contact us today and we'll tell you what it would take to fix it — including when the honest answer is an app.

More builds