---
title: Product Variants
description: One model, several configurations, switched without reloading the asset - the GLB already holds them.
source: https://adamjurek.com/components/product-variants
---

## Home


One paragraph on what this does and when to reach for it. This is the first
thing on the component's page and the line that goes into `llms.txt`, so it
should survive being read on its own.

<!-- ::start:showcase demo="product-variants" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

The decision somebody would otherwise have to reverse-engineer from the source.
Not what the code does - the source says that - but what it is avoiding.

## What it does not do

The boundary. A component that lists what it is not is a component you can
decide against in ten seconds.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack, shadcn and pnpm
> commands appended to the bottom of this tab, along with its version,
> dependencies and files. Do not add your own - the generated ones cannot go
> stale, and a second copy immediately does.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/product-variants.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/product-variants.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | 3d · Scenes |
| Files | `variant-swatch.tsx` |
| Dependencies | `@sushindustries/react-product-viewer@workspace:*`, `three@0.185.1` |
| Also installs | `product-viewer` |
| Tags | 3d, toggle, group, state |


## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Something } from "@sushindustries/ui";

export function Example() {
	return <Something />;
}
```

## What you should see

Describe the result, so somebody can tell a working install from a silent one.
A component that renders nothing when it is correct needs this paragraph more
than any other component does.

## If nothing happens

The two or three things that are actually wrong when it does not work: the
tokens are not imported, the parent has no height, a prop defaults to off.


## Guides


The Guides tab is for the things that are true after it works. If it belongs in
"how do I install this", it goes in Get Started; if it is a prop table, it goes
in API.

## Composing it

What it is meant to sit inside, and what it expects from that parent. Most
components that "do not work" are components in a parent that gives them no
height, no position or no width.

## Variants

Variants are data attributes, never a second class name. The component takes a
prop; the prop writes `data-*`; the stylesheet selects on it:

```tsx
<Something tone="quiet" />
```

```css
.product-variants[data-tone="quiet"] {
	color: var(--fg-faint);
}
```

Adding a class from the outside works exactly once, on the page that also has
the CSS for it. A prop travels with the component.

## Motion and reduced motion

If it animates, say what it does under `prefers-reduced-motion: reduce`, and
say it here rather than leaving somebody to test it.

## When not to use it

The case this is the wrong answer to. A component that lists this is a
component somebody can decide against quickly, which is a service.


## API


<!-- generated:api -->

## Props

Accepts every prop of `Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children">`, plus:

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `variant` | `string` | - | The variant name, used as the accessible label. |
| `label?` | `string` | - | Shown instead of the raw variant name. |
| `swatch?` | `string` | - | Data URL from {@link useVariantSwatches}. |
| `selected?` | `boolean` | `false` |  |
| `missing?` | `boolean` | `false` | The asset does not carry this variant. Rendered as a visible state rather than hidden, because the failure this guards against is a control that looks like it works and does nothing. |
| `className?` | `string` | - | Added after `pv-variant`. |
| `showPendingSwatch?` | `boolean` | `false` | Reserve the swatch's space while it renders. Swatches arrive an effect late, so without this a row of buttons shifts sideways the moment the pictures land. |



<!-- /generated:api -->

## Notes

Anything the types cannot say: which combinations are meaningless, which
prop is ignored when another is set, and what it does when handed
something it cannot render.

<!-- /generated:api -->


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="product-variants" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { Something } from "@sushindustries/ui";

export function Page() {
	return (
		<main className="container section">
			<Something />
		</main>
	);
}
```

## What this example is not

Whatever the example quietly assumes: a fixed height, a parent that scrolls, a
route that exists. Say it, so nobody copies the example and gets the assumption
without it.
