Zid Docs
AppsThemes
Payments
AppsThemes
Payments
Help Center
Slack
  1. Features
  • Getting Started
    • Introduction
    • Theme Development
    • Vitrin Changelog
    • Creating and Managing Theme Presets
    • Legacy Theme Migration
      • Store Settings Mapping
      • Moving to Vitrin Using LLMs
      • Twig to Jinja
      • Breaking Changes
  • Key Concepts
    • Architecture
    • Templates
      • Overview
      • Overridable Templates
      • Legacy Templates
      • Template Replacements
      • Templates Library
        • home.jinja
        • product.jinja
        • cart.jinja
        • category.jinja
        • products.jinja
        • categories.jinja
        • page.jinja
        • blogs.jinja
        • blog.jinja
        • faqs.jinja
        • reviews.jinja
        • questions.jinja
        • shipping_payment.jinja
        • 404_not_found.jinja
    • Settings
      • Schema files
      • Input Settings
      • Media Settings
      • Form Controls Settings
      • Products Settings
      • Additional Settings
      • Conditional Visibility
      • Migrating twig settings schema
    • Localization
      • localization (jinja v. twig)
    • Theme Editor
      • Overview
  • Building with Vitrin
    • Jinja Basics
    • Vitrin's Jinja Extensions
  • Vitrin CLI
    • Introduction
    • CLI Commands
  • Tips & Tricks
    • Performance
  • JS Integration
    • Supporting both Vitrin and Legacy themes
    • Responses & Errors
    • Cart
    • Products
    • Categories
    • Store
    • Account
    • Blogs
    • Options
    • Events
  • Features
    • SDK Popups – Integration Guidelines
    • Custom Styles Guide
    • Gift Card as a Popup
    • Addresses as a Popup
    • Login as a Popup
    • Checkout as a Popup
    • Apple Pay Quick Checkout
    • Region & Language Popup
    • Dynamic Bundle Products
    • Progressive Discounts
    • Customer Wallet & Cashback
    • Add Preorder Support to Your Theme
    • Display Product Options as Colors or Images
  • Mobile Apps
    • Scripts
    • Login & Checkout via Page Views
    • Theme Sections Settings
  • API's
    • Authentication
      • Logout
      • Login Status
      • SMS Login
      • Verify SMS Login
      • WhatsApp Login
      • Verify WhatsApp Login
      • Email Login
      • Verify Email Login
      • Register
      • Register Guest
    • Products
      • List Products
      • Search Products
      • Calculate Product Options Price
      • Notify Product Stock Availability
      • Fetch Bundle Offers
      • Fetch Bundle Offers for a Product
      • List My Product Reviews
      • List Product Reviews
      • Create Product Review
      • Update Product Review
      • Delete Product Review
      • List Product Questions
      • Create Product Question
      • Get Product by Slug
      • Get Selection Groups
    • Categories
      • List Categories
    • Checkout
      • Get Cart
      • Remove Cart
      • Duplicate Cart
      • Add Cart Item
      • Empty Cart
      • Update Cart Item
      • Remove Cart Item
      • Upload Cart Input Field
      • Add Gift Card
      • Remove Gift Card
      • Apply Coupon
      • Remove Coupon From Cart
      • Check Coupon Validity
      • Apply Loyalty Points
      • Remove Loyalty Points
      • Preview Rewarded Points
      • List Redemption Methods for Cart
      • Customer’s Loyalty Wallet
      • Customer’s Current Points Balance
    • Account
      • Get Profile
      • Delete Account
      • Update Customer Profile
      • Get Addresses
      • Create an Address
      • Get an Address by ID
      • Update an Existing Address
      • Delete Address
      • Get Orders
      • Get Shareable Wishlist Link
      • Get Wishlist
      • Add Products to Wishlist
      • Remove Product from Wishlist
      • Get Address Form Schema
      • Check Product Purchase Status
    • Storefront
      • Store Scripts
      • Pages
      • Blogs
    • Countries
      • Get Countries
      • Get Cities By Country
  1. Features

Display Product Options as Colors or Images

Catalog product options can include display metadata that lets a Zid theme show each choice as normal text, a color swatch, or an image thumbnail.
Zid product options use the optional 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.
Product option and color.png

At a glance#

