Shopify Builds>Nudestix>"Save With Sets": shade-selectable kit contents on the component product's page

"Save With Sets": shade-selectable kit contents on the component product's page

A metafield-linked list of kits renders as an accordion on the product pages of the products inside them, and each kit opens a modal where every component gets its own shade picker before a single add-to-bag.

A kit whose components have shades cannot be bought until the shopper picks a shade for each one. This module renders those kits on the component product's own page, gives every component its own swatch picker inside a modal, and then adds the kit to cart as an ordinary Shopify product on its own variant — so nothing custom reaches the cart at all.

Fact Strip

  • Client: Nudestix
  • Surface: Product detail page
  • Templates served: 2
  • Complexity: High
  • Attribution: Deploi-authored. Two snippets of ours, mounted in a theme based on Dawn 6.0.2 and reusing that theme's modal and price snippets.
  • Status: Live, verified 2026-09-06
  • Evidence: Two theme snippets plus four client requirement records — confidence: moderate
  • Scale: Roughly 2,400 lines across two snippets

The Problem

Nudestix sells kits: a set of full-size products at a saving. The useful place to merchandise a kit is not the kit's own page — it's the page of a product already inside it, in front of a shopper who has shown intent for a component.

That surface creates a sequencing problem. A kit whose components have shades is unsellable until the shopper has chosen a shade for each component, and the kit product's page is where that choice normally happens. Sending the shopper there to make it costs the session they were already halfway through. The choice has to be made where they are.

The Constraint

The kit is a real Shopify product. Its price and inventory come from Shopify and must keep coming from Shopify, because that is what makes it reportable alongside every other product. But "which shade of each component" does not map onto the kit's own variants — the kit has its own option set, and the components have theirs.

So the relationship had to be expressed outside the variant model: product-list metafields from kit to components, and per-component pickers that resolve to the kit's own variant on submit. The theme is based on Dawn 6.0.2, with no Functions and no bundles API in play, so there is no server-side object to hold the composition.

What We Built

bundle-kits.liquid renders an accordion titled "Save With Sets" from product.metafields.custom.pdp_bundle_kits, a product list capped at five, and lists only the kits whose own custom.pdp_bundle_products list is non-empty. Each row carries the kit image and a title, subtitle and description resolved through a three-level metafield fallback chain, plus a "What's included" button.

That button opens a modal-opener / modal-dialog pair — Dawn's own components, reused rather than reimplemented. Inside it: a value line from sf_product_meta.product_value, the Shopify price rendered through the theme's shared price snippet, the kit's whats_included rich text, a shipping accordion whose kit-level metafield overrides the section's shipping_info block settings, and a Details accordion that splits the kit description on its <h5> headings. The buy control is a <bundle-product-form> wrapping a standard product form that posts the kit's own variant id.

bundle-products.liquid renders the component cards inside that modal. Each card gets a <color-select> custom element listing that component's Color option values, painted from settings.swatch_colors — including the & duotone form and the | product-scoped form the swatch registry uses. Each option's full variant JSON is embedded in a sibling <script type="application/json" data-resource> element, so a shade selection resolves with no network call while the shopper is inside a modal.

A "More Info" button on each card fetches /products/<handle>?view=quickviewbundle, parses the response with DOMParser, and lifts the image, the title, that card's own color picker and the description accordion into an in-modal detail pane. The component's real product page is doing the rendering; the module is only relocating the result. A "VIEW MORE" toggle expands the list past the first row.

Why This Way

Rendering an alternate product view and lifting from it is how you reuse a component's real page inside a modal without duplicating that page into a second template. The drill-down always matches the live PDP because it is the live PDP, rendered through a different view.

Embedding each option's variant JSON beside its swatch is the same instinct applied to data. A shopper configuring three components inside a modal would otherwise generate a round-trip per shade, on a surface where every one of those round-trips is visible as a stall.

