Shopify Builds>Nudestix>Shade swatch rendering and per-variant content payload

Shade Swatch Rendering and Per-Variant Content Payload

The swatches are the visible half. The other half is a hidden input per variant carrying that shade's copy, ingredients, SEO fields and badges — a data contract three separate features read without asking the server anything.

A shade change on this product page has to repaint copy, ingredients, how-to, SEO fields and two badge types at once. We publish every variant's content as data attributes in the initial HTML — one hidden input per variant — so the swatch picker, the bundle quick view and the cart cross-sell all read the same contract instead of fetching.

Fact Strip

  • Client: Nudestix — color cosmetics DTC, nudestix.com
  • Surface: Product detail page, in its bundle and quick-view form
  • Templates served: One — product.quickviewbundle, the view the bundle builder and the grid's quick view both fetch
  • Complexity: High — roughly 700 lines in one section
  • Attribution: Deploi-modified vendor. The storefront runs Dawn 6.0.2, and this section is a fork of the theme's product section. The section scaffolding and the variant radio group are the theme's; the swatch resolution, the per-variant payload, the badge logic and the change event are our modification of it.
  • Status: Live — verified 2026-09-06
  • Evidence: Strong — the section read from the theme, plus a client specification and task-register records covering the same swatch work
  • Platform primitives: 4

The Problem

In color cosmetics the variant is the product. Pick a different shade and the swatch changes, and so does the shade description, the ingredient list, the how-to, the details copy, the SEO title, the meta description, the Allure award art and the social-proof badge. That is nine things, and a shopper trying six shades in a row triggers all of them six times.

The surface makes it harder. This is the bundle and quick-view rendering of the product page — the view a palette builder pulls in when someone opens a component, and the view the grid opens in a modal. The section is fetched, dropped into a page it doesn't control, and then has to be usable by JavaScript it never met. Anything it wants a shopper to see after a click has to already be in what it sent.

The Constraint

Two platform facts set the shape.

There is no native color on an option value here. This store's product data predates Shopify's swatch metaobjects for these lines, so a shade name is a string with no pigment attached to it anywhere in the product model. Something outside the product has to supply the color.

And Liquid renders once. A section is a string the server produced; it has no second pass. Any content that must change when a shopper picks a shade is either already in that string or it costs a network request. On a page whose entire interaction is shade-switching, a request per switch is the interaction.

There's a data problem underneath both. Shade names in this catalog are reused — the same color word appears on two products with genuinely different pigment — so a flat name-to-color lookup is ambiguous before anyone makes a mistake with it.

What We Built

The swatch layer. Swatch fill comes from the store-wide shade registry the standard product page already runs on. That registry — where the colors live, how a shade name is scoped to a product, and what a merchandiser-editable color map costs to keep — is the subject of its own page, and this section consumes it rather than reinventing it.

What this section adds sits on the label, not in the color. Each swatch is a radio input plus a label painted with an inline background-color, and the label carries that shade's own identifiers: data-color, data-sku, data-id, attr_color, variant-fil from my_fields.stick-type, data-face from theme.variant_face_data and data-variant_text from my_fields.color_text. A data-selected-option span below the row names the chosen shade in words.

The payload mirror. This is the part that makes the section unusual. Separately from the swatches, the section emits one hidden input per variant carrying that variant's whole content set as data attributes — data-meta-title, data-meta-description, data-meta-ingredient, data-meta-how-to, data-meta-details, data-title and data-orgmeta-description, plus data-allure-badge and data-allure-badge2 rendered through image_url at 120px, plus data-social-proof-badge assembled from the custom.sticky_badge metaobject with its label, background color and label color. The mirror is emitted only where a product has more than the default variant, and each hidden input carries a visually hidden label.

The contract. Changing a swatch dispatches a variant:changed CustomEvent carrying the variant id and the color value. That event plus those attributes are the whole public interface, and three different features consume it: the section's own content repaint, the compact quick-view swatch strip that renders alongside the standard fieldset with data-qv-swatch and data-qv-swatch-index, and the cart cross-sell, which hydrates a recommendation card's description by reading [data-meta-description] off a product page it fetched. None of those three shares code with the others. What they have in common is the markup this section emits.

Why This Way

Publishing the payload in the initial HTML turns a shade change into a DOM read. There is no fetch, no loading state to design, no ordering problem between the swatch highlight and the copy underneath it. On a page where the median session is a run of shade switches, that is the difference between an interaction and a sequence of small waits.