AreaRequirement
Supported presentationNormal text, color swatch, or image thumbnail
Standard implementationGrowth Theme with Vitrin's V2 product-options renderer
Theme changes for the standard implementationAdd the required CSS, rebuild the theme assets, and test
Dynamic Bundle optionsHandled internally by vitrin-sdk
Scope of the CSS in this guideProduct Details Page list options only
Native <select> dropdownsText-only
Custom renderingUse 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.

Quick start#

Most Zid themes are based on Growth Theme and already use Vitrin's V2 product-options renderer. No Jinja, JavaScript, or variant-selection changes are needed. Add the CSS in Required CSS, rebuild your theme assets, and test the result.
Vitrin already renders the correct markup and safely falls back to text when product metadata is missing or invalid. Your theme is responsible only for styling that markup.

Dynamic Bundle options#

Dynamic Bundle option chips are handled internally by vitrin-sdk. The SDK already supports text, color, icon, and fallback states, so no theme-level implementation is required for Dynamic Bundle options.
The CSS in this guide applies to Product Details Page list options only.
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.

Supported display types#

Metadata typeCustomer-facing result
defaultNormal text option
colorColor swatch
iconImage thumbnail
Terminology
Although the metadata value is named icon, it contains an absolute image URL and is displayed as an image thumbnail.
If metadata is missing, incomplete, or invalid, Vitrin renders the normal text option. Existing products therefore continue to work without theme changes beyond CSS.

Product option metadata#

The choices_metadata object is optional and appears inside a product option. It is keyed by the exact raw value in choices.

Metadata example#

{
  "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
    }
  }
}

Metadata mapping#

Raw choice in choicesMatching metadata keyDisplay result
Redchoices_metadata.RedRed color swatch
Whitechoices_metadata.WhiteWhite color swatch with a visible border
Patternchoices_metadata.PatternImage thumbnail
Bluechoices_metadata.BlueNormal 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.

Supported metadata values#

typeExpected type_value
defaultUsually null or empty
colorHex color, for example #fff or #ffffff
iconAbsolute image URL
type is authoritative. A choice with type: "default" must render as text even if type_value is present.

Fallback behavior#

The product page must render a normal text choice when:
choices_metadata is missing.
The choice has no metadata entry.
The display type is unknown.
A color value is missing or is not a valid hex color.
An image URL is missing or invalid.
This preserves compatibility with existing products and partial Catalog responses.
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.

Choose an implementation path#

Theme setupRequired action
Growth Theme–based theme using the V2 product-options rendererAdd the CSS in this guide, rebuild the assets, and test
Custom theme that can use the shared macroCall render_variant_option_choice(option, choice, is_selected)
Theme with fully custom product-option markupResolve the display with option.choice_display(choice) and preserve the existing selection contract
Dynamic Bundle option chipsNo theme-level implementation is required
Native <select> dropdownKeep 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.

Required CSS#

Add these styles to your theme stylesheet. Adjust tokens and dimensions to match your design system.

CSS selectors#

CSS selectorPurpose
.product-options__listKeeps the list flexible, wrapping, and responsive
.product-options__item--colorIdentifies a color-swatch choice
.product-options__item--iconIdentifies an image-thumbnail choice
.product-options__swatchDefines the visible color swatch
.product-options__thumbDefines the image-thumbnail dimensions and crop behavior
.activeDisplays the selected state
.product-options__item-text--srKeeps the raw choice label available to screen readers

Styling considerations#

A swatch needs a width and height. Without them, its empty <span> can appear invisible.
Keep a border around every swatch so white and light colors are visible.
Use a border or ring for the active state; avoid covering a swatch or thumbnail with a solid active background.
Keep thumbnail dimensions fixed and use object-fit: cover to prevent layout shift.
Existing flex and gap styles work in both LTR and RTL layouts.
Tip — Design-system customization
You can adjust the fallback values for --border, --radius, --foreground, --background, and --primary to match your theme. Keep the required dimensions, visible light-color border, and distinguishable active state when customizing the appearance.

Testing checklist#

After rebuilding your theme assets, verify the following on a product details page.

Standard renderer checks#

A product with no choices_metadata still renders text choices exactly as before.
A valid color choice displays a visible swatch.
A white or light swatch has a visible border.
A valid icon choice displays a correctly sized thumbnail.
A choice with invalid color data falls back to text.
A choice with an empty or invalid image URL falls back to text.
Selecting a swatch or thumbnail clearly shows the active state.
Selecting a swatch or thumbnail still selects the correct product variant.
Add to cart continues to work unchanged.
The layout works on mobile and in RTL.

Custom renderer checks#

