@customos/configurator
v0.6.0
Published
Embeddable product configurator SDK for Custom OS — render composed product images and manage part/material/color selection state.
Readme
@customos/configurator
Embeddable product configurator SDK for Custom OS. Render composed product images and manage part/material/color selection state.
Install
pnpm add @customos/configurator
# or
npm install @customos/configuratorReact hooks require React 18+ as a peer dependency.
Quick start
Vanilla JS (no React)
import { createClient, buildComposeUrl } from "@customos/configurator";
const client = createClient({
apiKey: "ck_live_your_api_key",
baseUrl: "https://api.customos.platforme.com",
});
// Build the compose URL
const params = buildComposeUrl({
brand: "your-brand",
model: "your-product",
frame: "side-0",
selections: [
{ part: "sole", material: "leather", color: "black" },
{ part: "lining", material: "canvas", color: "white" },
],
width: 1000,
height: 1000,
});
// Fetch the composed image
const blob = await client.fetcher(`v1/compose?${params}`, {
signal: AbortSignal.timeout(10_000),
});
const url = URL.createObjectURL(blob);
document.getElementById("product-img").src = url;React
import { createClient } from "@customos/configurator";
import { useConfigurator, useComposePreview } from "@customos/configurator/react";
const client = createClient({ apiKey: "ck_live_your_api_key" });
function ProductConfigurator({ model }) {
const {
activeFrame,
setActiveFrame,
selections,
selectMaterial,
selectColor,
composeSelections,
} = useConfigurator(model);
const { imageUrl, loading, error } = useComposePreview({
brand: "your-brand",
model: model.code,
frame: activeFrame,
selections: composeSelections,
width: 800,
height: 800,
fetcher: client.fetcher,
});
return (
<div>
{loading && <p>Loading...</p>}
{imageUrl && <img src={imageUrl} alt="Product preview" />}
{error && <p>Error: {error.message}</p>}
{model.parts.map((part) => (
<div key={part.code}>
<h3>{part.code}</h3>
{part.materials.map((mat) => (
<button
key={mat.code}
onClick={() => selectMaterial(part.code, mat.code)}
>
{mat.code}
</button>
))}
</div>
))}
</div>
);
}Personalization (text overlays)
Add customer-typed text (initials, names, monograms) that renders directly onto the composed product image. The compose engine handles positioning, font size, rotation, and blending automatically based on admin-configured settings.
Vanilla JS
import { createClient, buildComposeUrl } from "@customos/configurator";
import type { TextSelection } from "@customos/configurator";
const client = createClient({ apiKey: "ck_live_your_api_key" });
const params = buildComposeUrl({
brand: "your-brand",
model: "your-product",
frame: "side-0",
selections: [
{ part: "body", material: "leather", color: "black" },
],
texts: [
{
bindingId: "a1b2c3d4e5f6...", // 32-char hex binding ID from the model
fontCode: "helvetica",
colorCode: "gold",
text: "A.M.",
},
],
width: 1000,
height: 1000,
});
const blob = await client.fetcher(`v1/compose?${params}`, {
signal: AbortSignal.timeout(10_000),
});Each TextSelection becomes a t=<bindingId>:<fontCode>:<colorCode>:<text> query parameter. Empty text entries are automatically skipped.
React
Pass texts to useComposePreview alongside your material/color selections. The image updates live as the user types (debounced).
import { useState } from "react";
import { createClient } from "@customos/configurator";
import type { TextSelection } from "@customos/configurator";
import { useConfigurator, useComposePreview } from "@customos/configurator/react";
const client = createClient({ apiKey: "ck_live_your_api_key" });
function ProductConfigurator({ model }) {
const { activeFrame, composeSelections, selectMaterial } = useConfigurator(model);
const [texts, setTexts] = useState<TextSelection[]>([]);
const { imageUrl, loading } = useComposePreview({
brand: "your-brand",
model: model.code,
frame: activeFrame,
selections: composeSelections,
texts,
width: 800,
height: 800,
fetcher: client.fetcher,
});
function updateText(bindingId: string, value: string) {
setTexts((prev) => {
const existing = prev.find((t) => t.bindingId === bindingId);
if (existing) {
return prev.map((t) =>
t.bindingId === bindingId ? { ...t, text: value } : t
);
}
return [
...prev,
{ bindingId, fontCode: "helvetica", colorCode: "gold", text: value },
];
});
}
return (
<div>
{imageUrl && <img src={imageUrl} alt="Product preview" />}
{/* Text input for each personalization slot */}
<input
type="text"
placeholder="Type your initials..."
onChange={(e) => updateText("a1b2c3d4e5f6...", e.target.value)}
/>
</div>
);
}How it works
- A model can have multiple personalization slots (bindings), each keyed by a unique 32-char hex
bindingId - Each slot is independent — a model might have "Monogram" on the flap and "Name" on the strap
- The backend silently skips any binding that isn't placed on the active frame, so you don't need to filter by frame
- Fonts come in two kinds: TrueType (
.ttf/.otf, works with any color) and bitmap (.fnt, only renders for specific font+color pairs that have uploaded variants)
TextSelection
| Field | Type | Description |
|---|---|---|
| bindingId | string | 32-char hex ID of the personalization binding |
| fontCode | string | Font code to render with |
| colorCode | string | Text color code |
| text | string | The customer's text (initials, name, etc.) |
Turntable / 360 rotation
import { useRotation } from "@customos/configurator/react";
function Turntable({ brand, model, selections, fetcher }) {
const { imageUrl, loading, loadedCount, totalCount, bind } = useRotation({
brand,
model: model.code,
frames: model.frames,
selections,
width: 800,
height: 800,
enabled: true,
fetcher,
});
return (
<div {...bind} style={{ cursor: "grab", touchAction: "none" }}>
{loading && <p>Loading {loadedCount}/{totalCount}...</p>}
{imageUrl && <img src={imageUrl} alt="Rotation" draggable={false} />}
</div>
);
}Entry points
| Import path | Contains | React required? |
|---|---|---|
| @customos/configurator | Core logic + API client | No |
| @customos/configurator/react | React hooks + re-exports core | Yes (>=18) |
API
createClient(options)
Creates an authenticated client for the Custom OS API.
const client = createClient({
apiKey: "ck_live_...", // Required
baseUrl: "https://api...", // Optional, defaults to production
});
client.fetcher(url, { signal }); // ComposeFetcherCore utilities
| Function | Description |
|---|---|
| buildComposeUrl(input) | Build /v1/compose query params (pure, no fetch) |
| autoSelect(part, frameCode) | Pick the best default material/color for a part at a frame |
| validSelection(part, mat, col) | Check if a material/color pair exists on a part |
| missingAtFrame(model, frame, selections) | Find selections missing assets at a frame |
| describeComposeError(error) | Map HTTP status to user-facing error description |
| sortFramesByCode(frames) | Natural-sort frames for rotation order |
React hooks
| Hook | Description |
|---|---|
| useConfigurator(model) | State machine for frame + part/material/color selection |
| useComposePreview(input) | Debounced compose image fetcher with object-URL lifecycle |
| useRotation(input) | Preload all frames + pointer-drag turntable interaction |
