Full Customizer (@chamevo/customizer)
The fastest way to add a product customizer to any site. Install one package, drop in a web component, and you have a working customizer with a complete UI.
When to use: You want a full-featured customizer with minimal code. The built-in UI handles text editing, image uploads, layer management, color picking, and more.
Installation
npm install @chamevo/customizer
Or via CDN:
<script src="https://cdn.jsdelivr.net/npm/@chamevo/customizer/dist/chamevo.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@chamevo/customizer/dist/chamevo.min.css" />
Two Ways to Initialize
Option A: createCustomizer() Factory
import { createCustomizer } from '@chamevo/customizer';
const customizer = createCustomizer('#container', {
stageWidth: 900,
stageHeight: 600,
});
customizer.addEventListener('cvReady', async (e) => {
await customizer.loadProduct(myProduct);
});
createCustomizer(target, options) registers all web components, creates a <cv-customizer> element, and appends it to the target container.
In addition to all ChamevoOptions, the factory accepts one extra parameter:
| Option | Type | Description |
|---|---|---|
resourcesUrl | string | Absolute URL to the directory containing the Stencil lazy-chunk files. Required when the bundle is served from a different origin or path than the page (e.g. WordPress admin, CDN, subdirectory deploy). |
createCustomizer('#container', {
stageWidth: 900,
resourcesUrl: 'https://cdn.example.com/assets/chamevo/js/',
});
Without resourcesUrl, Stencil resolves lazy chunks relative to document.baseURI. If your JS assets are hosted at a different path this will 404 — set resourcesUrl to the directory containing cv-customizer.entry.js to fix it.
resourcesUrl is only read on the first createCustomizer() call per page. If you need to change it, reload the page.
Option B: Declarative HTML
<cv-customizer
id="customizer"
options='{"stageWidth": 900, "stageHeight": 600}'
color-scheme="light"
ui-style="default"
>
</cv-customizer>
<script type="module">
import { defineCustomElements } from '@chamevo/customizer';
defineCustomElements();
const customizer = document.getElementById('customizer');
customizer.addEventListener('cvReady', async (e) => {
await customizer.loadProduct(myProduct);
});
</script>
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
options | options | string | Partial<ChamevoOptions> | '{}' | Customizer configuration. Pass as JSON string in HTML or object in JS. |
colorScheme | color-scheme | 'light' | 'dark' | 'light' | Color theme. |
uiStyle | ui-style | 'default' | 'rounded' | 'sharp' | 'default' | Shape preset (border radius, shadows). |
moduleDisplay | module-display | 'sidebar' | 'dialog' | 'sidebar' | How module panels are displayed. |
shadow | shadow | 'none' | 'sm' | 'md' | 'lg' | 'none' | Box shadow preset on the customizer container. |
proxyFileServer | proxy-file-server | string | '' | Proxy URL for cross-origin images and fonts. |
Methods
All methods return Promises and are called on the <cv-customizer> element:
const customizer = document.querySelector('cv-customizer');
| Method | Returns | Description |
|---|---|---|
loadProduct(product) | Promise<void> | Load a CVProduct or legacy CVView[]. |
selectView(index) | Promise<void> | Switch to a view by index. |
getProduct() | Promise<CVProduct | null> | Get the current product with all customizations. |
getOrder() | Promise<object> | Get product + used fonts + used colors. |
getElements(viewIndex?, type?) | Promise<object[]> | Get serialized elements. type: 'all', 'text', 'image'. |
undo() | Promise<void> | Undo last action. |
redo() | Promise<void> | Redo last undone action. |
reset() | Promise<void> | Clear all user-added elements in all views. |
setProducts(catalog) | Promise<void> | Set the product catalog for the products module. |
setGraphics(catalog) | Promise<void> | Set the graphics catalog for the graphics module. |
registerModule(moduleId, target) | Promise<() => void> | Mount a built-in or custom module into an external DOM element. Returns a cleanup function. |
Events
Events are dispatched as CustomEvent on the <cv-customizer> element:
customizer.addEventListener('cvReady', (e) => {
const { chamevo, canvas } = e.detail;
});
| Event | e.detail | Description |
|---|---|---|
cvReady | { chamevo: ChamevoJS, canvas: ChamevoCanvas } | Customizer fully initialized. Access core instances here. |
cvProductCreate | { product: CVProduct } | Product loaded. |
cvProductChange | { product: CVProduct } | Product data changed. |
cvViewSelect | { view: CVView, index: number } | Active view changed. |
cvElementSelect | { element: object | null } | Element selected or deselected. |
cvPriceChange | { price: number, singleProductPrice: number, pricingRulesPrice: number } | Price updated. |
Layout & Slots
cv-customizer uses named slots for layout customization:
<cv-customizer options='{"stageWidth": 900}'>
<div slot="toolbar-top">Custom top toolbar</div>
<div slot="sidebar">Custom sidebar content</div>
<div slot="panel">Custom right panel</div>
<div slot="toolbar-bottom">Custom bottom toolbar</div>
</cv-customizer>
Responsive Behavior
The customizer automatically adapts to its container size:
| Container Width | Layout |
|---|---|
| < 580px (small) | Bottom tab bar + dialog panels |
| 580-1024px (medium) | Dialog panels |
| > 1024px (large) | Sidebar panels |
Layout size is exposed via the data-layout-size attribute (sm, md, lg) for CSS overrides.
Theming
Override CSS custom properties on the element or any ancestor:
cv-customizer {
--cv-primary: #e11d48;
--cv-radius: 0.5rem;
--cv-panel-width: 300px;
}
See the Theming Guide for the full token reference.
Custom Modules
You can add custom web components as mainbar modules alongside the built-in ones. Define a CustomModule descriptor in the mainBarModules array:
const customizer = document.querySelector('cv-customizer');
customizer.options = {
mainBarModules: [
'text',
'images',
'designs',
{
id: 'quote',
element: 'my-quote-picker',
icon: 'sparkle',
label: 'Get Quote',
},
],
};
CustomModule Interface
| Property | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique module ID. Used in disabledModules and initialActiveModule. |
element | string | Yes | Custom element tag name (e.g. 'my-quote-picker'). Must be registered in the DOM. |
icon | string | No | Phosphor icon name for the mainbar. Default: 'sparkle'. |
label | string | No | Display label for the mainbar and panel header. Falls back to id. |
The custom element will receive a chamevo property set to the ChamevoJS instance, giving it full access to the customizer API.
Distributed Modules
Use registerModule() to mount any module (built-in or custom) into an external DOM element outside the customizer:
const customizer = document.querySelector('cv-customizer');
customizer.addEventListener('cvReady', async () => {
// Mount the text module into a sidebar div
const cleanup = await customizer.registerModule('text', '#my-sidebar');
// Later, unmount the module
cleanup();
});
This is useful for placing modules in custom layouts, separate panels, or other parts of your page.
Framework Integration
React
import { useEffect, useRef } from 'react';
import { defineCustomElements } from '@chamevo/customizer';
import type { CVProduct } from '@chamevo/types';
defineCustomElements();
export function ProductCustomizer({ product }: { product: CVProduct }) {
const ref = useRef<HTMLCvCustomizerElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const handleReady = async () => {
await el.loadProduct(product);
};
el.addEventListener('cvReady', handleReady);
return () => el.removeEventListener('cvReady', handleReady);
}, [product]);
return (
<cv-customizer
ref={ref}
options={JSON.stringify({ stageWidth: 900, stageHeight: 600 })}
/>
);
}
Vue
<template>
<cv-customizer
ref="customizer"
:options="JSON.stringify(options)"
@cvReady="onReady"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { defineCustomElements } from '@chamevo/customizer';
import type { CVProduct } from '@chamevo/types';
defineCustomElements();
const props = defineProps<{ product: CVProduct }>();
const options = { stageWidth: 900, stageHeight: 600 };
const customizer = ref<HTMLCvCustomizerElement>();
const onReady = async () => {
await customizer.value?.loadProduct(props.product);
};
</script>
Angular
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { defineCustomElements } from '@chamevo/customizer';
defineCustomElements();
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
// ...
})
export class AppModule {}
<!-- customizer.component.html -->
<cv-customizer
#customizer
[attr.options]="optionsJson"
(cvReady)="onReady($event)"
></cv-customizer>
// customizer.component.ts
import { Component, ViewChild, ElementRef } from '@angular/core';
@Component({ /* ... */ })
export class CustomizerComponent {
@ViewChild('customizer') customizerRef: ElementRef;
optionsJson = JSON.stringify({ stageWidth: 900, stageHeight: 600 });
async onReady(e: CustomEvent) {
const { chamevo } = e.detail;
await this.customizerRef.nativeElement.loadProduct(this.product);
}
}
Keyboard Controls
When an element is selected on the canvas:
| Key | Action |
|---|---|
| Arrow keys | Move element by 1px |
| Shift + Arrow | Move element by 10px |
| Delete / Backspace | Remove selected element |
Keyboard input is ignored when focus is inside an input field.
Next Steps
- ChamevoJS API — Build your own UI with the headless API
- Configuration — Full options reference
- Theming — Customize colors, radius, and shadows