Multiple Variant Images
Different images for variants in the Horizon theme from Shopify (without apps and without Shopify Plus). A free solution from VecomLab.
Relevant for Horizon theme version 3.1.0
If you want to avoid coding, try the Shrine theme.
Shrine offers over 120 built-in features that help improve conversion rates and increase the average order value, without the need to configure additional apps or pay for subscriptions. Everything is customizable without code, saving you time and resources.
Your discount code: VECOMLAB

Problem description
By default, in the Shopify admin and in the Horizon theme settings, each variant can only be assigned one image โ not several.
The platform partially solves this for stores on the Plus plan. You can also use third-party Shopify apps, but this is often unprofitable and inefficient: errors appear regularly and sometimes take months to fix via support.
You can solve this task using Metafields and Metaobjects for Variants and the theme code editor.
Idea
Each variant has a metafield
custom.variant_gallery(list of files).We treat as โownedโ all images whose file name matches the stem from the metafield or starts with the stem +
_or-.On the server (Liquid) we pre-hide extra images for the first frame.
On the client (JS) we read the โvariant โ filesโ map and filter when the variant changes.
If none of the variants has the metafield โ nothing changes, everything works as in the original theme.
Whatโs new: zoom and quick add
The zoom dialog is now also filtered by variant: both thumbnails and large images.
Mobile quick add (collection page modal): on mobile, the modal shows only one image โ the featured image of the current variant (as in the standard Horizon), even if the variant has a whole series.
Step 1. Variant metafield (once per store)
Settings โ Metafields and metaobjects โ Variants โ Add definition
Name: Variant Gallery
Namespace and key:
custom.variant_galleryType: File โ List of files
Validations: Images only
Options: enable Storefront API access
Save
Why this way: this is the native way to link a variant with files in Shopify Files. The key matches what the code uses.
Step 2. File name convention
In Files, give each series of photos for a variant a common stem:
In the variant metafield attach one โanchorโ file from the series (for example, ring-gold.jpg). The rest will be picked up automatically by the stem.
Step 3. Inserts in _product-media-gallery.liquid
_product-media-gallery.liquidBelow is exactly what we added/changed, and why.
3.1. JSON map variant โ files (server โ front)
Insert before {%- if has_image_drop -%}:
Why: the front receives ready JSON without parsing the DOM. We immediately normalize URLs to the base file name.
3.2. Flags on <media-gallery>
<media-gallery>In the tag attributes, next to data-presentation and style, add:
data-vg-ready="0"โ anti-flicker: while the script is running, images are hidden (CSS below).data-initial-variant-idโ optional, for debugging/analytics; the script itself takes the variant id from the form.
If you want things maximally โcleanโ, you can remove data-initial-variant-id. Functionality wonโt change.
3.3. vg_list for the first render
vg_list for the first renderRight after {{ block.shopify_attributes }}:
Why: we need to know the current variantโs file list on the server in order to pre-hide extra images even before JS runs (correct first frame).
3.4. Server-side pre-hide in slides (Carousel)
Before you compute class/style/attributes for each media:
Then add vg-prehide and display:none:
3.5. Unified set of data attributes on each slide
Before the block with attributes:
And replace the attributes block with options that inject {{- common_data_attrs -}} (for model / image / โno zoomโ).
Before:
After:
Why: the front needs the image type and URL. data-vg-allow/data-vg-prehide help CSS before JS initializes.
3.6. Same logic โ for the Grid
Inside <ul class="media-gallery__grid"> before each <li> repeat the prehide logic:
And in the Grid <li> replace class="" and style="" with:
Add data attributes:
3.7. Filtering inside the zoom dialog (Liquid)
Zoom thumbnails (1)
Inside the thumbnails block (buttons .dialog-thumbnails-list__thumbnail) add pre-hide logic, like we did for slides/grid:
In the <button> replace class="" and style="":
And add attributes:
Large zoomed frames (2)
For <li> inside .dialog-zoomed-gallery apply the same prehide check and the same data attributes.
Insert the check before <li>:
Replace class="" and style="" in <li>:
And add attributes:
Same idea: we ensure a โcleanโ first frame in zoom even before JS runs.
3.8. Client script and mini CSS (after </media-gallery>)
</media-gallery>)Script:
reads
#variant-galleries-json;if the product has no variants with the metafield โ sets
data-vg-ready="1"and exits (changes nothing);otherwise filters
imageslides by the current variant; on variant change โ repeats;at the end sets
data-vg-ready="1"(anti-flicker off).
CSS:
Why: completely removes โjitter/flickerโ and intermediate states before everything is ready. We donโt touchthumbnails/arrows โ in the โcleanโ version of this script we donโt even hide them.
Why this is needed:
while
data-vg-ready="0"is set, the modal doesnโt โflashโ with a set of images;JS additionally guarantees that even after switching variants in the modal there will be exactly one image.
What we deliberately donโt touch (remains โas in the themeโ)
Weโll do this in future script versions:
Counter / arrows / thumbnails of the carousel โ we donโt configure or hide them with separate rules.
Small audit of โextra stuffโ
data-initial-variant-idโ optional, can be removed (wonโt break the script).MutationObserveris currently attached todocument.bodyfor robustness with dynamic re-renders. You can narrow it to only watch<media-gallery>, but the current option is more reliable across themes/scripts.The duplicated logic โserver-side pre-hideโ + โclient-side filterโ is intentional: it gives an ideal first frame (no junk) and correct behavior on further switches. This is not redundant; itโs an anti-FOUC layer.
Result
If a variant has the Variant Gallery metafield filled in, only its images are shown on the page (by file name). If there is no metafield โ everything works as before. The first frame is clean and without flickering (server pre-hide), and when switching variants the gallery is updated by the script. No apps, conflicts, or slowdowns.
Theme settings
The Product media (Grid + Hint) and Grid + Dots modes are supported. The โsingle image in Mobile quick addโ behavior is enabled only inside the modal on screens โค 749 px; on the product page and on desktop you still have the full variant-based gallery.