Reusing Dawn's modal-opener / modal-dialog pair and the theme's price snippet keeps the kit modal visually identical to the rest of the product page, and keeps it inheriting the theme's fixes rather than needing its own.

The cost is coupling. The detail pane reads markup produced by another template, so that template and this module have to be changed together — a PDP refactor has to account for a consumer that is not on the PDP.

Why Not an App

The bundle app category is mature, and the requirement here looks like exactly what it sells. The reason it wasn't the answer is what happens to the cart line.

A bundle app — and Shopify Bundles after it — takes ownership of the line. The set is defined in the app, priced by the app, and represented in the cart by whatever the app emits. What was needed here was the opposite: the kit is an ordinary Shopify product that keeps its own price, its own inventory and its own place in reporting, with a shade-selection step layered on top of it. The add-to-bag posts the kit's variant id and nothing else. Every system downstream sees a normal product sale.

The second reason is where the module renders. App bundle widgets are built to sit on the bundle's page. This one sits on the component's page, driven by a metafield pointing the other way, and its drill-down re-renders the component's real product page rather than an app's copy of that product's data.

Implementation Notes

  • Kit list, titles, subtitles and descriptions each fall back through kit metafield, then variant metafield, then product field, so a half-authored kit still renders instead of rendering blank.
  • clamp.js truncates kit descriptions to fit and appends a "see more..." link pointing at the kit's own URL. It is re-run on accordion toggle and on the VIEW MORE expansion, because both change the available box.
  • A MutationObserver watches the submit button's disabled and aria-disabled attributes and rewrites its label to "Select your shades", greying the quantity stepper at the same time so the whole control reads as one unfinished step.
  • Component variant JSON is inlined per option value with value.variant | json, so <color-select> resolves a selection without a fetch.
  • forceScrollRestoration() strips every scroll-lock style, class and attribute from body and documentElement after the modal closes, then nudges the scroll position by a pixel so scroll listeners re-fire.
  • The "More Info" handler is delegated at document level so it works for cards rendered inside any modal-dialog, and stores data-parent-modal-id so the back button knows where it came from.

Edge Cases

  • Kits with an empty component list are skipped entirely rather than rendering an empty row.
  • A sold-out kit variant hides the submit button and renders the translated sold-out label in its place.
  • Components with only a default variant emit their variant JSON directly instead of rendering a color picker for one option.
  • Option values that are unavailable render as disabled swatches rather than disappearing from the set.
  • The escape key and both the close and back buttons route through the same hide handler plus scroll restoration, so there is one exit path, not three.
  • A failed fetch renders an inline error string in the detail pane instead of leaving the loading state up.
  • The white swatch gets a gray border so it stays visible against the card.

Platform Primitives Used

  • Metafields — the kit-to-component relationship, the copy fallback chains, the shipping overrides and the value line.
  • Liquid section schema and settings — the swatch registry and the shipping block settings kit metafields override.
  • Cart AJAX API — the standard product form posting the kit's own variant id.

Where It Runs

Two product templates: the single-product page template, where the accordion renders, and the alternate quick-view bundle view that the "More Info" pane fetches and parses. The second is not a page a shopper navigates to — it exists so the module has a render target for the component.

What This Demonstrates

How We Know

Two theme snippets were read directly, plus four client requirement records from a product-page specification that names this module by the name it shipped under. The requirement for the module is documented; the implementation decisions above are not. Documented from the code; no client-side record of the decision survives. What the code does is verifiable. Why each call was made is our reading of it.

Related Builds

The Buy-vs-Build Question

If the kit could be a private object owned by an app, buying is the cheaper answer and usually the right one. The build case here rests on one requirement: the kit stays an ordinary Shopify product with its own price, inventory and reporting. Our position on where that line falls is on the fixed bundles and multipacks decision page.

Ready to Make Your Kits Sellable Where the Shopper Already Is?

Kit merchandising fails in a predictable place: the moment a component needs a choice. If that's where yours is stuck, we'll look at your metafield model and say whether it needs theme work or an app. Contact us today.

More builds