What that buys in speed it charges in coupling, and the bill is an interface made out of attribute names. Three features read this markup — the section's own repaint, the bundle quick view, the cart cross-sell — and not one of them imports anything from the other two. Rename data-meta-description, or drop a field out of the mirror because the section that emits it no longer needs it, and the break appears in a cart drawer on a different page from the file that was edited. Nothing type-checks a data attribute. A contract like this only holds because it is treated as a published interface rather than as markup somebody is free to tidy, and that discipline is a standing cost on every future edit to this section.

Why Not an App

Swatch apps are one of the most-rented categories on Shopify and they are good at the thing on the label. The reason this is in the theme has less to do with the swatch than with what surrounds it.

Start with the mechanical difference. A swatch app keeps the color map in its own database and injects swatches into the page after it renders. On a color page that inverts the order of operations: the first frame is the fallback, and the shopper watches the product arrive. Resolving hex in Liquid puts the correct swatch in the initial response, which also means the swatch is in the HTML a crawler or an answer engine reads.

The larger argument is about scope. What a swatch app sells you is a swatch. What this page needs is a shade-change contract — one event and one set of attributes that the section's own copy, a bundle builder rendering this view inside a modal, and a cart cross-sell running on a different page all read the same way. Those three consumers were built at different times and don't call each other, and the markup this section publishes is the only thing they have in common. A vendor swatch component can't be that contract: its internal state isn't addressable from a cart drawer, and its own change event is a shape you don't control across a theme upgrade. Owning the markup is what makes a fourth consumer a selector away rather than an integration.

There's a data point worth stating plainly too. Shade color is brand data. It sits alongside SKU and shade name in the merchandising vocabulary, and the same values feed the collection grid and the bundle builder. A hex in a vendor database is a piece of the brand's own catalog living somewhere the brand can't query.

Implementation Notes

  • The quick-view mode renders a second, compact swatch strip carrying data-qv-swatch and data-qv-swatch-index, in addition to the standard fieldset, so a modal gets a swatch row sized for it without a second rendering path.
  • The payload mirror is emitted only when a product has more than the default variant — one hidden input per variant, each with a visually hidden label.
  • custom.sticky_badge is a metaobject reference, read as .value.label, .value.background_color and .value.label_color rather than as three loose fields.
  • Award art goes through image_url at 120px rather than a raw metafield URL, so the badge is served at the size the row draws it.
  • variant:changed carries the variant id and the color value, and is the only event the swatch layer emits.

Edge Cases

  • Near-white swatches get an explicit border so they stay visible against the page.
  • Variants with no Allure or social-proof badge emit an explicit false, so a consumer can tell "no badge" apart from "attribute missing."
  • A variant with no sticky badge of its own falls back to the badge on the currently selected variant, so the slot doesn't flicker empty mid-selection.
  • Products with a single default variant skip the swatch layer and the payload entirely.

Platform Primitives Used

  • Metafields — every field in the payload is a product or variant metafield read at render time: shade copy, ingredients, how-to, details, the SEO pair and both award slots.
  • Metaobjects — the social-proof badge is a metaobject record, so its label and its two colors are edited once and referenced per variant.
  • Liquid section schema and settings — the shade map is a theme setting, which is why a color correction is an admin edit rather than a deploy.
  • Custom elements — the swatches render inside the theme's variant radio group, so the theme's existing variant behavior applies to them unchanged.

Where It Runs

On the product detail page, through one template: product.quickviewbundle. That view is fetched rather than navigated to — the palette builder pulls it in when a shopper opens a component, and the grid's quick view opens it in a modal. The payload it emits is then read on a third surface, the cart, by the cross-sell that hydrates recommendation cards.

What This Demonstrates

Primary: variant swatches and shade pickers — the swatch layer of a color catalog running from one theme setting rather than a swatch subscription.

Also variant-aware content and media, in its pre-serialized form. Most builds in that capability re-render a section on variant change; this one publishes every variant up front and repaints from the DOM. Both are in the corpus, which is the useful comparison.

How We Know

One product section in the theme, roughly 700 lines, alongside a client specification and task-register entries covering the swatch work on this surface. The specification is detailed enough to name swatch spacing, which says something about how closely this surface was reviewed. Confidence is strong: the code and the client record describe the same work, and the metafield references the payload is assembled from resolve in the store's own data model.

Related Builds

The Buy-vs-Build Question

Everything here follows from Shopify's variant model: option values that carry no attributes, and a render pass that happens once. Whether you solve that with theme code or with an app comes down to how many features need to agree about a variant. We work through that decision, and where the maintenance actually lands, in variant limits and complex options: buy or build?.

Ready to Make Shade Switching Instant?

If your color page fetches when a shopper clicks a swatch, the fix is usually what you send, not how fast you send it. Contact us today to talk through what your product page could be publishing up front.

More builds