Product Variant Selector — HTML CSS JS Snippet
Product Variant Selector · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
data-color, data-grad, and data-price; selectColor reads them to retint the image, rename the colour, and update the price in one call.background transitions between gradient strings, so switching colours feels like the product itself is changing.data-price, so the displayed price updates with the variant — no separate price logic needed.disabled attribute (not just CSS), so they cannot be focused or selected; sold-out colours flash a badge instead.disabled reading "Select a size" and only unlocks once selectSize records a choice — preventing invalid, sizeless cart adds.STOCK map drives honest copy: amber "Only N left" under a threshold, green "In stock" above it, with a matching badge flash.box-shadow ring with a white gap so the active colour reads clearly without resizing the chip.addToBag echoes the exact colour and size and turns the button green, closing the loop on what was added.About this UI Snippet
Product Variant Selector — Colour Swatches, Size Pills, Sold-Out States & Stock-Aware Add to Bag

Almost every physical product comes in variants — sizes, colours, materials — and the variant selector is where shoppers translate "I want this" into "I want this specific one". Done badly, it lets people add out-of-stock combinations, hides the price impact of a premium colour, or enables checkout before a size is chosen. Done well, it guides the shopper to a valid, in-stock selection and is honest about scarcity. This snippet implements the complete pattern in plain HTML, CSS, and vanilla JavaScript: colour swatches that retint the product image and update price, size pills with sold-out states, live stock messaging, and an add-to-bag button that stays disabled until a size is selected.
Colour swatches with live retint and price
Each swatch is a button carrying its data on attributes: data-color, data-grad (a gradient string), and data-price. selectColor moves the .active ring, updates the colour name label, swaps the product image's background to the new gradient with a CSS transition, and sets the price — because some colours (like Cobalt) cost more. The active state uses a layered box-shadow to draw a ring with a white gap, the standard way to show a selected swatch without altering its size.
Sold-out variants are visibly blocked
A swatch marked .oos gets a diagonal slash via a pseudo-element and a not-allowed cursor; clicking it flashes a "Sold out" badge instead of selecting it. Sold-out sizes use the native disabled attribute plus an .oos style with a strike-through line, so they cannot be focused, clicked, or chosen — the browser enforces it, not just CSS.
Size pills gate the call to action
The add-to-bag button starts disabled and reads "Select a size". selectSize moves the active pill, records selectedSize, and only then enables the button and changes its label to "Add to bag". This gate is the single most important conversion-safety mechanism in the component: it makes it impossible to add an item without a size, which prevents the most common cause of fulfilment errors and refunds.
Honest, stock-aware messaging
A STOCK map holds remaining units per size. When a size is selected, selectSize reads the count: three or fewer shows an amber "Only N left in size X — order soon!" message and flashes a "Low stock" badge; more than three shows a green "In stock · ships in 1–2 days". This scarcity signalling is a well-documented conversion lever, but here it is driven by real numbers rather than fabricated urgency.
Add-to-bag confirmation
addToBag confirms the exact variant ("✓ Added — Cobalt / 11") and turns the button green, closing the loop so the shopper knows precisely what entered their cart.
Pair this selector with an order summary, a product card grid, an add to cart button micro-interaction, or a quantity stepper.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Instead of tracing every data attribute by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why selectColor reads data-grad and data-price directly off the clicked button rather than looking them up in a separate JS object, and why sold-out sizes use the native disabled attribute while sold-out colors instead intercept the click and flash a badge. It's also worth asking about a real gap in the current logic — there's no matrix linking which sizes are out of stock for which specific color, so ask what changes would be needed to gate size availability per selected color rather than treating STOCK as global. For extending it, have it add a quantity stepper that respects remaining STOCK, aria-pressed and role=radiogroup for accessibility, or a size-guide modal triggered by the existing "Size guide" link. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a product variant selector (color swatches plus size pills) in plain HTML, CSS, and vanilla JavaScript with no libraries.
Requirements:
- Color swatches as buttons, each carrying its color name, a CSS gradient string, and a price as data attributes; clicking a swatch must move the active-selection ring, update a text label showing the selected color name, transition the product image's background to the clicked swatch's gradient, and update the displayed price to that swatch's price — since some colors cost more than others.
- One swatch must be marked sold out with a native data attribute; clicking it must not select it or change any state, but instead trigger a temporary "Sold out" badge that fades in over the product image.
- Size options as a row of buttons; at least two must use the native disabled attribute (not just a CSS class) so they cannot be focused, hovered as clickable, or selected by any means.
- A STOCK lookup object mapping available size labels to a remaining-unit count; selecting an in-stock size must display "In stock" messaging in a neutral/positive style when the count is above a threshold, or an urgent low-stock message naming the exact remaining count when at or below that threshold, and must also flash a "Low stock" badge in the low case.
- An add-to-bag button that starts disabled with "Select a size" as its label and remains disabled and unclickable until a size has been chosen, at which point it enables and changes to "Add to bag".
- Clicking add-to-bag while enabled must update its own label to confirm the exact color and size that was added (e.g. "Added — Cobalt / 9") and change its background color to signal success.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA product card shows a slate sneaker, a $129 price, five colour swatches (one sold out), a size row (two sizes struck out), and a disabled "Select a size" button.
- 2Pick a colourClick Cobalt — the product image retints with a smooth transition, the colour name updates, and the price changes to $139 since that colour costs more.
- 3Try a sold-out colourClick the struck-through Amber swatch — instead of selecting, a "Sold out" badge flashes on the image.
- 4Choose a sizeClick size 9 — it highlights, a low-stock amber message warns "Only 2 left", and the add button unlocks and reads "Add to bag".
- 5Notice blocked sizesSizes 10 and 13 are struck through and disabled — the browser will not let you select them at all.
- 6Add to bagClick "Add to bag" — it turns green and confirms the exact variant, e.g. "✓ Added — Cobalt / 9".
Real-world uses
Common Use Cases
data-price pattern handles "256GB +\$100" upsells cleanly.Got questions?
Frequently Asked Questions
Render the swatch and size buttons from your product data, setting data-grad, data-price, and disabled per variant, and replace the STOCK object with live inventory from your API. selectSize already reads from that map, so accurate stock messaging follows automatically once the numbers are real.
Keep a matrix of availability keyed by color + size. In selectColor, after switching colour, loop the size pills and toggle disabled based on the matrix for the new colour. This prevents selecting a size that exists for one colour but not the chosen one — a common edge case in apparel.
Place a small − / + stepper above the add-to-bag button and track quantity in a variable. Multiply it into the cart payload in addToBag, and optionally clamp the max to the remaining STOCK count for the selected size so users cannot order more than exist.
Sizes and swatches are real <button> elements, so they are keyboard-focusable and respect disabled. Add aria-pressed to reflect the selected state, group each set with role="radiogroup" and an accessible label, and announce stock changes via an aria-live="polite" region so screen-reader users hear "Only 2 left".
In React, hold selectedColor and selectedSize in useState, derive the image gradient, price, and button disabled from them, and render stock copy from a lookup. In Vue, use refs with :class and :disabled bindings. In Angular, track selection on the component and bind [class.active]/[disabled]. The swatch and pill CSS port unchanged.