# VariantSwatches

> Visual variant/option selector. TWO MODES - Flat mode for single-option products, Axis mode for multi-option products.

Canonical URL: https://fa7e86e3d553:3005/islands/commerce/variant-swatches

> Visual variant/option selector. TWO MODES: (1) Flat mode (variants prop) — emits variant:changed directly, use for single-option products. (2) Axis mode (axis + values props) — emits option:changed, use WITH OptionResolver for multi-option products. Set BuyBox showVariantSelector:false when using this.

## Preview

**Category**: Commerce

## Props

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| variants | array | No | - | FLAT MODE: Variant records with id, title, price, availability, image/media mapping, color, and badge |
| axis | string | No | - | AXIS MODE: Option name (e.g., "Color", "Size", "Flavor") |
| values | array | No | - | AXIS MODE: Array of value objects with value, image (optional), color (optional), badge (optional), available (optional) |
| type | enum | No | "image" | Display type: "text", "image", "color", "size_grid" |
| label | string | No | - | Label text above swatches (e.g., "Choose Color") |
| columns | number | No | - | Number of desktop columns for image and size-grid layouts |
| selectedId | string | No | - | FLAT MODE: Variant selected during hydration, including an unavailable requested variant |
| selectedValue | string | No | - | AXIS MODE: Pre-selected option value |

## Embed Example

```html
<lx-island name="VariantSwatches">
  <script type="application/json">
{
  "variants": [
    {
      "id": "gid://shopify/ProductVariant/red",
      "title": "Red",
      "price": "$49.00",
      "available": true,
      "image": "https://cdn.example.com/red.jpg",
      "mediaIds": ["red-front", "red-detail"],
      "color": "#b91c1c"
    },
    {
      "id": "gid://shopify/ProductVariant/blue",
      "title": "Blue",
      "price": "$49.00",
      "available": false,
      "mediaId": "blue-front",
      "color": "#1d4ed8"
    }
  ],
  "type": "color",
  "label": "Color",
  "selectedId": "gid://shopify/ProductVariant/red"
}
  </script>
</lx-island>
```

## Usage Patterns

**Flat mode (single-option product, emits variant:changed):**

```html
<lx-island name="VariantSwatches">
  <script type="application/json">
{}
  </script>
</lx-island>
```

**Axis mode (multi-option product, use WITH OptionResolver):**

```html
<lx-island name="VariantSwatches">
  <script type="application/json">
{}
  </script>
</lx-island>

<lx-island name="OptionResolver">
  <script type="application/json">
{
  "variants": [
    {
      "id": "example-id",
      "selectedOptions": "example-selectedOptions",
      "price": "example-price",
      "available": true
    }
  ],
  "options": [
    {
      "name": "example-name",
      "position": 0
    }
  ]
}
  </script>
</lx-island>
```

**Color swatches (type:"color"):**

```html
<lx-island name="VariantSwatches">
  <script type="application/json">
{}
  </script>
</lx-island>
```

**Text swatches (type:"text", supplements/flavors):**

```html
<lx-island name="VariantSwatches">
  <script type="application/json">
{}
  </script>
</lx-island>
```

## Events

| Event | Direction | Payload |
|-------|-----------|---------|
| variant:changed | Emits (flat mode) | `{variantId, title, price, compareAtPrice?, available, image?, mediaId?, mediaIds?, images?}` |
| option:changed | Emits (axis mode) | `{axis: string, value: string}` |
| inventory:updated | Listens (flat mode) | `{variantId: string, quantity: number, available: boolean}` |

The initial event is queued until listening islands can receive it. The root
exposes `data-selected-id`, `data-selected-value`, and
`data-selected-available` as reliable runtime state.

## Tips

- **Flat mode** (variants prop) for single-option products (e.g., only Color OR only Size) — emits `variant:changed` directly
- **Axis mode** (axis + values props) for multi-option products (e.g., Color × Size) — emits `option:changed`, use WITH [OptionResolver](/islands/commerce/option-resolver)
- When using VariantSwatches, set [BuyBox](/islands/commerce/buy-box) `showVariantSelector:false` (avoid duplicate selectors)
- Always set [BuyBox](/islands/commerce/buy-box) `listenForEvents:true` when using VariantSwatches
- `type:"image"` — visual swatches with product images (best for variants with distinct looks)
- Image swatches use contained square images. On screens below 768px they
  become a horizontally scrollable row of 68px controls so configured desktop
  columns cannot overflow the page.
- `type:"color"` — circular color swatches (use `color` hex value in values array)
- `type:"text"` — text buttons (supplements, flavors, scents)
- `type:"size_grid"` — compact size selector (S, M, L, XL in grid, fashion/apparel)
- Use `badge` in values for "Low Stock", "Best Seller", "New" labels
- Set `available:false` in values to show strikethrough/disabled state
- Live inventory updates also disable and dim a variant when `quantity` reaches
  zero. Sold-out controls remain visible and include “Sold out” in their
  accessible name.
- Pair with [ProductGallery](/islands/commerce/product-gallery)
  `listenForVariant:true` and `variantMediaMode:"focus"` to move to matching
  media without collapsing the complete gallery.
- Wrap all cooperating islands in the same `data-scope`. Use a unique scope for
  each product when several products appear on one page.

## Stylable Parts

Target internal elements via `data-part` selectors in section CSS:

| Part | Description |
|------|-------------|
| `badge` | Badge overlay |
| `grid` | Swatch/option grid container |
| `label` | Unit label text |
| `root` | Outermost container |
| `swatch` | Individual swatch button |
| `swatch-color` | Color circle swatch |
| `swatch-image` | Image swatch thumbnail |
| `unavailable-slash` | Diagonal unavailable indicator |

```css
/* Example: larger color swatches with shadow */
[data-section-id='SECTION'] [data-part='swatch-color'] {
  width: 2.5rem;
  height: 2.5rem;
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
[data-section-id='SECTION'] [data-part='grid'] {
  gap: 1rem;
}
```

Image-swatch sizing variables:

| Variable | Purpose |
|---|---|
| `--lx-variant-swatches-mobile-size` | Narrow-screen square size; defaults to `68px` |
| `--lx-variant-swatches-desktop-min-size` | Minimum desktop image-swatch column size; defaults to `64px` |
| `--lx-variant-swatches-columns` | Runtime desktop column count derived from `columns` |

## Related Islands

- [BuyBox](/islands/commerce/buy-box) — Listens to `variant:changed`; set `showVariantSelector:false` and `listenForEvents:true`
- [OptionResolver](/islands/commerce/option-resolver) — Required for axis mode (multi-option products); resolves final variant from multiple option:changed events
- [ProductGallery](/islands/commerce/product-gallery) — Listens to `variant:changed` to swap images
- [InventoryIndicator](/islands/commerce/inventory-indicator) — Listens to `variant:changed` to show per-variant stock
- [PaymentOptions](/islands/commerce/payment-options) — Listens to `variant:changed` to update BNPL price
