useProductConfigurator
The development SDK adds typed product choices, compatibility rules, reversible
model appearance, and adapters for application-owned commerce services.
useProductConfigurator owns the choices and asynchronous service lifecycle;
ProductOptions is an optional accessible UI. These APIs require the development
SDK until its next release. ModelViewer still requires a valid license key.
Choose how much code you need
| You want | Use |
|---|---|
| No code on your site | Author it in the editor and embed the project |
| A panel beside your viewer | ModelViewer with productOptions |
| Your own layout, or your own commerce backend | This hook, with ProductOptions or your own controls |
Furniture example
"use client";
import {
ModelViewer,
ProductOptions,
useProductConfigurator,
type ProductCommerceAdapter,
type ProductDefinition,
type ObjectBinding,
} from "@liveroom-tech/react-immersive";
import "@liveroom-tech/react-immersive/styles.css";
const product: ProductDefinition = {
id: "studio-chair", label: "Studio chair", revision: "1",
groups: [
{ id: "finish", label: "Finish", required: true, defaultOptionId: "oak",
options: [
{ id: "oak", label: "Natural oak", appearance: [
{ target: { group: "frame" }, material: { baseColor: "#b89063" } },
] },
{ id: "walnut", label: "Dark walnut", appearance: [
{ target: { group: "frame" }, material: { baseColor: "#453025" } },
] },
] },
{ id: "seat", label: "Upholstery", required: true, defaultOptionId: "linen",
options: [
{ id: "linen", label: "Linen" },
{ id: "leather", label: "Leather", appearance: [
{ target: "Seat", material: { baseColor: "#99512b" } },
] },
] },
],
rules: [{ id: "leather-frame", when: { groupId: "seat", optionId: "leather" },
requires: [{ groupId: "finish", optionId: "walnut" }],
message: "Leather requires the walnut frame." }],
};
const bindings: Record<string, ObjectBinding> = {
Frame: { id: "frame", modelObjectId: "Frame", type: "furniture", group: "frame" },
Seat: { id: "seat", modelObjectId: "Seat", type: "furniture" },
};
export function ChairConfigurator({ licenseKey, adapter }: {
licenseKey: string; adapter: ProductCommerceAdapter;
}) {
const c = useProductConfigurator({ product, adapter, objectBindings: bindings });
return <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 24 }}>
<div style={{ height: 600 }}>
<ModelViewer modelUrl="/chair.glb" licenseKey={licenseKey} {...c.viewerProps} />
</div>
<ProductOptions configurator={c} />
</div>;
}Use binding keys, IDs, or the existing { group }, { tag }, and { type }
selectors in appearance.target. Edits support material and visible.
Each option can also select glTF material variants: ["Walnut"]. Start from
base bindings on every render: switching choices or removing sceneConfig.product
restores the authored appearance. Existing style rules apply after product options.
When multiple groups edit the same property, later groups in the definition win.
Variants use the existing glTF variant-group conflict handling.
The SDK source includes a runnable example at
examples/product-configurator/. It provides original chair geometry, finishes,
upholstery, armrests, local demo pricing, cart/quote callbacks, and choice saving.
From the SDK’s Vite server, open /examples/product-configurator/.
Rules and SKU mappings
Each group selects one option. A required group must have a choice; optional
groups allow null. Explicit defaultOptionId values seed initial choices.
requires and excludes contain option references, activated when when is
selected. Both sides are checked: changing a prerequisite is blocked if it would
invalidate a selected dependent option. Unavailable choices explain why.
selectOption(groupId, optionId) never changes another choice: it returns false
for a blocked choice and explains why in error. selectOptionWithFix(groupId, optionId) also changes whatever a rule needs, with as few changes as possible.
optionStates[n].fix lists those changes in advance so you can tell the shopper;
ProductOptions does this for you. A choice no combination allows has no fix.
Use setSelections(record) to change several choices at once. Logical conflicts
restored from a saved file remain visible for repair; they cannot be submitted to
commerce services.
Optional skus: [{ sku, selections }] entries map exact combinations to a SKU.
Include every group, using null for unselected optional groups. When the table
is nonempty, a combination without a mapping is invalid. Unmapped intermediate
choices remain selectable so users can reach a mapped combination. An option’s
own sku is listed in configuration.items. With no table, the chosen options’
SKUs are joined in group order, such as WALNUT-LEATHER, if they all have one.
generateProductSkus(product) builds a table of every allowed combination this way.
A resolveSku adapter can resolve or override the final SKU on the server.
Application-owned commerce adapter
import type {
ProductCommerceAdapter, ProductConfiguration, ProductAvailability,
ProductPrice, ProductCommerceResult,
} from "@liveroom-tech/react-immersive";
async function post<T>(url: string, body: unknown, signal: AbortSignal): Promise<T> {
const response = await fetch(url, {
method: "POST", signal, headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`Commerce service failed (${response.status}).`);
return response.json() as Promise<T>;
}
// Implement these routes in your application. No service credentials belong here.
export const chairCommerce: ProductCommerceAdapter = {
key: "store-usd-retail-v1",
resolveSku: async (configuration, { signal }) =>
(await post<{ sku: string | null }>("/api/commerce/sku", configuration, signal)).sku,
getAvailability: (configuration, { signal }) =>
post<ProductAvailability>("/api/commerce/availability", configuration, signal),
getPrice: (configuration, { signal }) =>
post<ProductPrice>("/api/commerce/price", configuration, signal),
addToCart: (configuration, { signal, idempotencyKey }) =>
post<ProductCommerceResult>("/api/commerce/cart", { configuration, idempotencyKey }, signal),
requestQuote: (configuration, { signal, idempotencyKey }) =>
post<ProductCommerceResult>("/api/commerce/quote", { configuration, idempotencyKey }, signal),
};| Method | Returns |
|---|---|
resolveSku(configuration, context) | Nonempty SKU string or null |
getAvailability(configuration, context) | { available, message?, options?: [{ groupId, optionId, available, reason? }] } |
getPrice(configuration, context) | { amountMinor, currency, expiresAt?, label? } |
addToCart(configuration, context) | { id?, message? } |
requestQuote(configuration, context) | { id?, message? } |
Methods may be synchronous or asynchronous. All are optional. The adapter’s
stable key identifies its backend and pricing context; change it when store,
currency, customer, or pricing context changes. Inline adapter objects with the
same key do not restart requests on every render. Call refresh() when stock
or pricing changes within the same context.
SKU resolution precedes price and availability reads. Each service receives a
detached configuration snapshot and an AbortSignal. Old responses are ignored
after choices/context changes. Errors clear the old price and prevent submission.
ProductPrice.amountMinor is a nonnegative safe integer in the currency’s minor
unit; currency is a three-letter uppercase currency code. Prices are configuration
totals for the current quantity. Optional ISO expiresAt disables submission
when the price expires. The hook validates service result shapes.
Cart/quote callbacks receive an additional idempotencyKey and the displayed
price (or null); persist/deduplicate that key in your own service. Duplicate
clicks share an in-flight request. Failed retries reuse the key; a successful
submission gets a new key on the next action. Choice/quantity edits wait while
a cart or quote request runs. Unmount aborts in-flight work.
The hook gates actions on valid choices, current reads, stock and price expiry.
A configured SKU resolver returning null blocks cart actions while permitting
quotes. If you omit price, stock or SKU services, your action callback handles
those checks itself. Server-side validation must recheck product revision,
choices, SKU, stock, quantity, taxes and final price before placing any order.
Rendering and a browser price do not authorize payment. The SDK ships no vendor
credentials, payment processing, or hosted checkout backend.
Hook options and result
| Option | Meaning |
|---|---|
product | Required ProductDefinition; new product ID/revision resets uncontrolled choices |
initialSelections | Initial uncontrolled choices, otherwise explicit product defaults |
selections, onSelectionsChange | Controlled choices; without a callback, controls are read-only |
initialQuantity | Integer from 1 to 9999, default 1 |
adapter | Application-owned commerce services |
objectBindings, sceneConfig | Base viewer data included in viewerProps |
The result includes configuration, optionStates, selectOption, setSelections,
reset, setQuantity, viewerProps, status, price, availability, error,
pendingAction, result, canAddToCart, canRequestQuote, hasCommerce, hasCart, hasQuote,
readOnly, addToCart, requestQuote, refresh, exportConfiguration and
importConfiguration. status is idle for invalid choices, loading during
reads, ready after reads, or error after a service failure. Without an adapter,
use the same hook for option selection and preview.
ProductOptions accepts { configurator, className?, showCommerce? }. Set
showCommerce={false} for an authoring preview. The component uses fieldsets,
radio groups and accessible reasons/status messages. Import the SDK stylesheet,
or build controls using the hook. Colors can be themed through
--ri-product-text, --ri-product-muted, --ri-product-warning,
--ri-product-error and --ri-product-success.
Prices and buy links without a backend
A definition can price itself and link to checkout, with no adapter:
const product: ProductDefinition = {
id: "studio-chair", label: "Studio chair", revision: "1",
pricing: { currency: "USD", basePriceMinor: 24900 },
checkout: { urlTemplate: "https://shop.example.com/cart/add?sku={sku}&qty={quantity}", label: "Add to cart" },
groups: [{
id: "finish", label: "Finish", required: true, defaultOptionId: "oak", display: "swatches",
options: [
{ id: "oak", label: "Oak", sku: "OAK", swatch: "#b89063" },
{ id: "walnut", label: "Walnut", sku: "WALNUT", swatch: "#453025", priceDeltaMinor: 4000 },
],
}],
};Amounts are in the currency’s smallest unit, so 24900 is $249.00. c.price and
c.checkoutUrl follow the choices. An adapter’s getPrice replaces the definition’s
price. A template can use {sku}, {quantity}, {productId}, {revision} and
{selections}, each URL-encoded, and must be http or https. checkoutUrl is null
until the configuration is valid, and until there’s an SKU if the template uses
{sku}. These prices are for display: validate and charge on your server.
Group display is "list" (the default), "chips" or "swatches". Options can
have a description and a swatch hex color.
Persistence and pure helpers
exportConfiguration() produces portable JSON containing version, product ID,
revision, quantity and canonical sorted selections. importConfiguration(input)
rejects another product/revision or unknown references and reevaluates rules;
SKU and validity are regenerated rather than trusted from the file. Commerce
reads run again. Prices, adapter methods and credentials are never serialized.
viewerProps.sceneConfig.product contains the definition and selections. Full
viewer configuration saving/sharing, Scene JSON/TypeScript exports, model
switching and editor autosave retain that data, including option textures.
Quantity belongs to the commerce hook’s portable configuration, not scene settings.
Short links still use your existing application-owned storage adapter.
To put choices in a link, use encodeProductSelections(selections) and
decodeProductSelections(text, product), which ignores anything the product doesn’t
have. productDefinitionWarnings(product, bindings, variantNames) reports authoring
problems, such as a change targeting objects the model doesn’t have.
The main package and React-free /utils entry export parseProductDefinition,
defaultProductSelections, evaluateProductConfiguration, productOptionStates,
parseSceneProduct, serializeProductConfiguration, parseProductConfiguration,
applyProductOptions, resolveProductSelections, productPriceFromDefinition,
formatProductPrice, productCheckoutUrl, encodeProductSelections,
decodeProductSelections, generateProductSkus and productDefinitionWarnings,
plus the product data types. Validate untrusted definitions
with parseProductDefinition before using pure evaluation/projection helpers.
configuration.key is canonical JSON of product ID, revision, quantity and sorted
choices; treat it as an identity, not a cryptographic signature. Increment the
revision when the catalog or business rules change.