Product Options
Choose a product's size, color or other options and a quantity, then add it to the cart.
Experience · Shares patterns with Option Builder, Entity Picker
≈ 2,154 tokens per use (tool 1,184 · guidance 297 · typical payload 673)How token costs are estimated
Example: Product Options
I'll take the Stride Lite 2.

Stride Lite 2
Rated 4.6 out of 5 from 1,240 reviewsWhen to use
- The user wants a specific product that comes in options (size, color, grind, bag size) and is ready to add it to the cart.
- The catalog gave you the options, prices and stock for that product.
When not to use
- The user is still comparing several products. Show them as Entity Cards first.
- The product has no options. Add it directly, or use an Entity Card's
add_to_cartaction. - You don't have the options or stock from the catalog. Never invent sizes, prices or availability.
- The user wants to pay. This component only adds to the cart; payment happens in the host's own checkout.
How the agent should use it
- Pass options, prices,
priceDeltaandvariantsexactly as the catalog returned them. - Give
variantswithstockwhenever the catalog has them, so out-of-stock combinations are disabled. - Set
compareAtonly when the catalog reports an original price. - On
add_to_cart, add exactly the returnedvariantId(or options) and quantity through the cart API, then confirm in one line. - On
cancel, add nothing. - Never ask for card or payment details in chat.
How availability works
- A value marked
available: falseis out of stock in every combination: it's greyed out and can't be chosen. A color value can carry aswatchhex, shown as a dot. - With
variants, a value is only available when an in-stock variant matches it and the user's other choices. Choosing "Ember" crosses out the sizes that aren't in stock in Ember, so the user can never reach a sold-out combination. Combinations missing fromvariantscount as out of stock. - A variant with
stockof 5 or fewer shows "Only N left", andstockcaps the quantity. - A group with a single in-stock value is chosen automatically.
- The unit price adds each chosen value's
priceDelta; the total multiplies by the quantity. Money is computed in integer minor units, so totals don't drift. - The card shows one price: the line total, large, next to the Add to cart button. With a quantity above 1 it notes the unit price ("2 × £18.00"); with
compareAtthe original line price sits struck through beside it.
The logic lives in product-options-variants.ts as pure functions (isValueAvailable, resolveVariant, priceFor), so you can reuse it on the server.
Installation
npx shadcn@latest add https://your-site.com/r/product-options.json
Usage
import { tool } from "ai"
import { productOptionsTool } from "@/components/genui/product-options.schema"
const tools = {
// No execute: the user's choice is the tool result.
[productOptionsTool.name]: tool({
description: productOptionsTool.description,
inputSchema: productOptionsTool.inputSchema,
}),
}Render the tool call, and return the user's choice as the tool result:
import { ProductOptions } from "@/components/genui/product-options"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-choose_product_options") return null
return (
<ProductOptions
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({
tool: "choose_product_options",
toolCallId: part.toolCallId,
output: result,
})
}
/>
)
}The agent receives one of:
{ action: "add_to_cart", productId: "sku_4471", variantId: "sku_4471-black-42", options: { size: "42", color: "black" }, quantity: 1 }
{ action: "cancel" }Add the item through your cart API with the returned ids. Don't trust prices from the client: the cart and checkout look them up again.
With LangGraph
Push it from a graph node with ui.push({ name: "choose_product_options", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
When a conversation is reloaded, pass the stored result back as result. The component shows the chosen options and "Added to cart" (or that it was cancelled) and never calls onResult again, so the item can't be added twice. With the AI SDK, that's the tool call's output:
<ProductOptions
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
product | Product | Data-bound |
options | OptionGroup[] | 1–3 groups. Data-bound |
variants | { id, options, available, stock? }[]? | Every combination sold, e.g. options: { size: "42", color: "black" }. Data-bound |
quantity | { min?, max?, default? }? | Default 1–10, starting at min |
submitLabel | string? | Default "Add to cart" |
Product
| Field | Type | Notes |
|---|---|---|
id | string | Returned as productId |
name | string | |
image | { url, alt }? | http(s) only. Placeholder when missing or broken |
price | { amount, currency } | Base unit price. ISO 4217 currency |
compareAt | { amount }? | Original price, shown struck through |
rating | { value, count? }? | value from 0 to 5 |
OptionGroup
| Field | Type | Notes |
|---|---|---|
id | string | e.g. size. Keys the result's options |
label | string | e.g. "Size" |
values | { value, label, available?, priceDelta? }[] | 1–16. priceDelta is added to the unit price |
Host props: status (streaming, ready, added, cancelled), onResult, result and className.