Skip to content
Nizivo UI

Variant picker

A product's colour and size options as chips of native radios, with an out-of-stock option shown as sold out rather than disabled.

From Figma 05 Commerce, Variant Option (7275:14018; Colour 7275:14019, Size 7275:14028). Uses ChipGroup, Field, Button, Stack, Text and NumberValue.

Examples

Colour and size

Figma 05's Variant Option (7275:14018) as a product's option picker: colour chips with swatches and size chips, each a ChipGroup of native radios in a Field that names it, so arrow keys move the choice and the form submits it with no JavaScript of its own. Sand and L are out of stock: they keep their colour, are struck through and are described as sold out, and can still be chosen (to offer "Notify me", for example).

Linen apron

$42.00

import type { JSX } from 'react';
import { Button } from '@nizivo/react/button';
import { Chip, ChipGroup } from '@nizivo/react/chip-group';
import { Field } from '@nizivo/react/field';
import { NumberValue } from '@nizivo/react/number-value';
import { Stack } from '@nizivo/react/stack';
import { Text } from '@nizivo/react/text';

/** Money as the store's currency: Intl formats it, never string concatenation. */
const USD = { style: 'currency', currency: 'USD' } as const;

interface Option {
  value: string;
  label: string;
  /** In stock in this option. An unavailable option stays choosable, struck through and described. */
  available: boolean;
}

interface ColorOption extends Option {
  /** A product colour is data: the variant's own swatch colour, never a theme token. */
  swatch: string;
}

const COLORS: ColorOption[] = [
  { value: 'moss', label: 'Moss', swatch: '#2e5a3c', available: true },
  { value: 'clay', label: 'Clay', swatch: '#b5643c', available: true },
  { value: 'sand', label: 'Sand', swatch: '#e9dcc3', available: false },
  { value: 'slate', label: 'Slate', swatch: '#4a5560', available: true },
];

const SIZES: Option[] = [
  { value: 's', label: 'S', available: true },
  { value: 'm', label: 'M', available: true },
  { value: 'l', label: 'L', available: false },
  { value: 'xl', label: 'XL', available: true },
];

/** The swatch: any element with the colour as its background, in the chip's 24 px circle. */
function Swatch({ color }: { color: string }): JSX.Element {
  return <span style={{ background: color }} />;
}

export function VariantPickerExample(): JSX.Element {
  return (
    <form action="#" aria-label="Linen apron">
      <Stack gap="5">
        <Stack gap="1">
          <Text variant="label-l">Linen apron</Text>
          <Text variant="numeric-m">
            <NumberValue value={42} format={USD} />
          </Text>
        </Stack>
        <Field label="Colour" name="color" group required>
          <ChipGroup defaultValue="moss" labels={{ unavailable: 'Sold out' }}>
            {COLORS.map((color) => (
              <Chip
                key={color.value}
                value={color.value}
                leading={<Swatch color={color.swatch} />}
                unavailable={!color.available}
              >
                {color.label}
              </Chip>
            ))}
          </ChipGroup>
        </Field>
        <Field label="Size" name="size" group required>
          <ChipGroup defaultValue="m" labels={{ unavailable: 'Sold out' }}>
            {SIZES.map((size) => (
              <Chip key={size.value} value={size.value} unavailable={!size.available}>
                {size.label}
              </Chip>
            ))}
          </ChipGroup>
        </Field>
        <div>
          <Button type="submit" variant="primary">
            Add to bag
          </Button>
        </div>
      </Stack>
    </form>
  );
}

Notes

  • Client JavaScript. ChipGroup and its Field are client components. The chips are native radios in a <form>, so the choice works and submits before they hydrate.
  • Native radios. Each group is a radio group named by its Field's label ("Colour", "Size"): Tab enters the group, arrow keys move the choice, and the form submits color and size. No headless library is needed.
  • Unavailable, not disabled. Figma's Disabled state greys the chip and replaces the colour. Here an out-of-stock option keeps its colour, is struck through and is described as "Sold out", and stays focusable, so a screen reader user learns it exists and an app can offer "Notify me". Add disabled only for an option that cannot be chosen at all.
  • Colour is data. Figma binds the swatch to a chart token; the swatch is the variant's own colour, passed as the leading element's background, with a thin ring so a light colour such as Sand shows on white.
  • No layout shift. Figma's selected chip grows from 90 × 40 to 92 × 42 as its stroke thickens; the chips keep their size when chosen.
  • Spelling. The visible label is "Colour", as Figma writes it; the form field is color.

On this page