logo

🚀 UI Package Integration Guide

The injectConfigurator function injects the OV25 configurator UI into existing DOM elements on your page. It finds elements by CSS selectors and either replaces or appends the configurator components.


🛠️ OV25 Setup (Visual Config Builder)

Before you start hand-writing the config object, the fastest way to dial in the right combination of selectors, display modes, branding, and behaviour flags is the visual setup tool at app.orbital.vision/configurator-setup.

OV25 Configurator Setup UI

It's a UI wrapper around the same InjectConfiguratorInput JSON shape documented on this page - every toggle, dropdown, and text field on the left maps 1:1 to a property of the config object, and the preview on the right re-injects the configurator live so you can confirm the result before copying anything into your storefront.

What the setup tool covers:

  • Product Type - Standard, Snap2, or Bed shells (controls productLink shape and the relevant bed block)
  • Elements - which selectors to inject (gallery, price, name, variants, swatches, configureButton)
  • Configurator - displayMode, triggerStyle, variants.displayMode, and variants.hideOptions
  • Image Gallery - carousel.desktop / carousel.mobile and carousel.maxImages
  • Branding - branding.logoURL, branding.mobileLogoURL
  • Element styles - targeted CSS rules for known OV25 UI elements, emitted through branding.cssString
  • Text overrides - customer-facing copy replacements, emitted through top-level stringReplacements|
  • Beaviour every entry in flags (hidePricing, disableAddToCart, disableBuyNow, hideGestureHint, hideAr, deferThreeD, showOptional, forceMobile, autoOpen)

Element Styles

The Style tab includes visual controls for many commonly styled elements of the configurator UI. These all end up in the generated config as branding.cssString.

Element Styles

You may want to further customise the configurator UI - The setup tool provides a searchable list of supported selectors, so you can easily target and apply styling to any part of the configurator UI.

Element Styles

The setup tool currently supports common CSS properties such as background, background-color, color, border, border-color, border-width, border-radius, padding, margin, gap, font-size, font-family, font-weight, opacity, display, max-height, and min-height.

If you want to add more complex CSS rules, you can add them in the Custom CSS field.

Text Overrides

The Text overrides section lets you customize customer-facing OV25 copy without changing component code. The generated config stores these values as top-level stringReplacements.

Overrides can be simple text replacements, or use conditions and dynamic values.

The below example shows how to rename the option "Fabric" to "Seat Material".

Text Overrides

This example changes the product name from "Windrush Loveseat" to "Range: Windrush / Product: Loveseat".

Text Overrides

Use text overrides for storefront wording such as button labels, option headings, product titles. The setup tool uses the same string catalog exposed by STRING_REPLACEMENT_DEFINITIONS, so each override is tied to a supported OV25 text key.

Basic text overrides use a single default rule:

{
  "stringReplacements": {
    "configureButtonText": [
      { "template": "Customise" }
    ]
  }
}

Conditional text overrides can use triggers. Trigger values are matched case-insensitively, and the first matching rule wins:

{
  "stringReplacements": {
    "optionHeader": [
      {
        "trigger": { "name": "OPTION_NAME", "value": "fabric" },
        "template": "Choose your fabric"
      },
      {
        "template": "${OPTION_NAME}"
      }
    ]
  }
}

Templates can include supported interpolation variables such as ${OPTION_NAME}, ${PRODUCT_NAME}, or ${PRICE}, depending on the selected text key. See the String Replacements section below for the full runtime config shape and matching behaviour.

Saving

When you hit Save, the tool emits the same JSON you'd otherwise pass to injectConfigurator(...):

{
  "apiKey": "15-...",
  "productLink": "217",
  "selectors": {
    "gallery": { "selector": ".configurator-container", "replace": true },
    "price": { "selector": "#price", "replace": true },
    "name": { "selector": "#name", "replace": true }
  },
  "configurator": {
    "displayMode": { "desktop": "sheet", "mobile": "drawer" },
    "triggerStyle": { "desktop": "single-button", "mobile": "single-button" },
    "variants": { "displayMode": { "desktop": "tree", "mobile": "list" } }
  },
  "carousel": { "desktop": "stacked", "mobile": "carousel" },
  "branding": { "logoURL": "https://example.com/logo.svg" },
  "flags": { "hidePricing": false, "autoOpen": false }
}

