@snowcone-app/canvas
v0.33.1
Published
Self-contained canvas editor component for e-commerce product customization
Maintainers
Readme
@snowcone-app/canvas
Building with an AI agent? This tarball is self-sufficient: start with
llms.txt(the agent index), copy the canonical example inexample/, and restyle via the generatedSTYLING_CONTRACT.md. The complete hosted docs are one fetch — https://developers.snowcone.app/llms-full.txt — grep them before assuming a field or behavior is undocumented.
Embeddable product personalization and design editing for e-commerce.
Two surfaces, one package:
Editor.*composable parts (@snowcone-app/canvas/embed) — live server-rendered product previews, layer-bound form fields, and smart add-to-cart, composed in your own layout. The turnkeySnowconeEditoris the same parts in our arrangement.SnowconeCanvas(@snowcone-app/canvas) — the standalone design editor: text, images, transforms, and effects across one or more print areas. The same editor that powers Edit and Remix on snowcone.app.
📖 Full documentation: developers.snowcone.app/canvas
Installation
npm install @snowcone-app/canvas
# or: pnpm add @snowcone-app/canvas / yarn add @snowcone-app/canvasreact and react-dom (>=18) are peer dependencies.
The canonical example — a personalization widget, no canvas
The fastest path to a working integration. A live product preview, one
personalized text layer, and a completeness-gated add-to-cart — the full
design editor never loads. This file ships in this package at
example/src/Widget.tsx (reading this in the
monorepo instead of the npm tarball? It lives at
packages/canvas/fixtures/no-canvas-widget/ there — example/ is assembled at
publish); copy it and change the design, the product ids, and the parts.
import React from 'react';
import { Editor, design, type EditorBuyPayload } from '@snowcone-app/canvas/embed';
const SHOP_KEY = import.meta.env.VITE_SNOWCONE_SHOP ?? 'demo-shop';
// The design is the single source of truth: it seeds the first render AND
// mints the typed layer tokens the TextField binds with.
const nameDrop = design({ name: 'Front', width: 1480, height: 2328 })
.background('https://dqMwU8Jct3.storage.snowcone.app/demo/demo-008.jpg', {
aspectRatio: 1480 / 2328,
name: 'art',
})
.text('YOUR NAME', { anchor: 'top', name: 'name' });
const product = {
productId: 'BEEB77',
mockupIds: ['FV1qjO'],
placements: ['Front'],
// BEEB77 has a COLOR axis (Frame) — a color product REQUIRES variantId, or
// a live render times out. Read real ids from the catalog with getProduct().
variantId: 'Pv1sLC',
};
export function PersonalizationWidget({
onAddToCart,
}: {
/** Your cart handler. NOTE the payload's `designState` is the EDITOR shape
* (flat `elements` + artboard metadata) — persist it as-is. It is NOT the
* server render shape (`artboards[].elements`) the session pushes;
* `payload.serverRequest` carries that one if you need it. */
onAddToCart?: (payload: EditorBuyPayload) => void;
} = {}): React.ReactElement {
return (
<Editor.Root shop={SHOP_KEY} product={product} design={nameDrop} grantUrl="/api/realtime/grant">
<div style={{ display: 'grid', gap: 12, maxWidth: 320 }}>
<Editor.Preview alt="Your personalized tee" fallback={<div aria-busy="true">Rendering…</div>} />
<Editor.TextField layer={nameDrop.layer('name')} placeholder="Your name" />
<Editor.Buy
onAddToCart={
onAddToCart ??
(({ productId, quantity, designState }) => {
// The host's one line: persist the designState (the
// re-renderable source of truth) with the cart line.
console.log('add-to-cart', { productId, quantity, designState });
})
}
>
Add to cart · $24
</Editor.Buy>
</div>
</Editor.Root>
);
}Don't forget the stylesheet (once, anywhere in your app):
import '@snowcone-app/canvas/style.css';grantUrl points at a small route on YOUR server that mints a render grant
(your keys never reach the browser). Framework variants (Next.js, Remix,
Express) are in the docs: https://developers.snowcone.app/primitives.
The parts
All from @snowcone-app/canvas/embed, all valid in any subset under one
Editor.Root — the host owns the layout:
| Part | What it carries |
|------|-----------------|
| Editor.Root | session, design state, error boundary — the one provider |
| Editor.Preview | live product mockup (×N under one Root is valid) |
| Editor.TextField | input bound to a design text layer by typed token |
| Editor.Buy | completeness gating + cart payload; submit is your callback |
| Editor.Canvas | the editable artboard (the one lazy-loaded heavy chunk) |
| Editor.Layers / Editor.Controls / Editor.History / Editor.Menu / Editor.Add | editor chrome as parts |
The turnkey default — the same parts in our arrangement:
import { SnowconeEditor } from '@snowcone-app/canvas/embed';
<SnowconeEditor shop={SHOP_KEY} product={product} design={d} grantUrl="/api/realtime/grant" onAddToCart={addLine} />Every part renders minimal DOM tagged data-sc-part and themes through
.sc-root CSS custom properties — the full generated reference is
STYLING_CONTRACT.md. Composition mistakes throw
structured EditorErrors (stable code + error.data) in dev AND prod,
contained by the Root's error boundary — the full generated code catalog is
ERROR_CONTRACT.md. Test with
@snowcone-app/canvas/testing: assertRenders proves a design's bindings
are alive; expectNoEditorErrors() makes contained errors fail your tests.
The standalone design editor
import { SnowconeCanvas } from '@snowcone-app/canvas';
import '@snowcone-app/canvas/style.css';
export default function Customizer() {
return (
<SnowconeCanvas
// One artboard per print area. The name is the export key.
artboards={[{ name: 'Front', width: 1200, height: 1200 }]}
// Optional: drop the shopper's art in to start from.
imageConfig={{ src: 'https://cdn.example.com/art.png', scaleMode: 'contain' }}
// Fires on every edit — persist this JSON to reload the design later.
onChange={(state) => saveDraft(state)}
/>
);
}That renders a working editor out of the box (toolbar, layers, text/image/effects).
Use the kit prop to switch presets ('pro-studio' default, 'compact-customizer',
'embed-only') or pass layoutConfig to go canvas-only.
Multiple print areas (placements)
<SnowconeCanvas
artboards={[
{ name: 'Front', width: 1200, height: 1200 },
{ name: 'Back', width: 1200, height: 1200 },
// clipShape masks content to a shape (e.g. a circular badge).
{ name: 'Pocket', width: 400, height: 400, clipShape: 'circle' },
]}
activeArtboard="Front"
onArtboardChange={(name) => console.log('now editing', name)}
/>Save and restore a design
// SAVE — onChange hands you a CanvasState (elements + artboards). Persist the
// JSON. Do NOT save a flattened PNG as the state — you'll lose the layers.
const [state, setState] = useState<CanvasState | null>(null);
<SnowconeCanvas
artboards={artboards}
onChange={setState}
// RELOAD — feed the saved elements back in to restore an editable design.
initialElements={savedState?.elements}
/>Print-ready exports
<SnowconeCanvas
artboards={[{ name: 'Front', width: 1200, height: 1200 }]}
exportConfig={{
autoExportConfig: { enabled: true, debounceMs: 200 },
format: 'blob', // 'blob' for uploads, 'dataUrl' for inline previews
exportAll: true, // export every artboard, not just the active one
scale: 2, // resolution multiplier (capped by maxSize)
}}
// Keyed by artboard name: { Front: Blob, Back: Blob }
onExport={(exports) => uploadToBackend(exports)}
/>Exports always produce transparent PNGs/WebP — the preview background is never baked in.
API at a glance
<SnowconeCanvas /> is the single entry point. Key props:
| Prop | Type | Purpose |
|------|------|---------|
| artboards | ArtboardConfig[] | One entry per print area; name is the export key |
| initialElements | AnyElementConfig[] | Restore a previously saved design |
| imageConfig | ImageConfig | Initial image: src, alignment, scale, scaleMode |
| exportConfig | ExportConfig | Auto-export, format, scale, max size |
| layoutConfig | LayoutConfig | Sizing, border radius, toggle toolbar/layers |
| kit | 'pro-studio' \| 'compact-customizer' \| 'embed-only' \| KitDefinition | Editor preset |
| onChange | (state: CanvasState) => void | Persist design state on edit |
| onExport | (exports: Record<string, string \| Blob>) => void | Receive rendered placements |
| onArtboardChange | (name: string) => void | Active placement changed |
Need lower-level control? @snowcone-app/canvas also exports React hooks
(useEditor, useArtboards, useLayers, useExport) and state helpers
(serializeState, deserializeState). See the docs for the full reference.
All exports are fully typed — TypeScript definitions ship with the package.
Requirements
- React 18 or 19
- A bundler that handles ESM and CSS imports (Vite, Next.js, Remix, etc.)
- Modern browsers: Chrome 69+, Firefox 105+, Safari 16.4+, Edge 79+
Links
License
MIT
