choices_metadata object to control how each choice is displayed. Most Growth Theme–based themes already use Vitrin's V2 product-options renderer and only need the CSS provided in this guide. No Jinja, JavaScript, or variant-selection changes are needed for the standard implementation.| Area | Requirement |
|---|---|
| Supported presentation | Normal text, color swatch, or image thumbnail |
| Standard implementation | Growth Theme with Vitrin's V2 product-options renderer |
| Theme changes for the standard implementation | Add the required CSS, rebuild the theme assets, and test |
| Dynamic Bundle options | Handled internally by vitrin-sdk |
| Scope of the CSS in this guide | Product Details Page list options only |
Native <select> dropdowns | Text-only |
| Custom rendering | Use only when the theme intentionally renders product options with its own markup |
Important — Choose the correct implementation path Most Growth Theme–based themes already use the shared V2 product-options renderer and require CSS only. Do not add custom Jinja, JavaScript, or selection logic unless your theme intentionally replaces the shared renderer.
vitrin-sdk. The SDK already supports text, color, icon, and fallback states, so no theme-level implementation is required for Dynamic Bundle options.Info — Dynamic Bundle scope Do not reuse the Product Details Page list-option CSS as an implementation requirement for Dynamic Bundle option chips. Dynamic Bundle display behavior is owned by vitrin-sdk.
| Metadata type | Customer-facing result |
|---|---|
default | Normal text option |
color | Color swatch |
icon | Image thumbnail |
Terminology Although the metadata value is named icon, it contains an absolute image URL and is displayed as an image thumbnail.
choices_metadata object is optional and appears inside a product option. It is keyed by the exact raw value in choices.{
"id": "color-option",
"name": "Color",
"slug": "color",
"choices": ["Red", "White", "Pattern", "Blue"],
"choices_metadata": {
"Red": {
"id": "choice-red",
"value": { "en": "Red" },
"type": "color",
"type_value": "#ff0000"
},
"White": {
"id": "choice-white",
"value": { "en": "White" },
"type": "color",
"type_value": "#ffffff"
},
"Pattern": {
"id": "choice-pattern",
"value": { "en": "Pattern" },
"type": "icon",
"type_value": "https://cdn.example.com/pattern.png"
},
"Blue": {
"id": "choice-blue",
"value": { "en": "Blue" },
"type": "default",
"type_value": null
}
}
}Raw choice in choices | Matching metadata key | Display result |
|---|---|---|
Red | choices_metadata.Red | Red color swatch |
White | choices_metadata.White | White color swatch with a visible border |
Pattern | choices_metadata.Pattern | Image thumbnail |
Blue | choices_metadata.Blue | Normal text option |
Important — Exact key matching A metadata entry must use the exact raw choice value as its key. Metadata is presentation-only; the raw choice string remains the value used for variant selection.
type | Expected type_value |
|---|---|
default | Usually null or empty |
color | Hex color, for example #fff or #ffffff |
icon | Absolute image URL |
type is authoritative. A choice with type: "default" must render as text even if type_value is present.choices_metadata is missing.type is unknown.Warning — Failed image requests in custom renderers The fallback rules above cover missing or invalid metadata resolved before rendering. In a fully custom renderer, an image URL may appear valid but still fail when the browser requests it. Prefer the shared renderer or shared macro, and preserve the theme's existing image-error handling when implementing custom markup.
| Theme setup | Required action |
|---|---|
| Growth Theme–based theme using the V2 product-options renderer | Add the CSS in this guide, rebuild the assets, and test |
| Custom theme that can use the shared macro | Call render_variant_option_choice(option, choice, is_selected) |
| Theme with fully custom product-option markup | Resolve the display with option.choice_display(choice) and preserve the existing selection contract |
| Dynamic Bundle option chips | No theme-level implementation is required |
Native <select> dropdown | Keep the choices text-only |
Recommended path Use the shared V2 product-options renderer whenever possible. It owns the supported metadata, markup, fallback behavior, and selection compatibility.
| CSS selector | Purpose |
|---|---|
.product-options__list | Keeps the list flexible, wrapping, and responsive |
.product-options__item--color | Identifies a color-swatch choice |
.product-options__item--icon | Identifies an image-thumbnail choice |
.product-options__swatch | Defines the visible color swatch |
.product-options__thumb | Defines the image-thumbnail dimensions and crop behavior |
.active | Displays the selected state |
.product-options__item-text--sr | Keeps the raw choice label available to screen readers |
<span> can appear invisible.object-fit: cover to prevent layout shift.Tip — Design-system customization You can adjust the fallback values for --border,--radius,--foreground,--background, and--primaryto match your theme. Keep the required dimensions, visible light-color border, and distinguishable active state when customizing the appearance.
choices_metadata still renders text choices exactly as before.color choice displays a visible swatch.icon choice displays a correctly sized thumbnail.Important — Regression testing Test products with and without metadata. The feature must enhance presentation without changing variant matching, availability behavior, or add-to-cart behavior.
Important — Standard themes should stop before this section This section is not needed for Growth Theme–based themes that use the V2 product-options renderer. Use it only if your theme intentionally renders product options with its own markup.
{% with options=product.options, product=product, style='list' %}
{% include 'vitrin:v2/products/options/variant_options.jinja' %}
{% endwith %}{{ render_variant_option_choice(option, choice, is_selected) }}Tip — Prefer the shared macro The shared macro reduces duplicated rendering logic and preserves the supported metadata and fallback behavior. Use fully custom markup only when the theme has a specific requirement that the shared renderer or macro cannot meet.
option.choice_display(choice) and preserve the existing selection contract.{% set display = option.choice_display(choice) %}
<li
value="{{ choice }}"
onclick="productOptionListItemClicked(event)"
class="product-options__item product-options__item--{{ display.type }}{{ ' active' if is_selected else '' }}"
{% if display.type != 'default' %}
title="{{ choice }}"
aria-label="{{ choice }}"
{% endif %}
>
{% if display.type == 'color' %}
<span
class="product-options__swatch"
style="background-color: {{ display.value }};"
aria-hidden="true"
></span>
<span class="product-options__item-text product-options__item-text--sr">{{ choice }}</span>
{% elif display.type == 'icon' %}
<img
class="product-options__thumb"
src="{{ image_url(display.value, w=64, q=85, f='auto') }}"
alt="{{ choice }}"
width="40"
height="40"
loading="lazy"
/>
<span class="product-options__item-text product-options__item-text--sr">{{ choice }}</span>
{% else %}
<span class="product-options__item-text">{{ choice }}</span>
{% endif %}
</li>Accessibility note for fully custom markup Preserve the keyboard and interaction behavior already used by your theme. A clickable custom choice must remain reachable and operable for keyboard users, expose its selected state through the theme's existing accessibility pattern, and keep the raw choice label available to assistive technologies. The shared renderer or macro remains the preferred implementation.
<li> child of its option <ul>.<li> value is the raw choice string.productOptionListItemClicked(event).active class.Warning — Metadata is not the selected value Do not use id,type_value, a hex color, or an image URL as the<li>value. Metadata controls presentation only; selection continues to use the raw choice string.
title and aria-label to the clickable choice.alt text to image thumbnails.aria-hidden="true" on a decorative color swatch.<select> dropdowns remain text-only. Browsers do not reliably render images or swatches inside <option> elements. Use a list-style renderer when visual choices are required.Info — Expected dropdown behavior A native dropdown continuing to show text is expected and is not a fallback error. Visual swatches and thumbnails require a list-style renderer.
| Issue | Likely cause | Resolution |
|---|---|---|
| Color swatch is invisible | The empty swatch <span> has no effective dimensions | Keep an explicit width and height on .product-options__swatch |
| White or light swatch blends into the page | The swatch has no visible border | Keep a border around every swatch |
| Selected swatch or thumbnail is covered | The active state uses a solid filled background | Use a border or ring for the active state |
| Thumbnail causes layout movement | Its dimensions are not fixed before loading | Keep fixed width and height and use object-fit: cover |
| Choice displays as text | Metadata is missing, incomplete, invalid, or uses an unknown type | Confirm the metadata key, type, and type_value; text fallback is expected when metadata is invalid |
| Wrong variant is selected | Custom markup uses metadata instead of the raw choice value | Set the <li> value to the exact raw choice string |
| Add to cart fails after customization | The existing selection contract was changed | Restore the raw value, click handler, direct <li> structure, and active class behavior |
| Native dropdown does not show an image or swatch | Native <option> elements are text-only | Use a list-style renderer |
| Dynamic Bundle chips do not use this CSS | Dynamic Bundle rendering is owned by vitrin-sdk | Do not add a separate theme-level Dynamic Bundle implementation |
choices_metadata is missing or invalid?type: "icon" expect an icon-library name?type_value for icon must be an absolute image URL, and the choice is displayed as an image thumbnail.vitrin-sdk. The CSS in this guide applies to Product Details Page list options only.<select> dropdowns display color swatches or images?<option> elements do not reliably render images or color swatches. Use a list-style renderer when visual choices are required.<select> menus.Final publishing check Before publishing, confirm that the implementation changes presentation only. Products without metadata must continue to render and behave as they did before, and selecting a visual choice must still resolve the correct variant and preserve add-to-cart behavior.