Skip to main content

Your First Product

Build a working t-shirt customizer in under 10 minutes. By the end of this tutorial you'll have a two-sided product with editable text and image upload.

Prerequisites

  • Node.js >= 18
  • A package manager (npm, pnpm, or yarn)

1. Set Up the Project

mkdir my-customizer && cd my-customizer
npm init -y
npm install @chamevo/customizer

Create an index.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>T-Shirt Customizer</title>
<style>
body { margin: 0; font-family: sans-serif; }
#customizer-container { width: 100%; height: 100vh; }
</style>
</head>
<body>
<div id="customizer-container"></div>
<script type="module" src="main.js"></script>
</body>
</html>

2. Define the Product

Create main.js:

import { createCustomizer } from '@chamevo/customizer';

// Define a t-shirt with front and back views
const tshirtProduct = {
title: 'Classic T-Shirt',
views: [
{
title: 'Front',
thumbnail: 'https://your-domain.com/images/tshirt-front-thumb.jpg',
elements: [
{
type: 'image',
source: 'https://your-domain.com/images/tshirt-front.png',
title: 'T-Shirt Front',
parameters: {
left: 450,
top: 300,
draggable: false,
removable: false,
resizable: false,
zChangeable: false,
},
},
],
options: {
stageWidth: 900,
stageHeight: 600,
},
},
{
title: 'Back',
thumbnail: 'https://your-domain.com/images/tshirt-back-thumb.jpg',
elements: [
{
type: 'image',
source: 'https://your-domain.com/images/tshirt-back.png',
title: 'T-Shirt Back',
parameters: {
left: 450,
top: 300,
draggable: false,
removable: false,
resizable: false,
zChangeable: false,
},
},
],
},
],
};

The product data model is simple:

  • A product has one or more views (front, back, sleeve, etc.)
  • Each view has elements (images, text, SVGs)
  • Each element has a type, source, title, and parameters

3. Initialize the Customizer

Add this below the product definition in main.js:

// Create the customizer and load the product
const customizer = createCustomizer('#customizer-container', {
stageWidth: 900,
stageHeight: 600,
fonts: [
{ name: 'Arial' },
{ name: 'Lobster', url: 'google' },
{ name: 'Playfair Display', url: 'google' },
],
});

customizer.addEventListener('cvReady', async (e) => {
const { chamevo, canvas } = e.detail;

// Load the t-shirt product
await customizer.loadProduct(tshirtProduct);

console.log('Customizer ready!');
});

createCustomizer() does three things:

  1. Registers all cv-* web components
  2. Creates a <cv-customizer> element with your options
  3. Appends it to the target container

4. Add Editable Text

Let's pre-place an editable text element on the front view. Add it to the front view's elements array:

// Add this as the second element in the Front view
{
type: 'text',
source: 'Your Text Here',
title: 'Custom Text',
parameters: {
left: 450,
top: 350,
fill: '#333333',
fontSize: 32,
fontFamily: 'Lobster',
draggable: true,
resizable: true,
rotatable: true,
removable: true,
editable: true,
colors: '#000000', // enables color picker
},
}

Users can now double-click the text to edit it, drag it around, resize it, and change its color.

5. Listen to Events

Track what the user is doing:

customizer.addEventListener('cvReady', async (e) => {
const { chamevo } = e.detail;

await customizer.loadProduct(tshirtProduct);

// Track element changes
chamevo.on('elementModify', ({ element, changes }) => {
console.log(`${element.title} modified:`, changes);
});

// Track price changes
chamevo.on('priceChange', ({ price, singleProductPrice }) => {
console.log(`Price: $${price.toFixed(2)}`);
});

// Track view switches
chamevo.on('viewSelect', ({ view, index }) => {
console.log(`Switched to ${view.title} (view ${index})`);
});
});

6. Get the Customized Product

When the user is done, retrieve their customization:

// Add a "Save" button handler
document.getElementById('save-btn')?.addEventListener('click', async () => {
const product = await customizer.getProduct();
console.log('Customized product:', product);

// Send to your server
await fetch('/api/save-design', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(product),
});
});

getProduct() returns a CVProduct with all views and their current element states serialized. You can later reload this product to restore the user's design.

7. Add Print Areas (Optional)

Print areas define the printable region on each view. Elements are constrained within print areas:

// Add printAreas to the Front view
{
title: 'Front',
elements: [ /* ... */ ],
printAreas: [
{
printingBox: {
left: 310,
top: 140,
width: 280,
height: 320,
},
},
],
}

Complete Example

import { createCustomizer } from '@chamevo/customizer';

const tshirtProduct = {
title: 'Classic T-Shirt',
views: [
{
title: 'Front',
thumbnail: 'https://your-domain.com/images/tshirt-front-thumb.jpg',
elements: [
{
type: 'image',
source: 'https://your-domain.com/images/tshirt-front.png',
title: 'T-Shirt Front',
parameters: {
left: 450, top: 300,
draggable: false, removable: false,
resizable: false, zChangeable: false,
},
},
{
type: 'text',
source: 'Your Text Here',
title: 'Custom Text',
parameters: {
left: 450, top: 350,
fill: '#333333', fontSize: 32, fontFamily: 'Lobster',
draggable: true, resizable: true,
rotatable: true, removable: true, editable: true,
colors: '#000000',
},
},
],
printAreas: [
{
printingBox: { left: 310, top: 140, width: 280, height: 320 },
},
],
options: { stageWidth: 900, stageHeight: 600 },
},
{
title: 'Back',
thumbnail: 'https://your-domain.com/images/tshirt-back-thumb.jpg',
elements: [
{
type: 'image',
source: 'https://your-domain.com/images/tshirt-back.png',
title: 'T-Shirt Back',
parameters: {
left: 450, top: 300,
draggable: false, removable: false,
resizable: false, zChangeable: false,
},
},
],
},
],
};

const customizer = createCustomizer('#customizer-container', {
stageWidth: 900,
stageHeight: 600,
fonts: [
{ name: 'Arial' },
{ name: 'Lobster', url: 'google' },
{ name: 'Playfair Display', url: 'google' },
],
});

customizer.addEventListener('cvReady', async (e) => {
const { chamevo } = e.detail;
await customizer.loadProduct(tshirtProduct);

chamevo.on('priceChange', ({ price }) => {
document.getElementById('price').textContent = `$${price.toFixed(2)}`;
});
});

Next Steps