The raw choice string remains the selected value.
Keyboard users can reach and select every interactive choice using the theme's existing interaction pattern.
Keyboard focus is visible.
Screen readers receive the raw choice label.
Image thumbnails include meaningful alternative text.
Decorative color swatches are hidden from screen readers.
A failed image request does not prevent the customer from identifying or selecting the choice.
Selected and unavailable choices remain visually distinguishable.
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.

Custom renderer implementation#

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.

Use the shared V2 product-options renderer#

The V2 renderer is included as follows:
{% with options=product.options, product=product, style='list' %}
  {% include 'vitrin:v2/products/options/variant_options.jinja' %}
{% endwith %}
If your theme uses this include, stop here and use the CSS above. Do not duplicate the rendering logic.

Use the shared product-option macro#

The easiest supported option is to call the shared macro:
{{ 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.

Create custom list markup#

If you need fully custom markup, resolve the choice display with 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.
For custom list renderers, keep all of the following unchanged:
Each choice is a direct <li> child of its option <ul>.
The <li> value is the raw choice string.
The click handler is productOptionListItemClicked(event).
The selected choice receives the active class.
Non-visual choices remain readable text.

Preserve variant-selection compatibility#

This applies only to custom renderers: keep the original choice string as the selected value.
Using a metadata value instead of the raw choice can break variant matching and add-to-cart behavior.
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.

Accessibility requirements#

For swatches and thumbnails:
Add title and aria-label to the clickable choice.
Add alt text to image thumbnails.
Keep the raw choice label in visually hidden text.
Set aria-hidden="true" on a decorative color swatch.
For a fully custom interactive implementation, also preserve the theme's existing keyboard behavior, visible focus treatment, and selected-state announcement.

Native dropdown limitations#

Native <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.

Troubleshooting#

IssueLikely causeResolution
Color swatch is invisibleThe empty swatch <span> has no effective dimensionsKeep an explicit width and height on .product-options__swatch
White or light swatch blends into the pageThe swatch has no visible borderKeep a border around every swatch
Selected swatch or thumbnail is coveredThe active state uses a solid filled backgroundUse a border or ring for the active state
Thumbnail causes layout movementIts dimensions are not fixed before loadingKeep fixed width and height and use object-fit: cover
Choice displays as textMetadata is missing, incomplete, invalid, or uses an unknown typeConfirm the metadata key, type, and type_value; text fallback is expected when metadata is invalid
Wrong variant is selectedCustom markup uses metadata instead of the raw choice valueSet the <li> value to the exact raw choice string
Add to cart fails after customizationThe existing selection contract was changedRestore the raw value, click handler, direct <li> structure, and active class behavior
Native dropdown does not show an image or swatchNative <option> elements are text-onlyUse a list-style renderer
Dynamic Bundle chips do not use this CSSDynamic Bundle rendering is owned by vitrin-sdkDo not add a separate theme-level Dynamic Bundle implementation

Frequently asked questions#

Do Growth Theme–based themes need custom Jinja or JavaScript?#

No. Growth Theme–based themes using Vitrin's V2 product-options renderer only need the required CSS. No Jinja, JavaScript, or variant-selection changes are needed.

What happens when choices_metadata is missing or invalid?#

Vitrin renders the normal text option. This preserves compatibility with existing products and partial Catalog responses.

Does type: "icon" expect an icon-library name?#

No. The type_value for icon must be an absolute image URL, and the choice is displayed as an image thumbnail.

Does this CSS apply to Dynamic Bundle options?#

No. Dynamic Bundle option chips are handled internally by vitrin-sdk. The CSS in this guide applies to Product Details Page list options only.

Can native <select> dropdowns display color swatches or images?#

No. Native <option> elements do not reliably render images or color swatches. Use a list-style renderer when visual choices are required.

Which value must a custom renderer use for variant selection?#

A custom renderer must use the exact raw choice string. Using the metadata ID, hex color, or image URL can break variant matching and add-to-cart behavior.

💡

Best practices#

1.
Prefer the shared V2 product-options renderer whenever possible; it owns the metadata and fallback logic.
2.
Treat metadata as presentation only. The raw choice string remains the source of truth for selection.
3.
Always provide a text fallback for incomplete or invalid metadata.
4.
Do not attempt visual options in native <select> menus.
5.
Validate the selected, unavailable, and responsive states before publishing your theme.
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.
Modified at 2026-07-20 10:08:00
Previous
Add Preorder Support to Your Theme
Next
Scripts
Built with