FAQ
What exactly do I need to configure in the admin?
Only one thing: create a variant metafield custom.variant_gallery of type File โ List of files (Images only) and enable Storefront API access. Everything else is inserts into a single template, _product-media-gallery.liquid.
How many files should I attach to the variant metafield?
One anchor file from the series is enough. The rest will be picked up automatically by the name (by the โstemโ) if they are called stem.jpg, stem_2.jpg, stem-3.jpg, etc.
How exactly should the file be named?
All photos of one modification should share a common stem (base of the file name):
ring-gold.jpg, ring-gold_2.jpg, ring-gold-3.jpg.
Matching goes by exact stem or stem with _ / - suffix. Case and Shopify sizes (_800x) donโt matter โ the script strips them.
What if the variant metafield is empty?
Then all product images are shown for that variant (as in the original theme). The logic only kicks in if at least one attached image is found for the variant.
What happens if none of the variants has the metafield?
The script detects this, does nothing, and immediately sets data-vg-ready="1". The gallery works exactly as in the theme, with no extra work or performance impact.
Will images โflickerโ on load or when switching variants?
No. We used a double protection:
on the Liquid side โ pre-hiding extra images for the first frame;
on the JS side โ filtering on variant switches + the
data-vg-readyflag with mini CSS to avoid intermediate states on screen.
In the zoom dialog all photos are shown. Is that intentional?
No, now zoom is also filtered. Both thumbnails and large images in the dialog show only the current variantโs images (by the same stem rule). The first frame is clean thanks to Liquid pre-hide; then JS takes over.
Why didnโt you touch the carousel counter, arrows, thumbnails?
To keep the solution as compatible and minimally โinvasiveโ as possible. We didnโt interfere with navigation elements, except for Dots โ those can be customized later when thereโs a specific need.
Does this work in both Grid and Carousel modes?
Yes. The pre-hide logic and data attributes are applied both to carousel slides and to grid <li> items.
In what cases might the script โnot seeโ the variant id?
If the theme/app renders the variant form in a non-standard way. The script looks for name="id" in the add-to-cart form. If that field is renamed or missing, update the selector in getVariantId() or dispatch a custom selectVariant/variant:change event with detail.variant.id.
I have a โquick viewโ / product modal โ will this work there?
Usually yes, because the script uses a MutationObserver on document.body and reapplies the filter. If the modal loads content dynamically, make sure the #variant-galleries-json block ends up inside its DOM.
What about performance?
The JSON block is built once on the server;
JS only walks visible gallery elements and uses simple string checks;
If there are no metafields โ the script effectively โturns offโ. Thereโs no overhead on pages without metafields.
Will SEO/alt texts/structured data be affected?
We donโt remove elements, we only add display:none to the initial state and toggle visibility. Alt texts and schema.org generated by the theme stay intact. The set of images in the DOM remains the same; some are just hidden when the variant is different.
Do I need to change the media order in the product itself?
Not necessarily. But for better UX, the โanchorโ photo of each variant (that stem) should ideally sit near its series, so pre-hide and the first frame look visually logical.
What if different variants accidentally get the same stem?
Then both series will be considered โsuitableโ for both variants. Keep the naming strict (different stems for different variants) to avoid overlap.
Can I use spaces/Unicode in file names?
You can, but itโs better to avoid it. Shopify gives you a URL, and the script uses the base name from the URL. Recommendation: Latin letters, hyphens, and underscores.
What about videos and 3D models?
Filtering is applied only to media_type="image". Videos/models stay as in the theme. If you want them to be variant-specific too, you can extend the matching logic using data-media-type and base names of posters/URLs.
What happens if I rename a file later in Files?
Matching is based on the file name. If you rename the โanchorโ or the series, either update the โanchorโ name in the metafield or revert the previous name โ otherwise the series may be โdetachedโ from the variant.
How do I roll back?
Remove the JSON block, the server inserts vg_list/prehide, the unified common_data_attrs block, the script, and the mini CSS. After that the theme returns to its original behavior.
Does this work across all languages/locales?
Yes. The logic does not depend on texts; only on attributes and file names.
Can this conflict with gallery apps?
If an app completely replaces the gallery markup, it probably ignores our data attributes. Then either disable the app on pages using this logic, or move the filtering into the appโs templates.
Is it safe to update the theme?
Major theme updates can overwrite _product-media-gallery.liquid. Keep git/Theme Download and run a diff: if Shopify changes the structure of this template, re-apply our inserts to the new version.
Can I attach all files in the series to the metafield instead of just the โanchorโ?
You can, but thereโs no need. The logic is built around stems so you donโt have to โpokeโ every file manually.
Can I remove the data-initial-variant-id attribute?
Yes. Itโs optional and does not affect the filter.
What if there are multiple products on the page (recommended panel, custom sections, etc.)?
In its current form the script looks for the closest <media-gallery> next to itself. Place it inside the relevant product block. If you have several galleries on one page, add unique containers/initializations so that each script instance sits right after its own <media-gallery>.
Last updated