webdevpuneet.com

Product Variant Selector — HTML CSS JS Snippet

Product Variant Selector · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="pv-card">
  <div class="pv-image" id="pvImage" style="background:linear-gradient(135deg,#475569,#1e293b)">
    <span class="pv-emoji">👟</span>
    <span class="pv-badge" id="pvBadge"></span>
  </div>

  <div class="pv-body">
    <div class="pv-name">Aero Runner</div>
    <div class="pv-price" id="pvPrice">$129.00</div>

    <div class="pv-group">
      <div class="pv-glabel">Colour: <strong id="pvColorName">Slate</strong></div>
      <div class="pv-swatches">
        <button class="pv-swatch active" style="--c:#475569" data-color="Slate" data-grad="135deg,#475569,#1e293b" data-price="$129.00" onclick="selectColor(this)" aria-label="Slate"></button>
        <button class="pv-swatch" style="--c:#ef4444" data-color="Crimson" data-grad="135deg,#ef4444,#b91c1c" data-price="$129.00" onclick="selectColor(this)" aria-label="Crimson"></button>
        <button class="pv-swatch" style="--c:#3b82f6" data-color="Cobalt" data-grad="135deg,#3b82f6,#1d4ed8" data-price="$139.00" onclick="selectColor(this)" aria-label="Cobalt"></button>
        <button class="pv-swatch" style="--c:#10b981" data-color="Mint" data-grad="135deg,#10b981,#047857" data-price="$129.00" onclick="selectColor(this)" aria-label="Mint"></button>
        <button class="pv-swatch oos" style="--c:#f59e0b" data-color="Amber" data-grad="135deg,#f59e0b,#b45309" data-price="$129.00" data-oos="1" onclick="selectColor(this)" aria-label="Amber (sold out)"></button>
      </div>
    </div>

    <div class="pv-group">
      <div class="pv-glabel">Size <span class="pv-guide">Size guide</span></div>
      <div class="pv-sizes">
        <button class="pv-size" onclick="selectSize(this)">7</button>
        <button class="pv-size" onclick="selectSize(this)">8</button>
        <button class="pv-size" onclick="selectSize(this)">9</button>
        <button class="pv-size oos" disabled>10</button>
        <button class="pv-size" onclick="selectSize(this)">11</button>
        <button class="pv-size" onclick="selectSize(this)">12</button>
        <button class="pv-size oos" disabled>13</button>
      </div>
    </div>

    <div class="pv-stock" id="pvStock">Select a size to see availability.</div>

    <button class="pv-add" id="pvAdd" onclick="addToBag()" disabled>Select a size</button>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Data-attribute driven swatches
Each swatch carries data-color, data-grad, and data-price; selectColor reads them to retint the image, rename the colour, and update the price in one call.
Animated image retint
The product image's background transitions between gradient strings, so switching colours feels like the product itself is changing.
Per-colour pricing
Premium colours expose a higher data-price, so the displayed price updates with the variant — no separate price logic needed.
Enforced sold-out states
Out-of-stock sizes use the native disabled attribute (not just CSS), so they cannot be focused or selected; sold-out colours flash a badge instead.
Size-gated add to bag
The CTA starts disabled reading "Select a size" and only unlocks once selectSize records a choice — preventing invalid, sizeless cart adds.
Stock-aware messaging
A STOCK map drives honest copy: amber "Only N left" under a threshold, green "In stock" above it, with a matching badge flash.
Ring-style active swatch
Selected swatches use a layered box-shadow ring with a white gap so the active colour reads clearly without resizing the chip.
Variant confirmation
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

Screenshot of the Product Variant Selector snippet rendered live

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.

Step by step

How to Use

  1. 1
    Paste 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.
  2. 2
    Pick 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.
  3. 3
    Try a sold-out colourClick the struck-through Amber swatch — instead of selecting, a "Sold out" badge flashes on the image.
  4. 4
    Choose 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".
  5. 5
    Notice blocked sizesSizes 10 and 13 are struck through and disabled — the browser will not let you select them at all.
  6. 6
    Add to bagClick "Add to bag" — it turns green and confirms the exact variant, e.g. "✓ Added — Cobalt / 9".

Real-world uses

Common Use Cases

Apparel and footwear pages
The classic use — pick colour and size before adding to the bag. Drop it beside an order summary on the product page.
Configurable electronics
Storage, colour, and finish options with per-variant pricing. The data-price pattern handles "256GB +\$100" upsells cleanly.
Furniture and home goods
Material and finish swatches that retint a preview image, paired with stock messaging for made-to-order lead times.
Print-on-demand and merch
T-shirt colour and size selection with sold-out combinations disabled — wire it to an add to cart button for the final action.
Food and subscription boxes
Plan tiers as pills and flavour packs as swatches, with low-stock urgency on limited runs and a subscription widget for billing.
Ticket and seating tiers
Tier pills with availability counts gate the purchase button, mirroring how this component gates add-to-bag on a valid selection.

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.