Drop that JSON straight into injectConfigurator(...) and add your callbacks (the setup tool can't generate addToBasket / buyNow / buySwatches for you - those are storefront-specific functions).

⚠️ Heads up: the setup tool can be a bit buggy in places (live preview occasionally needs a refresh, some combinations don't re-render until you toggle them again). Despite that, it's still the fastest way to find the combination of settings that look and behave correctly on your product page - once you're happy, copy the JSON out and treat this page as the source of truth for any field-level details.


📦 Installation

Install the package using npm:

npm i ov25-ui@latest

💡 Note: Always use @latest to ensure you have the most recent version with all the latest features and bug fixes.


📦 Usage

import { injectConfigurator, type InjectConfiguratorInput } from 'ov25-ui';
 
// Single configurator
injectConfigurator(config);
 
// Multiple configurators on the same page
injectConfigurator([config1, config2, config3]);

✅ Required Fields

FieldTypeDescription
apiKeyStringOrFunctionOV25 API key
productLinkStringOrFunctionProduct ID or path (e.g. '217', 'snap2/4' for multi-product modular, 'range/126')
selectorsSelectorsConfigDOM targets for gallery, price, name, variants, swatches, configureButton
callbacksCallbacksConfigaddToBasket, buyNow, buySwatches (required); onChange (optional)
type StringOrFunction = string | (() => string);
 
interface SelectorsConfig {
  /** Root target for full-page display modes that own their own shell (e.g. dining). */
  root?: ElementSelector;
  gallery?: ElementSelector;
  /** Optional header override for inline-sticky; omission enables automatic detection. */
  header?: string;
  /** Optional external carousel targets for each viewport. */
  desktopCarousel?: ElementSelector;
  mobileCarousel?: ElementSelector;
  price?: ElementSelector;
  name?: ElementSelector;
  variants?: ElementSelector;
  swatches?: ElementSelector;
  configureButton?: ElementSelector;
  /** Multi-product modular: external mount point for the starting-module menu. */
  initialiseMenu?: ElementSelector;
}

Full Config Type

interface InjectConfiguratorOptions {
  apiKey: StringOrFunction;
  productLink: StringOrFunction;
  configurationUuid?: StringOrFunction;
  images?: ProductImageInput[];
  uniqueId?: string;
  selectors: SelectorsConfig;
  carousel?: CarouselConfig;
  configurator?: ConfiguratorConfig;
  callbacks: CallbacksConfig;
  branding?: BrandingConfig;
  flags?: FlagsConfig;
  bed?: BedEmbedConfig;
  dining?: DiningEmbedConfig;
  stringReplacements?: StringReplacementsConfig;
}
 
type InjectConfiguratorInput = InjectConfiguratorOptions | LegacyInjectConfiguratorOptions;

Minimal Example

const config: InjectConfiguratorInput = {
  apiKey: () => '15-5f9c5d4197f8b45ee615ac2476e8354a160f384f01c72cd7f2638f41e164c21d',
  productLink: () => '217',
  selectors: {
    gallery: { selector: '.configurator-container', replace: true },
    price: { selector: '#price', replace: true },
    name: { selector: '#name', replace: true },
  },
  callbacks: {
    addToBasket: () => {},
    buyNow: () => {},
    buySwatches: () => {},
  },
};
injectConfigurator(config);

🎯 Selectors

Each selector can be a string (CSS selector) or an object:

type ElementConfig = {
  selector?: string;
  id?: string;  // deprecated, use selector
  replace?: boolean;
};
 
type ElementSelector = string | ElementConfig;
SelectorPurposeRequiredNotes
galleryMain 3D/image containerStandard productsFor multi-product modular configs, gallery chrome may live inside the configurator modal
headerStorefront header overrideNoUsed by inline-sticky; omit or leave blank to use automatic header detection
desktopCarouselExternal desktop carousel targetNoNon-Snap2 products only; the carousel remains embedded when no usable target exists
mobileCarouselExternal mobile carousel targetNoNon-Snap2 products only; the carousel remains embedded when no usable target exists
pricePrice displayWhen not hiding pricingOmit when flags.hidePricing: true
nameProduct nameRecommended
variantsVariant controlsProducts with variantsOmit for products without variants
swatchesSwatch selectorProducts with swatchesOmit when no swatches
configureButtonButton that opens configuratorMulti-product modularRequired for multi-product modular flows; optional for standard single-product
initialiseMenuStarting-module menu mountMulti-product modular inline layoutsOptional for other product types

Replace vs Append

  • replace: true – Replaces the target element's content with the configurator UI
  • replace: false or omitted – Appends the UI inside the target element

selectors.header is a CSS selector override for inline-sticky. It accepts a selector list, so a storefront with an announcement bar and header can provide both:

selectors: {
  gallery: { selector: '.configurator-container', replace: true },
  variants: '#ov25-controls',
  header: '#shopify-section-announcement-bar, #shopify-section-header',
},

When header is blank or omitted, OV25 automatically detects common Shopify/theme headers and suitable top-level semantic headers. An invalid selector falls back to automatic detection. A valid selector with no current match uses a zero offset and is observed in case the theme inserts the header later.

selectors.desktopCarousel and selectors.mobileCarousel move the non-Snap2 product carousel to a viewport-specific DOM target. They work independently of inline-sticky and can be used with any non-Snap2 display mode that renders a carousel:

<div class="product-layout">
  <div class="configurator-container"></div>
  <aside>
    <div id="ov25-controls"></div>
    <div data-ov25-mobile-carousel-target></div>
  </aside>
</div>
selectors: {
  gallery: { selector: '.configurator-container', replace: true },
  variants: '#ov25-controls',
  desktopCarousel: '#desktop-product-gallery',
  mobileCarousel: '[data-ov25-mobile-carousel-target]',
},

Only the selector for the current viewport is used. A target must resolve to exactly one HTML element within the configurator's gallery/variants scope. If the selector is missing, invalid, not found, or ambiguous, OV25 warns where appropriate and keeps one embedded carousel in its normal location.

Mobile inline-sticky additionally looks for [data-ov25-sticky-mobile-carousel] when mobileCarousel is omitted. If that target is also absent, the carousel remains embedded. Carousel targeting is ignored for Snap2.

header, desktopCarousel, and mobileCarousel belong to the grouped selectors API and do not have flat legacy aliases.

Examples from Tests

Standard product with full selectors:

selectors: {
  gallery: { selector: '.configurator-container', replace: true },
  variants: '#ov25-controls',
  swatches: '#ov25-swatches',
  price: { selector: '#price', replace: true },
  name: { selector: '#name', replace: true },
},

Multi-product (modular) with configure button:

selectors: {
  gallery: { selector: '.configurator-container', replace: true },
  configureButton: { selector: '#ov25-fullscreen-button', replace: false },
  variants: '#ov25-controls',
  swatches: '#ov25-swatches',
  price: { selector: '#price', replace: true },
  name: { selector: '#name', replace: true },
},

Configure button only:

selectors: {
  gallery: { selector: '.configurator-container', replace: true },
  price: { selector: '#price', replace: true },
  name: { selector: '#name', replace: true },
  configureButton: { selector: '[data-ov25-configure-button]', replace: true },
},

No variants – omit variants:

selectors: {
  gallery: { selector: '.configurator-container', replace: true },
  swatches: '#ov25-swatches',
  price: { selector: '#price', replace: true },
  name: { selector: '#name', replace: true },
},

No pricing – omit price, set flags.hidePricing: true:

selectors: {
  gallery: { selector: '.configurator-container', replace: true },
  variants: '#ov25-controls',
  swatches: '#ov25-swatches',
  name: { selector: '#name', replace: true },
},
flags: { hidePricing: true },

Controls thumbnail display below the main image.

type ResponsiveValue<T> = { desktop: T; mobile?: T };
 
type CarouselDisplayMode = 'none' | 'carousel' | 'stacked';
 
type CarouselConfig = ResponsiveValue<CarouselDisplayMode> & {
  maxImages?: number | ResponsiveValue<number>;
};
ValueDescription
'none'No carousel thumbnails
'stacked'Thumbnails stacked vertically
'carousel'Thumbnails in horizontal carousel

Defaults: desktop: 'stacked', mobile inherits from desktop.

Carousel layout and placement are separate. Use carousel to choose the presentation and image limit, then use selectors.desktopCarousel / selectors.mobileCarousel only when the carousel should render in another storefront element. With no resolved external target, it stays embedded below the gallery.

Examples

No carousel:

carousel: { desktop: 'none', mobile: 'none' },

Stacked with max images:

carousel: { desktop: 'stacked', mobile: 'stacked', maxImages: { desktop: 4, mobile: 6 } },

Horizontal carousel:

carousel: { desktop: 'carousel', mobile: 'carousel', maxImages: { desktop: 12, mobile: 6 } },

Standard:

carousel: { desktop: 'stacked', mobile: 'carousel' },

Note: Multi-product modular products (productLink: 'snap2/…') don't use the image carousel - carousel settings are ignored and treated as 'none'.


🎛️ Configurator

Controls how the configurator panel is shown and how variants are displayed.

type ResponsiveValue<T> = { desktop: T; mobile?: T };
 
type ConfiguratorDisplayMode = 'inline' | 'inline-sticky' | 'sheet' | 'drawer' | 'modal' | 'inline-sheet';
type VariantDisplayMode = 'wizard' | 'list' | 'tabs' | 'accordion' | 'tree';
 
type ConfiguratorConfig = {
  displayMode: ResponsiveValue<ConfiguratorDisplayMode>;
  triggerStyle?: ResponsiveValue<'single-button' | 'split-buttons'>;
  variants?: {
    displayMode: ResponsiveValue<VariantDisplayMode>;
    /** Multi-product modular only: which edge the variant sheet attaches to. Default 'RIGHT'. */
    position?: ResponsiveValue<'LEFT' | 'RIGHT'>;
    useSimpleVariantsSelector?: boolean;
    /** Option ids or display names (case-insensitive) to omit from the variant UI. Iframe defaults still apply. */
    hideOptions?: string[];
  };
  /** Multi-product modular only: where the compatible-modules picker is shown. Default 'BOTTOM'. */
  modules?: {
    position?: ResponsiveValue<'LEFT' | 'RIGHT' | 'BOTTOM'>;
  };
};

Display Mode

DesktopMobileDescription
'inline''inline'Variants shown inline on the page
'inline-sticky''inline-sticky'Inline variants with a viewport-sticky gallery for Standard and Bed products
'sheet''drawer'Full-screen sheet (desktop), bottom drawer (mobile)
'sheet''inline'Sheet on desktop, inline on mobile
'modal''modal'Centered modal on desktop and mobile (uses a deferred gallery container when no gallery selector is provided)
'inline-sheet''drawer'Multi-product modular only: inline gallery stage with the variant sheet docked over one edge (desktop)

Defaults: desktop: 'sheet'. When mobile is omitted, sheet and inline-sheet use drawer; inline, inline-sticky, and modal inherit the desktop mode.

Inline Sticky

inline-sticky keeps the 3D gallery visible while the page scrolls through a long inline variant list. It is available for Standard and Bed configurators and can be selected independently at each breakpoint.

import { injectConfigurator, type InjectConfiguratorInput } from 'ov25-ui';
 
const config: InjectConfiguratorInput = {
  apiKey: '15-...',
  productLink: '58',
  selectors: {
    gallery: { selector: '.configurator-container', replace: true },
    variants: '#ov25-controls',
    // Optional. Omit this to use automatic header detection.
    header: '#announcement-bar, #site-header',
    // Recommended on mobile so thumbnails sit with the product controls.
    mobileCarousel: '#mobile-product-carousel',
  },
  configurator: {
    displayMode: { desktop: 'inline-sticky', mobile: 'inline-sticky' },
    variants: { displayMode: { desktop: 'list', mobile: 'list' } },
  },
  callbacks: {
    addToBasket: () => {},
    buyNow: () => {},
    buySwatches: () => {},
  },
};
 
injectConfigurator(config);
  • Desktop: place the gallery and variants in a two-column product layout. OV25 accounts for the visible header, caps the gallery to the available viewport height, and keeps list option/group headers aligned with it.
  • Mobile: stack the gallery above the variants. OV25 makes the sticky viewer full width and keeps list headers directly below it.

The gallery and variants targets must share a product section with enough height to define the sticky boundary. OV25 repairs common blockers on its own wrappers and can use a bounded fallback when native sticky positioning is blocked. Use selectors.header when automatic header detection selects the wrong element.

If the storefront already has its own sticky implementation, use the simpler inline display mode and apply the site's sticky behavior to the gallery wrapper. This avoids OV25's header measurement, wrapper repairs, and fallback relocation.

Snap2 does not support inline-sticky. If it is requested for a snap2/... product, the runtime warns and uses modal for that breakpoint. Existing inline and inline-sheet behavior is unchanged and does not activate sticky handling.

Trigger Style

  • 'single-button' – One "Configure" button
  • 'split-buttons' – Separate Add to basket / Buy now Default: 'single-button'

Variant Display Mode

ValueDescription
'tree'Hierarchical tree
'list'Flat list
'tabs'Tabbed groups
'accordion'Collapsible option sections on desktop or mobile
'wizard'Step-by-step wizard

Defaults: desktop: 'tree', mobile: 'tree'

useSimpleVariantsSelector

When true, shows a single "Configure" button that opens the variant panel. Useful when you don't want inline variant controls.

Default: true (a single Configure button is rendered when no inline variant UI is requested).

hideOptions

Array of option ids or display names (case-insensitive) to omit from the variant UI (list, wizard, tabs, tree, accordion). Iframe defaults and CURRENT_SKU state still apply for hidden options - users simply cannot change them in the shell.

configurator: {
  displayMode: { desktop: 'inline', mobile: 'inline' },
  variants: {
    displayMode: { desktop: 'tree', mobile: 'list' },
    hideOptions: ['Wood Finish', 'feet'],
  },
},

Examples

Inline + wizard:

configurator: {
  displayMode: { desktop: 'inline', mobile: 'inline' },
  triggerStyle: { desktop: 'single-button', mobile: 'single-button' },
  variants: { displayMode: { desktop: 'wizard', mobile: 'wizard' } },
},

Sheet + tabs:

configurator: {
  displayMode: { desktop: 'sheet', mobile: 'drawer' },
  triggerStyle: { desktop: 'single-button', mobile: 'single-button' },
  variants: { displayMode: { desktop: 'tabs', mobile: 'tabs' } },
},

Inline + accordion:

configurator: {
  displayMode: { desktop: 'inline', mobile: 'inline' },
  triggerStyle: { desktop: 'single-button', mobile: 'single-button' },
  variants: { displayMode: { desktop: 'accordion', mobile: 'list' } },
},

Configure button only with simple selector:

configurator: {
  displayMode: { desktop: 'sheet', mobile: 'drawer' },
  triggerStyle: { desktop: 'single-button', mobile: 'single-button' },
  variants: {
    displayMode: { desktop: 'tabs', mobile: 'list' },
    useSimpleVariantsSelector: true,
  },
},

✏️ String Replacements

Use stringReplacements to customize text shown by the OV25 configurator without changing the component code. This is useful for client-specific wording, regional terminology, or changing labels based on the option/product currently being displayed.

import {
  injectConfigurator,
  STRING_REPLACEMENT_DEFINITIONS,
  type StringReplacementsConfig,
} from 'ov25-ui';

STRING_REPLACEMENT_DEFINITIONS exposes the supported text keys, their default text, and the interpolation variables each key accepts. Use it when building setup/admin UIs so users only configure valid keys and variables.

Config Shape

type StringReplacementsConfig = Record<string, StringReplacementRule[]>;
 
type StringReplacementRule = {
  trigger?: {
    name: string;
    value: string;
  };
  template: string;
};

Each key maps to an ordered list of rules.

  • Rules with a trigger are checked first, in array order.
  • Trigger matching is trimmed and case-insensitive.
  • The first matching triggered rule wins.
  • If no triggered rule matches, the first rule without a trigger is used as the default.
  • If no rule resolves, OV25 shows the built-in fallback text.
  • Missing interpolation variables are replaced with an empty string.

Basic Example

injectConfigurator({
  // ...existing config
  stringReplacements: {
    configureButtonText: [
      { template: 'Customize' },
    ],
    productTitle: [
      { template: '${RANGE_NAME} - ${PRODUCT_NAME}' },
    ],
  },
});

Conditional Rules

Use trigger when text should change only for a specific runtime value.

injectConfigurator({
  // ...existing config
  stringReplacements: {
    optionHeader: [
      {
        trigger: { name: 'OPTION_NAME', value: 'leg' },
        template: 'Leg Type',
      },
      {
        trigger: { name: 'OPTION_NAME', value: 'fabric' },
        template: 'Fabric Choice',
      },
      {
        template: '${OPTION_NAME}',
      },
    ],
  },
});

In this example, an option called Leg, leg, or leg will display as Leg Type. Other options fall back to their original option name.

Discovering Available Keys

Use STRING_REPLACEMENT_DEFINITIONS to list available keys and variables:

for (const definition of STRING_REPLACEMENT_DEFINITIONS) {
  console.log(definition.key, definition.label, definition.defaultTemplate);
  console.log(definition.interpolationValues);
}

Each definition has this shape:

type StringReplacementDefinition = {
  key: string;
  label: string;
  description?: string;
  defaultTemplate: string;
  interpolationValues: Array<{
    name: string;
    description?: string;
  }>;
};

Notes

  • stringReplacements is additive. If omitted, OV25 uses its normal built-in text.
  • Keep templates customer-facing. Internal ids, CSS selectors, and enum values should not be translated or replaced.
  • For Shopify and WooCommerce integrations, make sure saved config/metafields pass stringReplacements through to injectConfigurator.

📞 Callbacks

interface CallbacksConfig {
  addToBasket: (payload?: OnChangePayload) => void;
  buyNow: (payload?: OnChangePayload) => void;
  buySwatches: (swatches: Swatch[], swatchRulesData: SwatchRulesData) => void;
  onChange?: (payload: OnChangePayload) => void;
}
  • addToBasket – Add configured product or scene to basket. When invoked by the UI, receives a normalized OnChangePayload; skus and price may be null until those iframe messages have arrived.
  • buyNow – Checkout immediately. Same payload shape as addToBasket.
  • buySwatches – Purchase selected swatches. Receives Swatch[] and SwatchRulesData.
  • onChange – Optional. Fires when price or SKU updates. Payload is normalized by the UI package (see below); skus and price are each null until that message type has been received at least once.

📋 Payload Types (skus and price)

The iframe emits CURRENT_SKU and CURRENT_PRICE as postMessage events. Wire JSON can differ between single-product and multi-product configurators (one billable line vs many). The UI package normalizes both into one contract before your callbacks run.

OnChangePayload is an alias of UnifiedOnChangePayload:

type OnChangePayload = UnifiedOnChangePayload;
 
interface UnifiedOnChangePayload {
  skus: UnifiedSkuPayload | null;
  price: UnifiedPricePayload | null;
}
KeyTypeWhen populated
skusUnifiedSkuPayload | nullAfter first CURRENT_SKU
priceUnifiedPricePayload | nullAfter first CURRENT_PRICE

Canonical fields for new integrations: use payload.skus.lines and payload.price.lines, and branch on payload.skus.mode / payload.price.mode ('single' \| 'multi').

Legacy (single-product only): when skus.mode === 'single', top-level skuString and skuMap match older integrations. For multi-product SKU payloads, mode === 'multi' and there is no top-level skuString-iterate lines instead.

Null-check and narrow mode before reading skuString:

import type { OnChangePayload } from 'ov25-ui';
 
onChange: (payload: OnChangePayload) => {
  if (payload.skus?.mode === 'single') {
    const sku = payload.skus.skuString;
    const colorSku = payload.skus.skuMap?.['Color'];
  }
  if (payload.skus?.mode === 'multi') {
    for (const line of payload.skus.lines) {
      console.log(line.id, line.quantity, line.skuString, line.skuMap);
    }
  }
  if (payload.price) {
    const displayPrice = payload.price.formattedPrice;
    const hasDiscount = payload.price.discount.percentage > 0;
    for (const line of payload.price.lines) {
      console.log(line.name, line.formattedPrice, line.selections);
    }
  }
},

Optional helpers (same package) if you handle raw postMessage outside injectConfigurator: normalizeSkuPayload, normalizePricePayload, parseIframeJsonPayload.

UnifiedSkuPayload (skus)

Discriminated union:

interface CommerceLineItemSku {
  id: string;
  skuString: string;
  skuMap: Record<string, string>;
  quantity: number;
}
 
interface UnifiedSkuPayloadSingle {
  mode: 'single';
  lines: CommerceLineItemSku[]; // length 1
  skuString: string;
  skuMap?: OptionSkuMap;
}
 
interface UnifiedSkuPayloadMulti {
  mode: 'multi';
  lines: CommerceLineItemSku[];
}
 
type UnifiedSkuPayload = UnifiedSkuPayloadSingle | UnifiedSkuPayloadMulti;
type OptionSkuMap = Record<string, string>;
modeMeaningTop-level skuString / skuMapCanonical
'single'One configured productSet (backward compatible)lines[0] plus legacy fields
'multi'Multiple billable lines in the sceneAbsentlines only

OnChangeSkuPayload in TypeScript is an alias of UnifiedSkuPayload.

UnifiedPricePayload (price)

Order-level totals plus normalized per-line breakdown (replaces relying only on raw priceBreakdown / productBreakdowns from the iframe):

interface CommerceLineItemSelection {
  category?: string;
  name: string;
  sku?: string;
  price: number;
  formattedPrice: string;
  thumbnail?: string;
}
 
interface CommerceLineItemPrice {
  id: string;
  name: string;
  quantity: number;
  price: number;
  formattedPrice: string;
  subtotal: number;
  formattedSubtotal: string;
  discountedAmount: number;
  formattedDiscountAmount: string;
  discountPercentage: number;
  selections: CommerceLineItemSelection[];
  modelId?: string;
}
 
interface UnifiedPricePayload {
  mode: 'single' | 'multi';
  totalPrice: number;
  subtotal: number;
  formattedPrice: string;
  formattedSubtotal: string;
  discount: {
    amount: number;
    formattedAmount: string;
    percentage: number;
  };
  lines: CommerceLineItemPrice[];
  /** Present when the iframe sent single-product `priceBreakdown`. */
  priceBreakdown?: unknown[];
  /** Present when the iframe sent multi-product `productBreakdowns`. */
  productBreakdowns?: unknown[];
}
FieldDescription
totalPrice, subtotalMinor units (e.g. pence).
formattedPrice, formattedSubtotalDisplay strings.
linesCanonical per-line pricing; use for multi-product carts.
priceBreakdown / productBreakdownsOptional passthrough of raw iframe arrays for legacy tooling.

OnChangePricePayload is an alias of UnifiedPricePayload.

Swatch and SwatchRulesData (buySwatches)

interface Swatch {
  name: string;
  option: string;
  manufacturerId: string;
  description: string;
  sku: string;
  thumbnail: {
    blurHash: string;
    thumbnail: string;
    miniThumbnails: { large: string; medium: string; small: string };
  };
}
 
type SwatchRulesData = {
  freeSwatchLimit: number;
  canExeedFreeLimit: boolean;
  pricePerSwatch: number;
  minSwatches: number;
  maxSwatches: number;
  enabled: boolean;
};

Example with onChange

function formatSkuSummary(skus: OnChangePayload['skus']) {
  if (!skus) return '-';
  if (skus.mode === 'single') return skus.skuString;
  return skus.lines.map((l) => `${l.quantity}× ${l.skuString}`).join(', ');
}
 
callbacks: {
  addToBasket: (payload?: OnChangePayload) =>
    alert(`Checkout: ${payload?.price?.formattedPrice ?? '-'} / ${formatSkuSummary(payload?.skus ?? null)}`),
  buyNow: (payload?: OnChangePayload) =>
    alert(`Buy now: ${payload?.price?.formattedPrice ?? '-'} / ${formatSkuSummary(payload?.skus ?? null)}`),
  buySwatches: () => alert('Add swatches to cart'),
  onChange: (payload: OnChangePayload) => {
    if (payload.skus?.mode === 'single') {
      console.log('SKU:', payload.skus.skuString, payload.skus.skuMap);
    } else if (payload.skus?.mode === 'multi') {
      console.log('SKU lines:', payload.skus.lines);
    }
    if (payload.price) console.log('Price:', payload.price.formattedPrice, payload.price.discount, payload.price.lines);
  },
},

⚙️ Optional Fields

configurationUuid

Saved configuration UUID for multi-product modular experiences. Restores a previously saved scene/configuration.

productLink: () => 'snap2/4',
configurationUuid: () => '68245136-580c-4481-864c-1da82f3a50db',

images

Add storefront-provided images before the images returned by OV25 product metadata. Existing string arrays remain valid, and 0.8 also accepts tiered image objects:

import { injectConfigurator, type ProductImageInput } from 'ov25-ui';
 
const images: ProductImageInput[] = [
  'https://cdn.example.com/product/lifestyle.jpg',
  {
    alt: 'Sofa in green wool',
    urls: {
      thumbnail: 'https://cdn.example.com/product/thumbnail.jpg',
      small_image: 'https://cdn.example.com/product/small.jpg',
      image: 'https://cdn.example.com/product/standard.jpg',
      hero: 'https://cdn.example.com/product/hero.jpg',
      original: 'https://cdn.example.com/product/original.jpg',
    },
  },
];
 
injectConfigurator({
  // ...required config
  images,
});
type ProductImageInput =
  | string
  | {
      alt?: string;
      urls?: Record<string, string>;
    };

OV25 chooses the most suitable available URL for each surface:

SurfaceURL preference
Carousel thumbnailsmall_image, image, thumbnail, original, hero
Stacked imageimage, original, hero, small_image, thumbnail
Main gallery imagehero, image, small_image, thumbnail
Fullscreen imageoriginal, hero, image, small_image, thumbnail

carousel.maxImages applies after supplied images and product metadata images are combined. A missing image URL on a rendered variant, size, module, swatch, or carousel thumbnail uses OV25's bundled woven placeholder automatically; integrations should not provide their own placeholder URL. If neither supplied images nor product metadata contains any gallery image, the image carousel is not rendered.

The optional alt field is accepted as image metadata. OV25's current gallery and carousel components generate their own image labels when they render these inputs.

uniqueId

Disambiguates when multiple configurators share global containers (e.g. mobile drawer, toaster).

branding

type BrandingConfig = {
  logoURL?: string;
  mobileLogoURL?: string;
  cssString?: string;
  hideLogo?: boolean;
};

cssString – Custom CSS injected into configurator components. See Configurator Styling for CSS variables, class names, and data attributes.

hideLogo – Hide the OV25 / brand logo in the configurator chrome.

branding: {
  cssString: `
    .ov25-variant-control { background-color: red; }
    .ov25-dimensions-width, .ov25-dimensions-height { border: 2px dashed green; }
  `,
},

flags

type FlagsConfig = {
  hidePricing?: boolean;
  disableAddToCart?: boolean;
  disableBuyNow?: boolean;
  hideAr?: boolean;
  hideGestureHint?: boolean;
  deferThreeD?: boolean;
  showOptional?: boolean;
  forceMobile?: boolean;
  autoOpen?: boolean;
  currencySymbol?: string;
};
FlagTypeDescription
hidePricingbooleanHide price display
disableAddToCartbooleanHide the Add to Basket action while leaving Buy Now available when configured. Default false.
disableBuyNowbooleanHide the Buy Now action while leaving Add to Basket available when configured. Default false.
hideArbooleanHide AR features
hideGestureHintbooleanHide the animated drag indicator shown by the 3D viewer. Default false.
deferThreeDbooleanDefer 3D loading until the configurator is opened
showOptionalbooleanShow optional options
forceMobilebooleanForce mobile layout (e.g. for device frame testing)
autoOpenbooleanAuto-open configurator on load (non-inline only). Default false.
currencySymbolstringDisplay symbol replacing £ in iframe-formatted prices after normalization. Not FX conversion. Default £.

bed

Bed-specific configuration. Only relevant for bed iframe products.

type BedAllowNonePartsInput = {
  headboard: boolean;
  base: boolean;
  mattress: boolean;
};
 
type BedPartSizeFilterFlags = {
  headboard: boolean;
  base: boolean;
  mattress: boolean;
};
 
type BedEmbedConfig = {
  /** Allow-list of parts that may be set to "None". Omit (or set all true) to allow None on every part. */
  allowNone?: BedAllowNonePartsInput;
  /** When `true` for a part, variant UI hides selections whose `metadata.bedSize` ≠ iframe current size. */
  filterSelectionsByCurrentSize?: BedPartSizeFilterFlags;
};
bed: {
  allowNone: { headboard: true, base: false, mattress: false },
  filterSelectionsByCurrentSize: { headboard: false, base: true, mattress: true },
},

dining

Dining-specific configuration. Only relevant for dining configurator products (productLink: 'dining-configurator/<id>'); injectConfigurator delegates these links to the dining embed automatically (an injectDiningConfigurator export is also available).

type DiningEmbedConfig = {
  displayMode?: ResponsiveValue<'split' | 'full-page'>;
  displayOptions?: {
    /** Show in-scene dining attachment point buttons. Default true. */
    showAttachmentPoints?: boolean;
  };
  /** Optional hero images for the full-page style-choice screen. */
  styleImages?: { fullRange?: string; mixAndMatch?: string };
};

PatternExample
Single product'217', '58', '607'
Multi-product modular (snap2/… path)'snap2/4', 'snap2/126'
Range'range/126', 'range/85'
Bed configurator'bed-configurator/2'
Dining configurator'dining-configurator/3'

The snap2/ prefix is the URL convention for multi-product modular configurators; callback payloads still use mode: 'single' | 'multi', not this path string.


🔄 Multiple Configurators

Pass an array of configs. Each config must use distinct selectors (e.g. #gallery-1, #gallery-2). For standard configs, gallery and variants selectors must be unique across configs.

⚠️ Multi-product modular with replace: true on configure buttons: When multiple such configs use replace: true, only one configurator instance is active at a time. Clicking a different configure button switches to that product's configurator.

Multi-product modular, configure buttons only:

injectConfigurator([
  {
    apiKey: '15-...',
    productLink: 'snap2/126',
    selectors: { configureButton: { selector: '#ov25-fullscreen-button', replace: true } },
    callbacks: { addToBasket: () => {}, buyNow: () => {}, buySwatches: () => {} },
  },
  {
    apiKey: () => '15-...',
    productLink: () => 'snap2/292',
    selectors: { configureButton: { selector: '#test', replace: true } },
    callbacks: { addToBasket: () => {}, buyNow: () => {}, buySwatches: () => {} },
  },
]);

4 products with gallery, price, name:

injectConfigurator([
  { selectors: { gallery: '#gallery-1', price: { selector: '#price-1', replace: true }, name: { selector: '#name-1', replace: true } }, ... },
  { selectors: { gallery: '#gallery-2', price: { selector: '#price-2', replace: true }, name: { selector: '#name-2', replace: true } }, ... },
  // ...
]);

Inline variant controls per product – use configurator.displayMode: { desktop: 'inline', mobile: 'inline' }.

Ranges with variants – use productLink: 'range/126' with variants selector.


🌐 Global APIs

When the configurator is injected, these functions are exposed on window:

FunctionDescription
window.ov25OpenConfigurator(optionName?)Open configurator; optionally focus an option group (e.g. 'wood finishes')
window.ov25CloseConfigurator()Close configurator
window.ov25OpenSwatchBook()Open swatch book
window.ov25CloseSwatchBook()Close swatch book
window.ov25GenerateThumbnail()Capture the current 3D scene as an image. Returns Promise<string> resolving to a CDN URL of the screenshot. Rejects on timeout (10s) or iframe error.

Example:

<button onClick={() => window.ov25OpenConfigurator?.()}>Open configurator</button>
<button onClick={() => window.ov25OpenConfigurator?.('wood finishes')}>Open configurator (Wood finishes)</button>
<button onClick={() => window.ov25CloseConfigurator?.()}>Close configurator</button>
<button onClick={() => window.ov25OpenSwatchBook?.()}>Open swatches</button>
<button onClick={() => window.ov25CloseSwatchBook?.()}>Close swatches</button>
<button
  onClick={async () => {
    const url = await window.ov25GenerateThumbnail?.();
    console.log('Thumbnail URL:', url);
  }}
>
  Generate thumbnail
</button>

🕰️ Legacy Format

A flat config format is supported for backward compatibility. Use addToBasketFunction, buyNowFunction, buySwatchesFunction (or addSwatchesToCartFunction as alias), onChangeFunction, and flat selector/carousel/configurator fields instead of callbacks, selectors, carousel, configurator. The grouped format above is preferred.

Most flag/branding fields are also available at the top level of the legacy config (no flags / branding wrapper):

  • hidePricing, disableAddToCart, disableBuyNow, hideAr, deferThreeD, showOptional, forceMobile, autoOpen, currencySymbol
  • logoURL, mobileLogoURL, cssString, hideLogo
  • hideOptions (variant hide list)
  • bedAllowNone, bedFilterSelectionsByCurrentSize (bed iframe equivalents of bed.allowNone / bed.filterSelectionsByCurrentSize)
  • Selector aliases: galleryId, priceId, nameId, variantsId, swatchesId, configureButtonId (deprecated; use *Selector instead)
  • variantDisplayStyle / variantDisplayStyleMobile (deprecated aliases for variantDisplayMode / variantDisplayModeMobile)
  • useInlineVariantControls (deprecated; equivalent to configurator.displayMode.desktop = 'inline')

Built with ❤️ by the Orbital Vision team