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.


📦 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', 'range/126')
selectorsSelectorsConfigDOM targets for gallery, price, name, variants, swatches, configureButton
callbacksCallbacksConfigaddToBasket, buyNow, buySwatches (required); onChange (optional)
type StringOrFunction = string | (() => string);
 
interface SelectorsConfig {
  gallery?: ElementSelector;
  price?: ElementSelector;
  name?: ElementSelector;
  variants?: ElementSelector;
  swatches?: ElementSelector;
  configureButton?: ElementSelector;
}

Full Config Type

interface InjectConfiguratorOptions {
  apiKey: StringOrFunction;
  productLink: StringOrFunction;
  configurationUuid?: StringOrFunction;
  images?: string[];
  uniqueId?: string;
  selectors: SelectorsConfig;
  carousel?: CarouselConfig;
  configurator?: ConfiguratorConfig;
  callbacks: CallbacksConfig;
  branding?: BrandingConfig;
  flags?: FlagsConfig;
}
 
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 productsIgnored for Snap2 (gallery is inside modal)
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 configuratorSnap2Required for Snap2; optional for standard

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

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 },
},

Snap2 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.

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' },

🎛️ Configurator

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

type ResponsiveValue<T> = { desktop: T; mobile?: T };
 
type ConfiguratorDisplayMode = 'inline' | 'sheet' | 'drawer';
type VariantDisplayMode = 'wizard' | 'list' | 'tabs' | 'accordion' | 'tree';
 
type ConfiguratorConfig = {
  displayMode: ResponsiveValue<ConfiguratorDisplayMode>;
  triggerStyle?: ResponsiveValue<'single-button' | 'split-buttons'>;
  variants?: {
    displayMode: ResponsiveValue<VariantDisplayMode>;
    useSimpleVariantsSelector?: boolean;
  };
};

Display Mode

DesktopMobileDescription
'inline''inline'Variants shown inline on the page
'sheet''drawer'Full-screen sheet (desktop), bottom drawer (mobile)
'sheet''inline'Sheet on desktop, inline on mobile

Defaults: desktop: 'sheet', mobile: 'drawer' (when desktop is sheet) or 'inline' (when desktop is inline).

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 sections (desktop only; mobile falls back to tree)
'wizard'Step-by-step wizard

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

useSimpleVariantsSelector

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

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,
  },
},

📞 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 to basket. When invoked by the UI, always receives OnChangePayload; skus and price inside may be null until those messages have been received.
  • buyNow – Checkout immediately. When invoked by the UI, always receives OnChangePayload; skus and price inside may be null until those messages have been received.
  • buySwatches – Purchase selected swatches. Receives Swatch[] and SwatchRulesData.
  • onChange – Optional. Called when price or SKU changes. Receives OnChangePayload; skus and price are each null until that message type has been received at least once.

📋 Payload Types (skus and price)

The callbacks addToBasket, buyNow, and onChange receive an OnChangePayload. This object contains two keys: skus and price. Each key is null until the configurator iframe has sent that message type at least once. After the first CURRENT_SKU message, skus is populated; after the first CURRENT_PRICE message, price is populated.

Always null-check before using:

onChange: (payload) => {
  if (payload.skus) {
    const sku = payload.skus.skuString;
    const colorSku = payload.skus.skuMap?.['Color'];
  }
  if (payload.price) {
    const displayPrice = payload.price.formattedPrice;
    const hasDiscount = payload.price.discount.percentage > 0;
  }
},

OnChangePayload

interface OnChangePayload {
  skus: OnChangeSkuPayload | null;
  price: OnChangePricePayload | null;
}
KeyTypeWhen populated
skusOnChangeSkuPayload | nullAfter first CURRENT_SKU message from configurator
priceOnChangePricePayload | nullAfter first CURRENT_PRICE message from configurator

OnChangeSkuPayload (skus)

When payload.skus is not null, it contains the current SKU data for the configured product:

interface OnChangeSkuPayload {
  skuString: string;
  skuMap?: OptionSkuMap;
}
FieldTypeDescription
skuStringstringFull concatenated SKU string for the current configuration (e.g. 'RED-001-M-001'). Use this for cart/checkout.
skuMapOptionSkuMap | undefinedPer-option SKU values. Keys are option names; values are selected SKU codes.
type OptionSkuMap = Record<string, string>;
// Keys: option names (e.g. 'Color', 'Size'); reserved keys: 'Range', 'Product', 'Ranges', 'Products'.
// Example: { Color: 'RED-001', Size: 'M-001', Range: 'RANGE-123' }

OnChangePricePayload (price)

When payload.price is not null, it contains the current price data:

interface OnChangePricePayload {
  totalPrice: number;
  subtotal: number;
  formattedPrice: string;
  formattedSubtotal: string;
  discount: {
    amount: number;
    formattedAmount: string;
    percentage: number;
  };
}
FieldTypeDescription
totalPricenumberFinal price after discount, in minor units (e.g. pence, cents).
subtotalnumberSubtotal before discount, in minor units.
formattedPricestringDisplay string for final price (e.g. '£1,200.00').
formattedSubtotalstringDisplay string for subtotal (e.g. '£1,500.00').
discount.amountnumberDiscount amount in minor units.
discount.formattedAmountstringDisplay string for discount (e.g. '£300.00').
discount.percentagenumberDiscount percentage (e.g. 20 for 20%).

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

callbacks: {
  addToBasket: (payload?: OnChangePayload) =>
    alert(`Checkout: ${payload?.price?.formattedPrice ?? '—'} / ${payload?.skus?.skuString ?? '—'}`),
  buyNow: (payload?: OnChangePayload) =>
    alert(`Buy now: ${payload?.price?.formattedPrice ?? '—'} / ${payload?.skus?.skuString ?? '—'}`),
  buySwatches: () => alert('Add swatches to cart'),
  onChange: (payload: OnChangePayload) => {
    if (payload.skus) console.log('SKU:', payload.skus.skuString, payload.skus.skuMap);
    if (payload.price) console.log('Price:', payload.price.formattedPrice, payload.price.discount);
  },
},

⚙️ Optional Fields

configurationUuid

Snap2 saved configuration. Restores a previously saved config.

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

images

Override product images (e.g. for galleries with custom image sets).

images: Array.from({ length: imageCount }, (_, i) => `https://picsum.photos/600/600?random=${i + 1}`),

uniqueId

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

branding

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

cssString – Custom CSS injected into configurator components:

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

flags

type FlagsConfig = {
  hidePricing?: boolean;
  hideAr?: boolean;
  deferThreeD?: boolean;
  showOptional?: boolean;
  forceMobile?: boolean;
  autoOpen?: boolean;
};
FlagTypeDescription
hidePricingbooleanHide price display
hideArbooleanHide AR features
deferThreeDbooleanDefer 3D loading
showOptionalbooleanShow optional options
forceMobilebooleanForce mobile layout (e.g. for device frame testing)
autoOpenbooleanAuto-open configurator on load (non-inline only). Default false.

PatternExample
Single product'217', '58', '607'
Snap2'snap2/4', 'snap2/126'
Range'range/126', 'range/85'

🔄 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.

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

Snap2, 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

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>

🕰️ 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.


Built with ❤️ by the Orbital Vision team