> ## Documentation Index
> Fetch the complete documentation index at: https://docs.uselayers.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Product card controller

> Reference for the createProductCard() controller in SDK v3, covering variant selection, option availability, swatches, media, and price state.

The `createProductCard()` controller manages the state needed to render an interactive product card. It keeps variant selection, option availability, media, and prices together while you render the current `state`.

## Create a controller

```typescript theme={null}
import { createProductCard } from '@commerce-blocks/sdk'

const controller = createProductCard({
  product,
  selectedOptions: product.selectedOptions,
  breakoutOptions: product.breakoutOptions,
})
```

`createProductCard({ product, selectedOptions?, breakoutOptions? })` returns a `ProductCardController`. `selectedOptions` defaults to `product.selectedOptions`; pass `[]` to opt out. `breakoutOptions` defaults to `product.breakoutOptions`.

## Controller surface

| Member                          | Description                                                    |
| :------------------------------ | :------------------------------------------------------------- |
| `product`                       | The product passed to `createProductCard()`.                   |
| `state`                         | Readonly `ProductCardState` plain object.                      |
| `selectOption(option)`          | Select or replace an option by name.                           |
| `setSelectedOptions(options)`   | Set selected options.                                          |
| `setSelectedVariant(variantId)` | Select a variant by its numeric ID.                            |
| `setCarouselPosition(position)` | Set the active carousel position.                              |
| `subscribe(callback)`           | Subscribe to state changes and return an unsubscribe function. |
| `dispose()`                     | Remove listeners when the card is torn down.                   |

`state` is a plain object, not a signal. Read it synchronously or subscribe to updates.

## State types

```typescript theme={null}
interface ProductCardState {
  variants: ProductVariant[]
  selectedVariant: ProductVariant | null
  options: OptionGroup[]
  images: Image[]
  price: PriceData
  priceRange: PriceRangeData
  carouselPosition: number
  isSelectionComplete: boolean
}

interface OptionGroup {
  name: string
  values: OptionValue[]
}

interface OptionValue {
  value: string
  status: OptionStatus
  selected: boolean
  swatch: Swatch | null
}

type OptionStatus = 'available' | 'backorderable' | 'sold-out' | 'unavailable'

interface PriceData {
  price: Price | null
  compareAtPrice: Price | null
  isOnSale: boolean
}

interface PriceRangeData {
  priceRange: PriceRange | null
  compareAtPriceRange: PriceRange | null
}
```

## Vanilla JavaScript example

Subscribe to the controller to render option buttons and the selected price. Disable options whose status is `unavailable`. Call the unsubscribe function and `dispose()` when the card is removed.

```js theme={null}
import { createProductCard } from '@commerce-blocks/sdk'

const controller = createProductCard({ product })
const card = document.querySelector('[data-product-card]')

function render(state) {
  const optionButtons = state.options
    .flatMap((group) =>
      group.values.map((option) => `
        <button
          type="button"
          data-option-name="${group.name}"
          data-option-value="${option.value}"
          ${option.status === 'unavailable' ? 'disabled' : ''}
          aria-pressed="${option.selected}"
        >
          ${option.value}
        </button>
      `),
    )
    .join('')

  card.innerHTML = `
    <div class="product-card__options">${optionButtons}</div>
    <span class="product-card__price">${state.price.price?.formatted ?? ''}</span>
  `

  card.querySelectorAll('[data-option-name]').forEach((button) => {
    button.addEventListener('click', () => {
      controller.selectOption({
        name: button.dataset.optionName,
        value: button.dataset.optionValue,
      })
    })
  })
}

const unsubscribe = controller.subscribe(render)

function teardown() {
  unsubscribe()
  controller.dispose()
}
```

For a React integration, see [ProductCard integration](/sdk/framework-integration#productcard-integration).

## Next steps

* [Framework integration](/sdk/framework-integration)
* [Response types and error handling](/sdk/api-reference/responses-and-errors)
* [Client methods](/sdk/api-reference/client-methods)
