Variant matrix
ProEvery variant of a generated batch on one table, concepts across and formats down, each at its own shape, showing what is ready, what is still being made and what needs a person.
npx prodular-ui add variant-matrixPart of Prodular UI Pro. Preview and read the docs freely, install with a license. See pricing
Campaign
Five concepts in three formats. A warning ring needs review, the AI dot was fixed by the agent, the last concept is still being made. Pick any variant.
One ledger, Story 9:16. The headline runs under the app's safe area on iPhone. Check it before export.
"use client"
import * as React from "react"
import { VariantMatrix, VariantMatrixCell, VariantMatrixColumn, VariantMatrixRow, VariantMatrixRowLabel, type VariantStatus,} from "@/components/ui/variant-matrix"
const concepts = [ { id: "close", label: "Close in days", headline: "Close in days", color: "var(--chart-1)" }, { id: "chase", label: "Fewer chases", headline: "Stop chasing", color: "var(--chart-2)" }, { id: "ledger", label: "One ledger", headline: "One ledger", color: "var(--chart-3)" }, { id: "audit", label: "Audit ready", headline: "Audit ready", color: "var(--chart-4)" }, { id: "cash", label: "Cash on time", headline: "Paid on time", color: "var(--chart-5)" },]const formats = [ { id: "feed", label: "Feed 1:1", aspect: "1 / 1" }, { id: "story", label: "Story 9:16", aspect: "9 / 16" }, { id: "display", label: "Display 1.91:1", aspect: "1.91 / 1" },]const status: Record<string, VariantStatus> = { "ledger-story": "review", "chase-display": "fixed", "audit-display": "generating", "cash-feed": "generating", "cash-story": "queued", "cash-display": "queued",}const note: Partial<Record<VariantStatus, string>> = { review: "The headline runs under the app's safe area on iPhone. Check it before export.", fixed: "Relay shortened the headline to fit the 1.91:1 crop.",}
function Ad({ headline, color, format }: { headline: string; color: string; format: string }) { return ( <span className="flex size-full flex-col justify-end gap-1 p-2 text-start" style={{ backgroundColor: `color-mix(in oklch, ${color} 22%, var(--card))` }}> <span className="h-1.5 w-6 rounded-full" style={{ backgroundColor: color }} /> {format !== "display" && <span className="text-xs leading-tight font-semibold text-foreground">{headline}</span>} </span> )}
export default function VariantMatrixCampaign() { const [selected, setSelected] = React.useState("ledger-story") const [concept, format] = selected.split("-") const selectedStatus = status[selected] ?? "ready" return ( <div className="flex w-full min-w-0 flex-col gap-4"> <VariantMatrix columns={concepts.length}> <VariantMatrixRow> <VariantMatrixColumn /> {concepts.map((c, i) => ( <VariantMatrixColumn key={c.id} label={c.label} description={`Concept ${i + 1}`} /> ))} </VariantMatrixRow> {formats.map((f) => ( <VariantMatrixRow key={f.id}> <VariantMatrixRowLabel>{f.label}</VariantMatrixRowLabel> {concepts.map((c) => { const key = `${c.id}-${f.id}` return ( <VariantMatrixCell key={key} status={status[key] ?? "ready"} aspect={f.aspect} label={`${c.label}, ${f.label}`} selected={selected === key} onSelect={() => setSelected(key)} > <Ad headline={c.headline} color={c.color} format={f.id} /> </VariantMatrixCell> ) })} </VariantMatrixRow> ))} </VariantMatrix> <p className="m-0 text-[13px] text-muted-foreground" aria-live="polite"> <span className="font-medium text-foreground"> {concepts.find((c) => c.id === concept)?.label}, {formats.find((f) => f.id === format)?.label}. </span>{" "} {note[selectedStatus] ?? (selectedStatus === "ready" ? "Ready to export." : "Still being made.")} </p> </div> )}Anatomy
<VariantMatrix columns={concepts.length}> <VariantMatrixRow> <VariantMatrixColumn /> {concepts.map((c) => <VariantMatrixColumn key={c.id} label={c.name} />)} </VariantMatrixRow> {formats.map((f) => ( <VariantMatrixRow key={f.id}> <VariantMatrixRowLabel>{f.label}</VariantMatrixRowLabel> {concepts.map((c) => ( <VariantMatrixCell key={c.id} status={status} aspect={f.aspect} label={`${c.name}, ${f.label}`}> <img src={url} alt="" /> </VariantMatrixCell> ))} </VariantMatrixRow> ))}</VariantMatrix>Guidance
Use it when
- Use it when an agent makes a set of variants at once: ad formats, image sizes, email versions, localized screens.
- Show the whole batch while it is being made, so people see progress and can open what is ready.
- Keep the real proportions of each format so problems with a crop are visible.
Avoid
- A gallery of unrelated images. Use a grid of cards.
- More than eight concepts. Split the batch or let it scroll sideways, as it does.
- Marking every variant for review. Mark the ones a person must decide on.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Render one VariantMatrixRow of VariantMatrixColumn first, starting with an empty VariantMatrixColumn above the row labels, then one VariantMatrixRow per format with a VariantMatrixRowLabel and one VariantMatrixCell per concept, in the same order.
- Pass columns with the number of concepts, and aspect on every cell from the format, for example "9 / 16".
- Status: queued before work starts, generating while the model makes it, review when a check failed and a person must look, fixed when the agent corrected it itself, ready otherwise. Leave children out while queued or generating.
- Give every cell a label with the concept and format, for example "Night runs, Story 9:16". It is what screen readers hear, with the status.
- Pass onSelect to open the variant, and set selected on the open one.
API
VariantMatrix
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | number | Concept columns after the row labels. Set --variant-width to change the column width (96px). |
VariantMatrixColumn
| Prop | Type | Default | Description |
|---|---|---|---|
| label / description | ReactNode | Concept heading. The first, empty one sits above the row labels. |
VariantMatrixCell
| Prop | Type | Default | Description |
|---|---|---|---|
| status | "ready" | "queued" | "generating" | "review" | "fixed" | "ready" | Exposed as data-status. Review draws a warning ring, fixed an AI dot. |
| aspect | string | "1 / 1" | The format's shape as a CSS aspect ratio. |
| label | string | Concept and format, for screen readers. | |
| selected / onSelect | boolean, () => void | Makes the cell a button that opens the variant. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add variant-matrix# or, with the registry in components.jsonnpx shadcn@latest add @prodular/variant-matrix