# Element
URL: https://polotno.com/docs/element
**Element** represents a graphical object on the page or inside a group. An element can have one of these types:
* **text**
* **image**
* **svg**
* **line**
* **figure**
* **video**
* **table**
* **group**
```ts
const element = store.activePage.addElement({
type: 'text',
x: 50,
y: 50,
fill: 'black',
text: 'hello',
});
// logs 50
console.log(element.x);
// set new position
element.set({ x: 100 });
```
## Basic actions [#basic-actions]
### Read properties [#read-properties]
At any time, you can access any property of an element. See documentation for every element type to view all available properties.
```ts
const element = store.selectedElements[0];
// logs type of element
console.log(element.type);
// logs id of element
console.log(element.id);
```
### element.set(attrs) [#elementsetattrs]
Set new attributes to the element.
```ts
text.set({ x: text.x + 10, text: 'new text' });
```
### Custom properties [#custom-properties]
You can't write any arbitrary property directly on an element. If you want to store additional data, use the `custom` attribute.
```ts
// 🛑 this line will not work, because elements have no attribute `userId`
element.set({ userId: '12' });
// 🟢 you can write such data into "custom" attribute
element.set({ custom: { userId: '12' } });
// read custom attribute
console.log(element.custom?.userId);
```
### element.moveUp() [#elementmoveup]
Move element up on z-index.
```ts
text.moveUp();
```
### element.moveDown() [#elementmovedown]
Move element down on z-index.
```ts
text.moveDown();
```
### element.moveTop() [#elementmovetop]
Move element to the top of the page or a group.
```ts
text.moveTop();
```
### element.moveBottom() [#elementmovebottom]
Move element to the bottom of the page or a group.
```ts
text.moveBottom();
```
### Locking [#locking]
Use `draggable`, `contentEditable`, `styleEditable`, `removable` and `resizable` attributes to lock element editing.
```ts
// lock the object
element.set({
// can element be moved and rotated
draggable: false,
// can we change content of element?
contentEditable: false,
// can we change style of element?
styleEditable: false,
// can we resize element?
resizable: false,
// can we remove an element?
removable: false,
});
console.log(element.locked); // true
// unlock it
element.set({
// can element be moved and rotated
draggable: true,
// can we change content of element?
contentEditable: true,
// can we change style of element?
styleEditable: true,
// can we resize element?
resizable: true,
removable: true,
});
```
See [Element Locking](/docs/element-locking) for comprehensive documentation on locking properties, use cases, and custom UI examples.
## Text element [#text-element]
Text elements allow you to create text on the canvas.
Here is the example of default properties:
```ts
page.addElement({
type: 'text',
x: 0,
y: 0,
rotation: 0,
locked: false, // deprecated
blurEnabled: false,
blurRadius: 10,
brightnessEnabled: false,
brightness: 0,
shadowEnabled: false,
shadowBlur: 5,
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowColor: 'black',
shadowOpacity: 1,
name: '', // name of element, can be used to find element in the store
text: 'Hey, polotno',
// placeholder is working similar to input placeholder
// it will be rendered (dimmed) if no text is defined
// and we will use it in input element too
// useful for template canvas, where users will need to replace text elements
// important (!) placeholders are visible in export results too,
// so remove or fill them before exporting
placeholder: '',
fontSize: 14,
fontFamily: 'Roboto',
fontStyle: 'normal', // can be normal or italic
fontWeight: 'normal', // numeric weight as string too, e.g. '400', '700'; 'bold' works as alias
textDecoration: '',
fill: 'black',
align: 'center',
width: 0,
strokeWidth: 0,
stroke: 'black',
lineHeight: 1,
letterSpacing: 0, // % from font size,
backgroundEnabled: false,
backgroundColor: '#7ED321',
backgroundOpacity: 1,
backgroundCornerRadius: 0.5, // % from half line height
backgroundPadding: 0.5, //% from half line height
// can user select element?
// if false, element will be "invisible" for user clicks
selectable: true,
// use for absolute positing of element
alwaysOnTop: false,
// also we can hide some elements from the export
showInExport: true,
// can element be moved and rotated
draggable: true,
// can we change content of element?
contentEditable: true,
// can we remove element from UI with button or keyboard?
removable: true,
// can we resize element?
resizable: true,
// can we change style of element?
styleEditable: true,
});
```
### text.toggleEditMode() [#texttoggleeditmode]
Enable edit mode for the text. It puts the cursor inside the text and a user can do regular text editing. You can call `text.toggleEditMode()` programmatically (e.g. right after creating a new text element).
## Image element [#image-element]
Image element draws an image on the canvas. By default images do smart-cropping to fit into its size.
```ts
page.addElement({
type: 'image',
x: 0,
y: 0,
rotation: 0,
locked: false, // deprecated
blurEnabled: false,
blurRadius: 10,
brightnessEnabled: false,
brightness: 0,
shadowEnabled: false,
shadowBlur: 5,
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowColor: 'black',
shadowOpacity: 1,
name: '', // name of element, can be used to find element in the store
// url path to image
src: 'https://example.com/image.png',
keepRatio: false, // can we change aspect ratio of element on resize
stretchEnabled: false, // can we stretch image on any axis
// url path to svg or image that will be used to clip image
// can be used for image framing
clipSrc: '',
width: 100,
height: 100,
cropX: 0, // 0-1 : % from original image width
cropY: 0, // 0-1 : % from original image height
cropWidth: 1, // 0-1 : % from original image width
cropHeight: 1, // 0-1 : % from original image height
cornerRadius: 0,
borderColor: 'black',
borderSize: 0,
flipX: false,
flipY: false,
// can user select element?
// if false, element will be "invisible" for user clicks
selectable: true,
// use for absolute positing of element
alwaysOnTop: false,
// also we can hide some elements from the export
showInExport: true,
// can element be moved and rotated
draggable: true,
// can we change content of element?
contentEditable: true,
// can we remove element from UI with button or keyboard?
removable: true,
// can we resize element?
resizable: true,
});
```
### image.toggleCropMode() [#imagetogglecropmode]
Enter into "crop mode" of the image programmatically.
```tsx
}
// use capture handler to handle click event sooner
onClickCapture={(e) => {
// stop propagation to prevent autohide of crop mode on current click
e.stopPropagation();
element.toggleCropMode(true);
}}
/>
```
### Image filters [#image-filters]
Polotno recognizes two filter categories, each with its own API pattern.
1. Old filters (use `element.set`):
```txt
sepiaEnabled
grayscaleEnabled
blurEnabled, blurRadius
brightnessEnabled, brightness
```
2. Combined filters (use `element.setFilter`):
```txt
cold
natural
warm
temperature
contrast
shadows
white
black
saturation
vibrance
```
Examples:
```ts
// 1. Old filters
shape.set({ sepiaEnabled: true });
shape.set({ blurEnabled: true, blurRadius: 12 });
shape.set({ brightnessEnabled: true, brightness: 0.25 }); // +25 %
// 2. One‑active preset (replaces previous preset)
shape.setFilter('warm');
// 3. Combined numeric filters (stackable)
shape.setFilter('temperature', -0.2);
shape.setFilter('contrast', 0.15);
shape.setFilter('vibrance', 0.3);
// disabling:
// Old filters off
shape.set({
sepiaEnabled: false,
blurEnabled: false,
brightnessEnabled: false,
});
// Remove current preset
shape.setFilter('warm', null);
// Clear all combined filters
['temperature', 'contrast', 'shadows', 'white', 'black', 'saturation', 'vibrance']
.forEach((f) => shape.setFilter(f, null));
```
## SVG element [#svg-element]
SVG elements work similarly to Image elements but can replace internal colors.
```ts
const svgElement = page.addElement({
type: 'svg',
src: 'https://example.com/image.svg',
maskSrc: '', // should we draw mask image over svg element?
keepRatio: false, // can we change aspect ratio of svg?
stretchEnabled: false, // can we stretch image on any axis
x: 0,
y: 0,
rotation: 0,
locked: false, // deprecated
blurEnabled: false,
blurRadius: 10,
brightnessEnabled: false,
brightness: 0,
shadowEnabled: false,
shadowBlur: 5,
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowColor: 'black',
shadowOpacity: 1,
name: '', // name of element, can be used to find element in the store
width: 100,
height: 100,
flipX: false,
flipY: false,
cornerRadius: 0,
// can user select element?
// if false, element will be "invisible" for user clicks
selectable: true,
// use for absolute positing of element
alwaysOnTop: false,
// also we can hide some elements from the export
showInExport: true,
// can element be moved and rotated
draggable: true,
// can we change content of element?
contentEditable: true,
// can we remove element from UI with button or keyboard?
removable: true,
// can we resize element?
resizable: true,
// map of originalColor -> newColor, used to replace colors in svg image
// do not change it manually. Instead use `el.replaceColor(originalColor, newColor)`
colorsReplace: {},
});
```
If you want to detect colors in an SVG string you can use this:
```ts
import { urlToString, getColors, useSvgColors } from 'polotno/utils/svg';
// in react component:
const Toolbar = ({ element }) => {
const colors = useSvgColors(element.src); // will return array of colors detected in the svg image
};
// in functions:
async function getSvgColors(element) {
const svgString = await urlToString(element.src);
const colors = getColors(svgString);
}
```
### svgElement.replaceColor(oldColor, newColor) [#svgelementreplacecoloroldcolor-newcolor]
Some SVG files support color replacement when a color is defined as `fill` on internal nodes. You can replace specific colors to modify the original image.
```ts
svgElement.replaceColor('red', 'blue');
```
In the editor, multi-color SVGs open a color palette popover listing every detected color, with **Recolor all** to tint them at once. Each `replaceColor` call adds an entry to the element's `colorsReplace` map.
### svgElement.resetColors() [#svgelementresetcolors]
Revert all color replacements, restoring the SVG's original colors (clears the `colorsReplace` map). This backs the **Reset** action in the SVG color popover.
```ts
svgElement.resetColors();
```
## Line element [#line-element]
Use line elements to draw lines and arrows on the canvas. For now, line elements may not support all available filters from JSON.
```ts
const lineElement = page.addElement({
type: 'line',
x: 0,
y: 0,
width: 400,
height: 10,
name: '', // name of element, can be used to find element in the store
color: 'black',
rotation: 0,
dash: [], // array of numbers, like [5, 5]
startHead: '', // can be empty, arrow, triangle, circle, square, bar
endHead: '', // can be empty, arrow, triangle, circle, square, bar
// can user select element?
// if false, element will be "invisible" for user clicks
selectable: true,
// use for absolute positing of element
alwaysOnTop: false,
// also we can hide some elements from the export
showInExport: true,
// can element be moved and rotated
draggable: true,
// can we change content of element?
contentEditable: true,
// can we remove element from UI with button or keyboard?
removable: true,
// can we resize element?
resizable: true,
styleEditable: true,
});
```
## Figure element [#figure-element]
Use figure to draw basic shapes on the canvas. It has a large collection of shapes controlled by `subType`.
```ts
const figureElement = page.addElement({
type: 'figure',
subType: 'rect',
x: 0,
y: 0,
width: 400,
height: 400,
fill: 'black',
stroke: 'red',
strokeWidth: 40,
cornerRadius: 40,
});
```
Currently supported list of `subType`:
**rect**, **circle**, **star**, **triangle**, **rightTriangle**, **diamond**, **pentagon**, **hexagon**, **speechBubble**, **cross**, **arc**, **cloud**, **rightArrow**, **leftArrow**, **downArrow**, **upArrow**, **asterisk1**, **asterisk2**, **bookmark**, **butterfly**, **cylinder**, **diamond2**, **door**, **drop1**, **drop2**, **explosion**, **flag**, **flower**, **frame**, **heart1**, **home**, **home2**, **hourglass**, **house**, **keyhole**, **kiss**, **leaf**, **lightning1**, **lightning2**, **magnet**, **mithosis**, **orangeRicky**, **party**, **pillow**, **polygon**, **rainbow**, **rhodeIsland**, **shell**, **shield1**, **shield2**, **skewedRectangle**, **softFlower**, **softStar**, **stairs1**, **stairs2**, **teewee**, **blob1**, **blob10**, **blob11**, **blob12**, **blob13**, **blob14**, **blob15**, **blob16**, **blob17**, **blob18**, **blob19**, **blob2**, **blob20**, **blob21**, **blob22**, **blob23**, **blob24**, **blob25**, **blob26**, **blob27**, **blob28**, **blob29**, **blob3**, **blob30**, **blob31**, **blob32**, **blob4**, **blob5**, **blob6**, **blob7**, **blob8**, **blob9**
## Video element [#video-element]
Use video to render a video on the canvas. Video element has many properties similar to the image element.
```ts
const videoElement = page.addElement({
type: 'video',
x: 0,
y: 0,
rotation: 0,
// url path to video
src: 'https://example.com/image.png',
// start/end time of video in % from video duration
// can be used for trimming video
startTime: 0,
endTime: 1,
shadowEnabled: false,
shadowBlur: 5,
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowColor: 'black',
shadowOpacity: 1,
name: '', // name of element, can be used to find element in the store
width: 100,
height: 100,
cropX: 0, // 0-1 : % from original image width
cropY: 0, // 0-1 : % from original image height
cropWidth: 1, // 0-1 : % from original image width
cropHeight: 1, // 0-1 : % from original image height
borderColor: 'black',
borderSize: 0,
flipX: false,
flipY: false,
// can user select element?
// if false, element will be "invisible" for user clicks
selectable: true,
// use for absolute positing of element
alwaysOnTop: false,
// also we can hide some elements from the export
showInExport: true,
// can element be moved and rotated
draggable: true,
// can we change content of element?
contentEditable: true,
// can we remove element from UI with button or keyboard?
removable: true,
// can we resize element?
resizable: true,
});
```
## Group element [#group-element]
Group element is a container for other elements. It can be used to move multiple elements together.
Here is the example of default properties and usage:
```ts
const page = store.addPage();
page.addElement({
type: 'text',
text: 'Hello world',
id: 'text-1',
});
page.addElement({
type: 'text',
text: 'Hello world',
id: 'text-2',
});
const group = store.groupElements(['text-1', 'text-2']);
group.set({
name: 'group',
opacity: 0.5,
custom: {},
visible: true,
selectable: true,
removable: true,
alwaysOnTop: false,
showInExport: true,
});
// ungroup:
store.ungroupElements([group.id]);
```
## Table element [#table-element]
A table element renders a grid of editable text cells on the canvas. Tables support configurable rows and columns, per-cell text styling, cell merging, per-side border customization, keyboard navigation, and auto-growing rows that expand to fit content.
Tables do **not** support shadow, blur, brightness, sepia, grayscale, or filter effects.
```ts
const table = page.addElement({
type: 'table',
x: 50,
y: 50,
width: 400,
height: 250,
rows: 3,
cols: 4,
// text styling defaults (inherited by all cells)
fontSize: 30,
fontFamily: 'Roboto',
fontWeight: 'normal',
fontStyle: 'normal',
textDecoration: '',
textTransform: 'none',
fill: 'black',
align: 'left',
verticalAlign: 'top',
lineHeight: 1.2,
letterSpacing: 0,
strokeWidth: 0,
stroke: 'black',
// border defaults
borderColor: '#000000',
borderWidth: 1,
borderStyle: 'solid', // 'solid', 'dashed', 'dotted', 'none'
// cell defaults
cellBackground: 'transparent',
cellPadding: 4,
name: '',
selectable: true,
alwaysOnTop: false,
showInExport: true,
draggable: true,
contentEditable: true,
removable: true,
resizable: true,
styleEditable: true,
});
```
Cells, `colWidths`, and `rowHeights` are generated automatically when omitted. Each column and row starts with equal width/height.
### Pre-populating cells [#pre-populating-cells]
You can pass cell data when creating a table. Cells are stored in **row-major order** (left-to-right, top-to-bottom). Only properties that differ from the table defaults need to be specified.
```ts
page.addElement({
type: 'table',
x: 50,
y: 50,
width: 400,
height: 200,
rows: 2,
cols: 3,
cells: [
{ id: 'h1', text: 'Name' },
{ id: 'h2', text: 'Role' },
{ id: 'h3', text: 'Email' },
{ id: 'r1', text: 'Alice' },
{ id: 'r2', text: 'Engineer' },
{ id: 'r3', text: 'alice@example.com' },
],
});
```
### Cell access [#cell-access]
```ts
// Get a specific cell by row/column index (0-based)
const cell = table.getCell(0, 2); // row 0, column 2
// All visible (non-merged) cells
const visible = table.visibleCells;
// Currently focused cells
const focused = table.focusedCells;
// Cell currently being edited (or undefined)
const editing = table.editingCell;
```
### Row operations [#row-operations]
```ts
// Add a row at index (pushes existing rows down)
table.addRow(1);
// Remove a row by index
table.removeRow(2);
// Resize a row border between row[index] and row[index+1]
// delta is a fraction of total height
table.resizeRow(0, 0.05);
// Make all rows the same height
table.distributeRowsEvenly();
```
### Column operations [#column-operations]
```ts
// Add a column at index (pushes existing columns right)
table.addColumn(2);
// Remove a column by index
table.removeColumn(0);
// Resize a column border between col[index] and col[index+1]
// delta is a fraction of total width
table.resizeColumn(1, -0.03);
// Make all columns the same width
table.distributeColumnsEvenly();
```
### Cell focus and editing [#cell-focus-and-editing]
```ts
// Focus a single cell
table.focusCell(cell.id);
// Add a cell to the current selection
table.focusCell(cell.id, true);
// Focus a rectangular range from the anchor cell to (row, col)
table.focusCellRange(2, 3);
// Clear all cell focus and exit editing
table.clearCellFocus();
// Enter text editing mode for a cell
table.enterCellEdit(cell.id);
// Exit text editing mode
table.exitCellEdit();
```
### Cell styling [#cell-styling]
Each cell can override any of the table's text styling defaults. When a property is `null` or `undefined`, the cell inherits the table-level value.
```ts
// Update cell properties
cell.set({
text: 'Hello',
fontSize: 24,
fill: 'red',
fontWeight: 'bold',
cellBackground: '#f0f0f0',
});
// Reset a cell property to inherit from the table
cell.set({ fontSize: null });
```
### Borders [#borders]
Each cell has four border sides (`top`, `right`, `bottom`, `left`). Border properties follow a three-level resolution: cell override → table default → hardcoded fallback.
```ts
// Set borders on multiple cells at once
// Automatically syncs adjacent cells
table.setCellBorders(
[cell1.id, cell2.id],
['top', 'bottom'],
{ color: 'red', width: 2, style: 'dashed' },
);
// Set a single border on one cell (does NOT sync adjacent cells)
cell.setBorder('top', { color: 'blue', width: 1 });
// Read the effective border for a side
const border = cell.getEffectiveBorder('top');
// => { color: string, width: number, style: string }
```
### table.clone() [#tableclone]
Clone a table (generates new IDs for the table and all cells, remaps `mergedInto` references).
```ts
const copy = table.clone({ x: table.x + 50 });
```
---
# Page
URL: https://polotno.com/docs/page
**Page** is a container for elements in the **store**. A **store** may have several pages.
```ts
const page = store.addPage({
background: 'grey', // default is "white"
});
```
## page.set(attrs) [#pagesetattrs]
You can use `set()` to change properties of the page container. You can change `background`, `custom`, `bleed` (plus per-side `bleedTop`, `bleedRight`, `bleedBottom`, `bleedLeft` overrides), `width`, and `height`.
See also: [docs](/docs/page#pagesetattrs)
```ts
page.set({
// background can be any CSS color string or a URL to an image
background: 'red',
// you can use "custom" to save your own app-specific data
custom: { myInternalId: 'some-id-here' },
bleed: 10, // in pixels
bleedBottom: 20, // optional per-side override of "bleed"
width: 1000, // in pixels. Use 'auto' to inherit width from the store
height: 1000, // in pixels. Use 'auto' to inherit height from the store
});
```
## `page.setSize({ width, height, useMagic })` [#pagesetsize-width-height-usemagic-]
Change size of the specified page. If `useMagic` is `true`, all elements will be resized proportionally.
See also: [docs](/docs/page#pagesetsize-width-height-usemagic-)
```ts
page.setSize({
width: 1000,
height: 1000,
useMagic: true,
});
```
## page.addElement(attrs) [#pageaddelementattrs]
Create an element with the specified attributes. It is important to provide the `type` attribute.
See also: [docs](/docs/page#pageaddelementattrs)
```ts
store.activePage?.addElement({
type: 'text',
x: 50,
y: 50,
fill: 'black',
text: 'hello',
});
```
## page.clone() [#pageclone]
Duplicate the page.
See also: [docs](/docs/page#pageclone)
```ts
store.activePage?.clone();
```
## page.setZIndex(index) [#pagesetzindexindex]
Change the order of pages.
See also: [docs](/docs/page#pagesetzindexindex)
```ts
// move active page to the beginning of the list
store.activePage?.setZIndex(0);
```
## page.children [#pagechildren]
Use `children` to access elements inside the page.
See also: [docs](/docs/page#pagechildren)
```ts
// move every element by 10px to the right
store.activePage?.children.forEach((element) => {
element.set({ x: element.x + 10 });
});
```
## page.computedWidth [#pagecomputedwidth]
Return width of the page in pixels.
See also: [docs](/docs/page#pagecomputedwidth)
## page.computedHeight [#pagecomputedheight]
Return height of the page in pixels.
See also: [docs](/docs/page#pagecomputedheight)
---
# Store
URL: https://polotno.com/docs/store
Store is a **root data model** object that you can use to read and control canvas content, check selection, export into different formats and a lot more! It will also provide you with access to [Pages](/docs/page) and [Elements](/docs/element).
```ts
import { createStore } from 'polotno/model/store';
const store = createStore({
key: 'YOUR_API_KEY', // you can create it here: https://polotno.com/cabinet/
// you can hide back-link on a paid license
// but it will be good if you can keep it for Polotno project support
showCredit: true,
});
```
## Working with pages [#working-with-pages]
### store.addPage() [#storeaddpage]
Store is a set of pages. Each page has elements on it.
```ts
const page = store.addPage();
```
### store.pages [#storepages]
Getter. Returns all pages of the store.
### store.activePage [#storeactivepage]
Getter. Returns current active (focused) page or the last created.
### store.selectPage(id) [#storeselectpageid]
Mark a page as active. It will automatically scroll `Workspace` to that page. You may think about this method as “focusPage”. For backward compatibility, we keep the original name for now.
```ts
// focus second page
store.selectPage(store.pages[1].id);
```
### store.selectedPages [#storeselectedpages]
Returns array of currently selected pages. A user may select a page by clicking on its background or from `PagesTimeline` component.
### store.selectPages(ids) [#storeselectpagesids]
Mark pages as selected for group actions in `PagesTimeline` or visibility of page background properties in the toolbar.
```ts
store.selectPages([store.activePage.id]); // show page props in the toolbar
store.selectPages([id1, id2]); // select several pages for highlight in the Timeline
```
### store.deletePages(ids) [#storedeletepagesids]
Remove pages from the store.
```ts
// remove current page
store.deletePages([store.activePage.id]);
```
### store.clear() [#storeclear]
Remove all pages from the store.
```ts
store.clear(); // it will remove all data and reset undo history
store.clear({ keepHistory: true }); // it will remove all data but keep undo history
```
## UI [#ui]
### store.width and store.height [#storewidth-and-storeheight]
Returns size of every page.
```ts
console.log('Width is', store.width);
```
### store.setSize(width, height, shouldUseMagicResize?) [#storesetsizewidth-height-shouldusemagicresize]
Set new size of every page in the store.
```ts
// just change the pages size
store.setSize(1000, 500);
// resize all pages and apply "magic resize"
// magic resize will automatically change sizes of all elements and adjust their position for a new size
store.setSize(1000, 500, true);
```
### store.scale [#storescale]
Getter for current zoom level of the active page.
```ts
console.log('zoom is', store.scale);
```
### store.setScale(size) [#storesetscalesize]
Change zooming of active page.
```ts
store.setScale(2);
```
### store.openedSidePanel [#storeopenedsidepanel]
Getter for current opened side panel.
### store.previousOpenedSidePanel [#storepreviousopenedsidepanel]
Name of the side-panel that was opened before the current one. Useful if you want to open a side panel temporarily for some action then restore the original side panel.
### store.openSidePanel(panelName) [#storeopensidepanelpanelname]
Change open side panel manually. The `panelName` corresponds to the `name` property of a section.
```ts
store.openSidePanel('templates');
```
In addition to the visible tab sections, Polotno has several built-in hidden panels: `effects`, `animation`, and `image-clip`. See [Hidden Panels](/docs/hidden-panels) for details.
**Note:** Calling `store.openSidePanel('')` before the `` component mounts will not work, because the component resets the panel to its default on mount. To hide the side panel on initial load, use the `defaultSection=""` prop on the `` component instead. See [Side Panel overview](/docs/side-panel-overview) for details.
### `store.setUnit({ unit, dpi })` [#storesetunit-unit-dpi-]
Set unit metrics to use in UI.
```ts
store.setUnit({
unit: 'mm', // mm, cm, in, pt, px
dpi: 300,
});
```
Important! Setting new unit will NOT change values in store and elements attributes, such as width, height, x, y, fontSize. They will still be saved in pixels. Unit metrics will be used only for default UI from Polotno.
### store.unit [#storeunit]
Returns unit used in UI to display measures. By default it is px. You can change it to pt, mm, cm, in.
### store.dpi [#storedpi]
Returns dpi used to convert pixels to other units.
### store.toggleBleed() [#storetogglebleed]
Show or hide bleed area on the workspace.
```ts
store.activePage.set({ bleed: 20 }); // set bleed in pixels
store.toggleBleed();
store.toggleBleed(false);
```
### store.bleedVisible [#storebleedvisible]
Getter for current state of bleed visibility.
### store.toggleRulers() [#storetogglerulers]
Show or hide rulers on workspace.
```ts
store.toggleRulers();
store.toggleRulers(false);
```
### store.rulersVisible [#storerulersvisible]
Getter for current state of rulers visibility.
### store.toggleDistanceGuides() [#storetoggledistanceguides]
Show or hide distance guides on the workspace. Distance guides appear when a user selects an element and holds `ALT`; this method forces them on even without the modifier key.
```ts
store.toggleDistanceGuides();
store.toggleDistanceGuides(false);
```
### store.distanceGuidesVisible [#storedistanceguidesvisible]
Getter for current state of distance guides visibility.
### store.setRole(role) [#storesetrolerole]
Set role of the current user. Possible values are: `user`, `admin`, and `viewer` (read-only). For more information see [Roles Documentation](/docs/user-roles).
```ts
store.setRole('admin');
```
## Tools [#tools]
### store.setTool(toolName) [#storesettooltoolname]
Switch between different canvas tools. Available values: `selection`, `draw`.
```ts
// enable drawing mode
store.setTool('draw');
// return to default selection mode
store.setTool('selection');
```
### store.tool [#storetool]
Getter for current active tool. Returns `selection` or `draw`.
```ts
console.log(store.tool); // 'selection'
```
### store.setToolOptions(options) [#storesettooloptionsoptions]
Configure options for the drawing tool. Sets stroke properties for freehand drawing.
```ts
store.setToolOptions({
strokeWidth: 50, // brush size in pixels
stroke: 'red', // stroke color (any CSS color)
opacity: 0.4 // opacity from 0 to 1
});
```
### store.toolOptions [#storetooloptions]
Getter for current tool options.
```ts
console.log(store.toolOptions);
// { strokeWidth: 50, stroke: 'red', opacity: 0.4 }
```
For more details, see [Drawing Tool Documentation](/docs/drawing).
## Working with elements [#working-with-elements]
### store.selectedElements [#storeselectedelements]
Returns array of currently selected elements on the current page.
```ts
const firstSelected = store.selectedElements[0];
```
### store.selectElements(ids) [#storeselectelementsids]
Selects elements on the canvas by their ids.
```ts
store.selectElements([element1.id, element2.id]);
// clear selection and unselect all elements
store.selectElements([]);
```
### store.deleteElements(ids) [#storedeleteelementsids]
Remove elements from the store.
```ts
store.deleteElements([element1.id, element2.id]);
```
### store.getElementById(id) [#storegetelementbyidid]
Finds element in the store by id:
```ts
const element = store.getElementById('some-id');
element.set({ x: 0 });
```
### store.groupElements(\[element1.id, element2.id]) [#storegroupelementselement1id-element2id]
Group elements. It will create a new element with type `group` and will move all passed elements inside it.
```ts
const ids = store.selectedElements.map((el) => el.id);
store.groupElements(ids);
// optionally you can use second argument to set properties of a group
store.groupElements(ids, { id: 'group-id' });
```
### store.ungroupElements(\[element1.id, element2.id]) [#storeungroupelementselement1id-element2id]
Ungroup elements. It will move all elements inside group to the page and remove the group.
```ts
const id = store.selectedElements[0].id;
store.ungroupElements([id]);
```
## History [#history]
### store.history.canUndo [#storehistorycanundo]
Can we undo state?
### store.history.undo() [#storehistoryundo]
Undo last changes
```ts
store.activeElements[0].set({ x: 10 });
store.history.undo();
```
### store.history.redo() [#storehistoryredo]
Redo changes.
```ts
// cancel changes
store.history.undo();
// apply changes again
store.history.redo();
```
### `store.history.transaction(async () => {})` [#storehistorytransactionasync---]
Batch several actions into one history entry.
```ts
store.history.transaction(async () => {
const element = store.activePage.addElement({
type: 'text',
text: 'loading',
});
const text = await serverRequest();
element.set({ text });
});
```
### `store.history.ignore(async () => {})` [#storehistoryignoreasync---]
Run transaction that should be ignored in history.
```ts
store.history.ignore(() => {
// that change will NOT create a new history point
element.set({ x: 0, y: 0 });
});
```
### store.history.startTransaction() [#storehistorystarttransaction]
Create a new history transaction. After that command all changes will be recorded as one history point.
### store.history.endTransaction() [#storehistoryendtransaction]
Finish created transaction and record all changes as one history point.
### store.history.clear() [#storehistoryclear]
Clear history.
## Serializations [#serializations]
### store.toJSON() [#storetojson]
Save store into plain object.
```ts
const json = store.toJSON();
// your own function to save JSON somewhere
saveIntoBackend(JSON.stringify(json));
```
### store.loadJSON(json, keepHistory?) / store.loadJSON(json, options?) [#storeloadjsonjson-keephistory--storeloadjsonjson-options]
Load passed json into the store. It will update all properties, pages and elements. By default loading JSON into the store will reset undo/redo history (so a user can't undo it). You can use `keepHistory` argument, if you want to preserve the history.
The second argument can also be an options object (`{ keepHistory?, modernizeTextBackground? }`). Pass `modernizeTextBackground: true` to force the modern per-line rich-text background render on pre-v3 documents — by default, loading a `schemaVersion < 3` design with `backgroundEnabled: true` preserves its pre-v3 full-rect look pixel-exact.
```ts
import { createStore } from 'polotno/model/store';
const store = createStore({
key: 'YOUR_API_KEY', // you can create it here: https://polotno.com/cabinet/
// you can hide back-link on a paid license
// but it will be good if you can keep it for Polotno project support
showCredit: true,
});
// save to JSON at some point of time
const json = store.toJSON();
// load from JSON
// remember that "json" should be a javascript object
// if you have a json string, you may need to parse it - JSON.parse(string);
store.loadJSON(json);
// load JSON but save previous undo history
// a user can undo this action
store.loadJSON(json, true);
// force modern per-line text backgrounds when loading pre-v3 designs
store.loadJSON(oldJson, { modernizeTextBackground: true });
```
## Events [#events]
### `store.on('change', () => {})` [#storeonchange---]
Listen to any changes in the store. The event may trigger frequently on some operations like resize or dragging. `store.on()` returns an unsubscribe function.
```ts
// unsubscribe function
const off = store.on('change', () => {
console.log('something changed');
});
// that line will unsubscribe from the event
off();
// write a function for throttle saving
let timeout: any = null;
const requestSave = () => {
// if save is already requested - do nothing
if (timeout) {
return;
}
// schedule saving to the backend
timeout = setTimeout(() => {
// reset timeout
timeout = null;
// export the design
const json = store.toJSON();
// save it to the backend
fetch('https://example.com/designs', {
method: 'POST',
body: JSON.stringify(json),
});
}, 1000);
};
// request saving operation on any changes
store.on('change', () => {
requestSave();
});
```
## Export [#export]
### store.waitLoading() [#storewaitloading]
Wait until all resources (images, fonts, etc) are loaded and displayed on the canvas.
```ts
// import data
store.loadJSON(json);
// wait for loading
await store.waitLoading();
// do export
const url = await store.toDataURL();
```
### await store.toDataURL() [#await-storetodataurl]
Export store into image in base64 URL format
```ts
// default export
await store.toDataURL();
// make exported image 2x bigger (higher quality)
await store.toDataURL({ pixelRatio: 2 });
// make small export for preview
// quickMode will skip some "perfect rendering" calculations
await store.toDataURL({ pixelRatio: 0.2, quickMode: true });
// ignore page background on export
await store.toDataURL({ ignoreBackground: true });
// export as jpg
await store.toDataURL({ mimeType: 'image/jpeg' });
// control jpg compression (0-1 range, defaults to browser value)
await store.toDataURL({ mimeType: 'image/jpeg', quality: 0.85 });
// export only required page
await store.toDataURL({ pageId: store.pages[1].id });
// export with page bleed
await store.toDataURL({ includeBleed: true });
// embed custom DPI metadata (for PNG/JPEG only)
await store.toDataURL({ dpi: 300 });
// control DPI metadata embedding behavior
await store.toDataURL({ dpi: 300, dpiMetadata: 'always' });
await store.toDataURL({ dpiMetadata: 'never' }); // skip DPI embedding
```
Use `quality` to control JPEG compression (0-1) whenever you switch `mimeType` to `image/jpeg`. Lower values produce smaller files; keep it above `0.8` for print previews.
**DPI Metadata:** By default (`dpiMetadata: 'auto'`), DPI metadata is embedded into PNG and JPEG images only when DPI differs from 72. Use `dpi` to override the store's DPI value for this export. Set `dpiMetadata: 'never'` to skip embedding entirely, or `dpiMetadata: 'always'` to force embedding even at 72 DPI. DPI metadata is only supported for raster formats (PNG/JPEG).
### await store.saveAsImage() [#await-storesaveasimage]
`saveAsImage` will export the drawing into an image and download it as a local file. By default, it exports just the first page. If you need to export other pages, pass `pageId` property.
```ts
// default export
store.saveAsImage({ fileName: 'polotno.png' });
// export as jpg
store.saveAsImage({ mimeType: 'image/jpeg' });
// control jpg compression (0-1 range, defaults to browser value)
store.saveAsImage({ mimeType: 'image/jpeg', quality: 0.85 });
// make exported image 2x bigger (higher quality)
store.saveAsImage({ pixelRatio: 2 });
// ignore page background on export
store.saveAsImage({ ignoreBackground: true });
// export second page
store.saveAsImage({ pageId: store.pages[1].id });
// export with page bleed
store.saveAsImage({ includeBleed: true });
// embed custom DPI metadata for print workflows
store.saveAsImage({ dpi: 300 });
// control DPI metadata embedding
store.saveAsImage({ dpi: 300, dpiMetadata: 'always' });
```
Tune `quality` for JPEG downloads when you need a balance between file size and preview fidelity. The option ranges from `0` (smallest) to `1` (best quality).
**DPI Metadata:** Use `dpi` to embed resolution metadata for print workflows. By default (`dpiMetadata: 'auto'`), DPI is embedded only when it differs from 72. Set `dpiMetadata: 'never'` to skip embedding, or `dpiMetadata: 'always'` to force it. Applies only to PNG and JPEG formats.
### async store.toPDFDataURL() [#async-storetopdfdataurl]
Export store into PDF file in base64 URL format. You can use it to save to the backend.
```ts
// default export
await store.toPDFDataURL();
// double exported quality
await store.toPDFDataURL({ pixelRatio: 2 });
// ignore page background on export
await store.toPDFDataURL({ ignoreBackground: true });
// export only sub set of pages
await store.toPDFDataURL({ pageIds: [store.pages[0].id, store.pages[2].id] });
// export with custom measurement units for trim boxes
await store.toPDFDataURL({ unit: 'mm' });
// export with page bleed and crop marks
await store.toPDFDataURL({ includeBleed: true, cropMarkSize: 20 });
// track progress
await store.toPDFDataURL({ onProgress: (progress) => console.log(progress) });
// render several pages concurrently while assets load
await store.toPDFDataURL({ parallel: 8 });
```
Set `unit` to align PDF geometry with print-ready specs (`pt`, `mm`, `cm`, or `in`). Increase `parallel` to process multiple pages simultaneously so images and fonts load sooner across large documents; lower it if bandwidth or memory is limited.
### await store.saveAsPDF() [#await-storesaveaspdf]
`saveAsPDF` will export the drawing into PDF and download it as a local file. By default, it exports just the first page. If you need to export other pages, pass `pageId` property.
```ts
// default export
await store.saveAsPDF({ fileName: 'polotno.pdf' });
// change default dpi
// changing DPI will not affect quality of the export. But it may change page size of exported PDF
await store.saveAsPDF({ dpi: 300 }); // default is store.dpi, it equals 72
// ignore page background on export
await store.saveAsPDF({ ignoreBackground: true });
// change export quality
await store.saveAsPDF({ pixelRatio: 2 });
// export with page bleed
await store.saveAsPDF({ includeBleed: true });
// export with page bleed and crop marks
await store.saveAsPDF({ includeBleed: true, cropMarkSize: 20 });
// export using millimeters for bleed and crop mark values
await store.saveAsPDF({ unit: 'cm' });
// export only sub set of pages
await store.saveAsPDF({ pageIds: [store.pages[0].id, store.pages[2].id] });
// track progress
await store.saveAsPDF({ onProgress: (progress) => console.log(progress) });
// render several pages concurrently while assets load
await store.saveAsPDF({ parallel: 8 });
```
Use `unit` when your workflow requires trim boxes in `mm`, `cm`, or `in` instead of the default `pt`. Adjust `parallel` to balance how many pages load assets at once versus overall memory usage.
### await store.toBlob() [#await-storetoblob]
Export store into blob object, it may work faster than `toDataURL` method.
```ts
// default export
await store.toBlob();
// make exported image 2x bigger (higher quality)
await store.toBlob({ pixelRatio: 2 });
// ignore page background on export
await store.toBlob({ ignoreBackground: true });
// export as jpg
await store.toBlob({ mimeType: 'image/jpeg' });
// control jpg compression (0-1 range, defaults to browser value)
await store.toBlob({ mimeType: 'image/jpeg', quality: 0.85 });
// export only required page
await store.toBlob({ pageId: store.pages[1].id });
// export with page bleed
await store.toBlob({ includeBleed: true });
// embed DPI metadata for print workflows
await store.toBlob({ dpi: 300 });
// control DPI metadata embedding
await store.toBlob({ dpi: 300, dpiMetadata: 'always' });
```
When exporting JPEG blobs, pass `quality` (0-1) to match asset pipelines that expect specific compression levels.
**DPI Metadata:** Use `dpi` and `dpiMetadata` to control resolution metadata embedding. By default, DPI is embedded only when it differs from 72. Applies only to PNG and JPEG formats.
### await store.toHTML() [#await-storetohtml]
Warning: this function is beta and may produce inconsistent result.
Export store into HTML string.
```ts
const html = await store.toHTML();
const html2 = await store.toHTML({
elementHook: ({ dom, element }) => {
// post process dom representation of the element
// it is a lite jsx-like object
// it should have "type", "props" and "children"
console.log(dom); // first you can log it to see what is inside
// then you can mutate it based on your custom logic
dom.props.className = 'my-element';
return dom;
},
});
```
### await store.saveAsHTML() [#await-storesaveashtml]
Warning: this function is beta and may produce inconsistent result.
Export store into HTML string and download it as local `.html` file.
### await store.toSVG() [#await-storetosvg]
Warning: this function is beta and may produce inconsistent result.
Export store into SVG string.
```ts
const svg = await store.toSVG({
fontEmbedding: 'inline', // "inline" or "none"
elementHook: ({ dom, element }) => {
// post process dom representation of the element
dom.className = 'my-element';
return dom;
},
});
```
### await store.saveAsSVG() [#await-storesaveassvg]
Export store into SVG string and download it as local `.svg` file.
### await store.toGIFDataURL() [#await-storetogifdataurl]
Export store into GIF image in base64 URL format.
```ts
// default GIF export
const url1 = await store.toGIFDataURL();
// export with full control and progress tracking
const url2 = await store.toGIFDataURL({
// GIF-specific option
fps: 10,
// control size/quality of the exported GIF
pixelRatio: 1,
});
// export only a single page
await store.toGIFDataURL({ pageId: store.pages[1].id });
// export a subset of pages
await store.toGIFDataURL({
pageIds: [store.pages[0].id, store.pages[2].id],
});
```
By default the GIF includes every page. Pass `pageId` to render just one page, or `pageIds` to render a subset in order.
### await store.saveAsGIF() [#await-storesaveasgif]
`saveAsGIF` will export drawing into GIF and download it as local file.
```ts
// or save directly as a file with similar options
await store.saveAsGIF({
fileName: 'polotno.gif',
fps: 10,
pixelRatio: 2,
});
// export only a single page
await store.saveAsGIF({ pageId: store.pages[1].id });
// export a subset of pages
await store.saveAsGIF({
pageIds: [store.pages[0].id, store.pages[2].id],
});
```
### store.setElementsPixelRatio(ratio) [#storesetelementspixelratioratio]
In most scenarios Polotno is rasterizing (converting into bitmap) vector elements such as texts and SVG images. When you do high quality exports, you may want to temporarily increase resolution of rendered elements.
```ts
// make sure all elements are rendered with increased quality
store.setElementsPixelRatio(2);
// do the export
await store.toDataURL({ pixelRatio: 2 });
```
## Animations and video [#animations-and-video]
### store.play() [#storeplay]
Preview animations inside the workspace
```ts
// start playing
store.play();
// start from a specific time
store.play({ startTime: 10000 });
// automatically pause on a specific time
store.play({ endTime: 10000 });
// animate only selected elements
store.play({ animatedElementsIds: store.selectedElements.map((el) => el.id) });
// loop play
store.play({ repeat: true });
```
### store.pause() [#storepause]
Pause playback, keeping the playhead where it is. You can resume from the same position with `store.play({ startTime: store.currentTime })`.
### store.stop() [#storestop]
Stop all animations, reset the current time
### store.currentTime [#storecurrenttime]
Returns current time of playback
### store.setCurrentTime() [#storesetcurrenttime]
Seek to a specific time
### store.setTimingPreview(mode) [#storesettimingpreviewmode]
Controls how the paused canvas treats element timing. In the default `'all'` mode all elements are visible for editing. In `'playhead'` mode the canvas reflects the playhead position — elements outside of their time range are hidden, like during playback. Useful for video-style editing UIs.
```ts
store.setTimingPreview('playhead');
// read the current mode
store.timingPreview; // 'all' | 'playhead'
```
## Working with audio [#working-with-audio]
### `store.addAudio({ src })` [#storeaddaudio-src-]
Add audio into the design
```ts
store.addAudio({
src: 'https://example.com/audio.mp3',
// optional props:
startTime: 0, // define relative point in time of original sound to start the play.
endTime: 1, // define relative point in time of original sound to stop the play.
volume: 1, // from 0 to 1
delay: 0, // in ms the duration between start of video (final scene) and audio play time
});
store.addAudio({
src: 'https://example.com/audio.mp3',
delay: 10000, // the audio will start to play after 10 seconds of video
startTime: 0.5, // if audio duration is 1 min, then we will start to hear it from its 30 secs
});
```
### store.removeAudio(audioId) [#storeremoveaudioaudioid]
Remove audio by its id
### store.audios [#storeaudios]
Access array of added audios of the scene
```ts
store.audios.map((audio) => {
console.log(audio.id, audio.src);
});
```
## Working with fonts [#working-with-fonts]
For a complete guide on font types and when to use each, see the [Fonts guide](/docs/fonts).
### `store.addFont({ fontFamily, url })` [#storeaddfont-fontfamily-url-]
Add a custom font to the current design. These "design fonts" are saved into exported JSON via `store.toJSON()`. Users can see and remove them from the Text side panel.
For app-wide fonts that shouldn't be stored in JSON, use [Global Fonts](/docs/fonts#global-fonts) instead.
```ts
store.addFont({
fontFamily: 'MyCustomFont',
url: serverUrlOrBase64,
});
```
Also you can use a richer API for more control:
```ts
store.addFont({
fontFamily: 'MyCustomFont',
styles: [
{
src: 'url("pathToNormalFile.ttf")',
fontStyle: 'normal',
fontWeight: 'normal',
},
{
src: 'url("pathToBoldFile.ttf")',
fontStyle: 'normal',
fontWeight: 'bold',
},
],
});
```
Or you can just register font in the store and then manually add required CSS into the page:
```ts
store.addFont({
fontFamily: 'MyCustomFont',
});
```
### store.removeFont(name) [#storeremovefontname]
Remove custom font from the store by its name
```ts
store.removeFont('MyCustomFont');
```
### store.loadFont(name) [#storeloadfontname]
Prepare the font to use on the webpage. Text elements inside `Workarea` will use this function automatically. But it can be useful if you want to show a list of fonts somewhere in the UI. `store.loadFont(name)` function will add font to the webpage, so the browser can render correct font.
---
# Utility Functions
URL: https://polotno.com/docs/utils
Polotno provides utility functions organized by module to help with common tasks like SVG manipulation, image processing, geometric calculations, and text measurement. These utilities are designed to work seamlessly with Polotno's data model.
## `polotno/utils/svg` [#polotnoutilssvg]
Utilities for working with SVG strings, converting them to data URLs, and extracting color information.
### `svgToURL(svgString)` [#svgtourlsvgstring]
Converts an SVG string to a data URL that can be used as an image source.
```ts
import { svgToURL } from 'polotno/utils/svg';
const svgString = '';
const dataURL = svgToURL(svgString);
// Returns: "data:image/svg+xml;utf8,"
```
Use this when you need to display an SVG string in an `` tag or use it as an element source.
### `urlToString(url)` [#urltostringurl]
Converts a URL (including data URLs) to a string. Useful for extracting SVG content from element sources.
```ts
import { urlToString } from 'polotno/utils/svg';
const svgString = await urlToString(element.src);
console.log(svgString); // ""
```
Returns a Promise that resolves to the SVG string content. Works with both regular URLs and data URLs.
### `getColors(svgString)` [#getcolorssvgstring]
Extracts all colors found in an SVG string. Returns an array of color values.
```ts
import { getColors, urlToString } from 'polotno/utils/svg';
const svgString = await urlToString(element.src);
const colors = getColors(svgString);
// Returns: ['#ff0000', '#00ff00', 'rgb(0,0,255)']
```
Use this to detect colors in SVG files for building color pickers or validation tools.
### `useSvgColors(src)` [#usesvgcolorssrc]
React hook that extracts colors from an SVG source. Returns an array of detected colors.
```tsx
import { useSvgColors } from 'polotno/utils/svg';
const Toolbar = ({ element }) => {
const colors = useSvgColors(element.src);
// colors is an array of color strings
return (
{colors.map((color, i) => (
))}
);
};
```
The hook automatically handles URL resolution and SVG parsing. Use this in React components when you need reactive color detection.
## `polotno/utils/image` [#polotnoutilsimage]
Utilities for working with image dimensions and calculating crop values.
### `getImageSize(src)` [#getimagesizesrc]
Asynchronously detects the dimensions of an image from a URL or data URL.
```ts
import { getImageSize } from 'polotno/utils/image';
const { width, height } = await getImageSize('https://example.com/image.jpg');
console.log(`Image is ${width}x${height} pixels`);
```
Returns a Promise resolving to `{ width: number, height: number }`. Use this to get image dimensions before setting element properties.
### `getCrop(placeholder, imageSize)` [#getcropplaceholder-imagesize]
Calculates crop values to fit an image into a placeholder element using CSS "cover" behavior. The result fills the placeholder and may crop edges if aspect ratios differ.
```ts
import { getImageSize, getCrop } from 'polotno/utils/image';
// After getting uploaded file as dataURL
const { width, height } = await getImageSize(dataURL);
const crop = getCrop(placeholder, { width, height });
placeholder.set({
src: dataURL,
...crop,
// crop contains: cropX, cropY, cropWidth, cropHeight (normalized 0-1)
});
```
Crop values are normalized (0-1) and can be applied directly to image elements. For "contain" behavior, resize the element instead of using crop.
## `polotno/utils/math` [#polotnoutilsmath]
Geometric calculation utilities for bounding boxes, center points, and transformations.
### `getTotalClientRect(elements)` [#gettotalclientrectelements]
Calculates the axis-aligned bounding box of one or more elements, accounting for rotation.
```ts
import { getTotalClientRect } from 'polotno/utils/math';
const shapes = store.selectedElements;
const bbox = getTotalClientRect(shapes);
// Returns: { x: number, y: number, width: number, height: number }
```
Use this instead of simple `x + width/2` calculations when elements may be rotated. The bounding box accounts for rotation, giving accurate visual bounds.
### `getClientRect(element)` [#getclientrectelement]
Gets the bounding box of a single element, accounting for its rotation and transformations.
```ts
import { getClientRect } from 'polotno/utils/math';
const box = getClientRect(element);
// Returns: { x: number, y: number, width: number, height: number }
// Use for full-width backgrounds
backgroundEl.set({
x: 0,
y: box.y,
width: element.page.computedWidth,
height: box.height,
});
```
Returns the visual bounding box after applying rotation. Use this when you need precise element bounds for layout calculations.
### `getCenter(rect)` [#getcenterrect]
Calculates the center point of a rectangle.
```ts
import { getTotalClientRect, getCenter } from 'polotno/utils/math';
const bbox = getTotalClientRect(shapes);
const center = getCenter(bbox);
// Returns: { x: number, y: number }
```
Use this to find rotation centers for groups or multiple selections. Works with any object containing `x`, `y`, `width`, and `height` properties.
### `rotateAroundPoint(shape, angleDelta, center)` [#rotatearoundpointshape-angledelta-center]
Rotates a shape around a specified center point by a given angle delta.
```ts
import { getTotalClientRect, getCenter, rotateAroundPoint } from 'polotno/utils/math';
const bbox = getTotalClientRect(shapes);
const center = getCenter(bbox);
const delta = 45 - (element.rotation || 0);
const newShape = rotateAroundPoint(
{
x: element.x,
y: element.y,
width: element.width,
height: element.height,
rotation: element.rotation || 0,
},
delta,
center
);
element.set(newShape);
```
Returns a new shape object with updated `x`, `y`, and `rotation` values. Use this for programmatic rotation of elements around arbitrary points, such as the center of a selection.
## `polotno/utils/to-svg` [#polotnoutilsto-svg]
Convert Polotno JSON designs to SVG strings for lightweight preview generation.
### `jsonToSVG(options)` [#jsontosvgoptions]
Converts a Polotno JSON design to an SVG string. Useful for generating previews without mounting the full editor.
```ts
import { jsonToSVG } from 'polotno/utils/to-svg';
import { svgToURL } from 'polotno/utils/svg';
// templateJSON is your saved Polotno design
async function getThumbnailURL(templateJSON) {
const svgString = await jsonToSVG({ json: templateJSON });
return svgToURL(svgString); // returns data:image/svg+xml;utf8,…
}
```
Returns a Promise resolving to an SVG string. SVG rendering is in beta and may produce slightly different results compared to canvas rendering, but works well for preview thumbnails.
## `polotno/utils/to-canvas` [#polotnoutilsto-canvas]
Render individual elements to canvas elements for custom export or processing.
### `unstable_elementToCanvas(elementId, options?)` [#unstable_elementtocanvaselementid-options]
Renders a single element to an HTML canvas element. Returns a Promise resolving to a canvas.
```ts
import { unstable_elementToCanvas } from 'polotno/utils/to-canvas';
const canvas = await unstable_elementToCanvas(store.selectedElements[0].id);
const dataURL = canvas.toDataURL();
```
**Options:**
| Option | Type | Default | Description |
| ------------ | ------ | ------- | ----------------------------------------------------------------- |
| `pixelRatio` | number | 1 | Output resolution multiplier. Use `2` for retina-quality exports. |
Use `pixelRatio` to control the output resolution. This is useful when the workspace is small on screen but you need a higher-resolution preview:
```ts
// Higher-resolution render at 2x
const canvas = await unstable_elementToCanvas(store.selectedElements[0].id, {
pixelRatio: 2,
});
```
Higher `pixelRatio` does not guarantee higher quality for all elements. The function renders from the current workspace state, so if the workspace is scaled down, some elements may still use low-resolution or cached textures. For reliable high-resolution output, use the store-level export methods (`store.toDataURL`, `store.saveAsImage`) instead.
> **Note**: This function is experimental and marked as `unstable_`. It may change in future versions. Use with caution in production code.
The canvas can be used for further processing, conversion to images, or custom export workflows.
## `polotno/utils/measure-text` [#polotnoutilsmeasure-text]
Measure text dimensions to check overflow or calculate dynamic layouts.
### `measureText(options)` [#measuretextoptions]
Calculates how much space text will occupy given specific styling properties. Useful for checking if text will overflow a container or for dynamic layout calculations.
```ts
import { measureText } from 'polotno/utils/measure-text';
// Option 1: Pass a Polotno text element
const textElement = store.selectedElements[0];
const result = await measureText(textElement);
// Option 2: Pass text properties directly
const result = await measureText({
text: 'Hello World',
width: 200,
fontSize: 16,
fontFamily: 'Arial',
richTextEnabled: true, // optional, defaults to global setting
});
console.log(`Text height: ${result.height}px`);
```
Returns a Promise resolving to `{ height: number }`. The function automatically uses HTML rendering if `richTextEnabled` is true (or uses the global setting if not specified), otherwise falls back to canvas-based measurement.
> **Note**: This function is experimental and may change in future versions.
## `polotno/pages-timeline/page-preview` [#polotnopages-timelinepage-preview]
Hook for building your own pages timeline / thumbnail strip without re-implementing preview rendering.
### `usePagePreview({ page, ref })` [#usepagepreview-page-ref-]
React hook that returns a thumbnail data URL for a single page. It automatically tracks whether your container is visible, queues the render alongside other previews, and re-renders when the page content changes.
```tsx
import * as React from 'react';
import { usePagePreview } from 'polotno/pages-timeline/page-preview';
const PageThumb = ({ page }) => {
const ref = React.useRef(null);
const preview = usePagePreview({ page, ref });
return (
{preview ? : null}
);
};
```
Pass the page object from `store.pages` and a ref attached to the element that should be observed for visibility. The hook returns `null` until the first render completes, then a `data:` URL of the page thumbnail.
Use this when the default `` component doesn't fit your UI and you want to render your own page navigator with the same lazy/queued thumbnail behavior.
## `polotno/utils/l10n` [#polotnoutilsl10n]
Localization utilities for translating UI strings.
### `t(key)` [#tkey]
Translates a localization key to the current language string.
```tsx
import { t } from 'polotno/utils/l10n';
const App = () => {
return
{t('sidePanel.yourLabel')}
;
};
```
Use this in custom components to access Polotno's translation system. See [Editor Configuration](/docs/editor-configuration) for details on setting up translations.
---
# Angular
URL: https://polotno.com/docs/angular
## How to integrate Polotno editor into an Angular app? [#how-to-integrate-polotno-editor-into-an-angular-app]
Polotno is a React-based editor. You can embed it inside an Angular application by rendering a small React component into an Angular host element.
### 1) Install dependencies [#1-install-dependencies]
```bash
npm install react react-dom polotno
```
Optionally, install MobX bindings for Angular to react to Polotno store changes from Angular templates:
```bash
npm install mobx-angular
```
### 2) Create the Polotno Editor as a React component [#2-create-the-polotno-editor-as-a-react-component]
Create `polotno/editor.tsx` and bootstrap the editor. Replace `YOUR_API_KEY` with your actual key from the Polotno cabinet.
```tsx
// polotno/editor.tsx
import React from 'react';
import { PolotnoContainer, SidePanelWrap, WorkspaceWrap } from 'polotno';
import { Toolbar } from 'polotno/toolbar/toolbar';
import { ZoomButtons } from 'polotno/toolbar/zoom-buttons';
import { SidePanel } from 'polotno/side-panel';
import { Workspace } from 'polotno/canvas/workspace';
import { createStore } from 'polotno/model/store';
// Polotno UI styles
import 'polotno/ui.css';
export const store = createStore({
key: 'YOUR_API_KEY',
showCredit: true,
});
store.addPage();
export const App = () => {
return (
);
};
```
### 3) Mount the React editor from an Angular component [#3-mount-the-react-editor-from-an-angular-component]
Use Angular lifecycle hooks to create and unmount a React root. Expose the Polotno `store` to the template if you want to show reactive data.
```ts
// app.component.ts
import { Component, OnDestroy, AfterViewInit } from '@angular/core';
import * as ReactDOM from 'react-dom/client';
import * as React from 'react';
import { App as ReactEditor, store } from './polotno/editor';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
standalone: true,
})
export class AppComponent implements AfterViewInit, OnDestroy {
root: ReactDOM.Root | undefined;
store = store; // make store available in template
ngAfterViewInit() {
const container = document.getElementById('editor')!;
this.root = ReactDOM.createRoot(container);
this.renderComponent();
}
private renderComponent() {
this.root!.render(React.createElement(ReactEditor));
}
ngOnDestroy() {
this.root?.unmount();
}
}
```
Make sure the component template has a container for the editor:
```html
```
### 4) (Optional) Setup MobX reactions [#4-optional-setup-mobx-reactions]
The `mobx-angular` library can automatically bind Polotno store updates to Angular templates. Add the module to your component (or app module) and use `*mobxAutorun`.
```ts
// app.component.ts (standalone component example)
import { Component } from '@angular/core';
import { MobxAngularModule } from 'mobx-angular';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
standalone: true,
imports: [MobxAngularModule],
})
export class AppComponent {}
```
```html
Number of pages: {{ store.pages.length }}
```
### Customization [#customization]
From `polotno/editor.tsx` you can customize panels, toolbar, and canvas. From Angular, interact with the exported `store` to load a design, modify pages/elements, and export results (images/PDF). See other guides in this section for theming and configuration.
## Live demo [#live-demo]
---
# Build with AI
URL: https://polotno.com/docs/build-with-ai
Polotno ships official tooling for AI coding assistants: **skills** that teach your agent the SDK, an **MCP server** for live documentation search, and **machine-readable docs**. Set these up before writing code — your assistant will know Polotno's APIs, patterns, and pitfalls instead of guessing.
## Install skills [#install-skills]
Skills are lightweight instruction packs your agent loads on demand. Polotno provides two in the [polotno-project/skills](https://github.com/polotno-project/skills) repository:
* **polotno-sdk** — SDK development assistant: core API patterns, a capability map of everything the SDK provides, and live documentation lookup.
* **polotno-design** — headless design generation: produce a Polotno JSON design from a text brief, render it to PNG/PDF, and bulk-generate variants from data.
### Claude Code [#claude-code]
Installs both skills plus the documentation MCP server:
```bash
claude plugin marketplace add polotno-project/skills
claude plugin install polotno@polotno
```
Or from within a Claude Code session:
```
/plugin marketplace add polotno-project/skills
/plugin install polotno@polotno
```
### Cursor, Copilot, Codex, Windsurf, Cline, and others [#cursor-copilot-codex-windsurf-cline-and-others]
```bash
npx skills add polotno-project/skills
```
The [skills CLI](https://github.com/vercel-labs/skills) auto-detects which agents you have and installs the skills in each one's native format.
### Manual setup [#manual-setup]
Copy the skill content from the repo into your agent's instructions file — `skills/polotno-sdk/SKILL.md` (with its `capabilities.md`) for SDK development:
| Platform | File |
| -------------- | --------------------------------- |
| Cursor | `.cursorrules` |
| GitHub Copilot | `.github/copilot-instructions.md` |
| Codex / Jules | `AGENTS.md` |
| Windsurf | `.windsurfrules` |
## Live documentation search (MCP) [#live-documentation-search-mcp]
The Polotno skills are designed to look up API details in the live docs rather than carry stale copies. The Claude Code plugin above already includes the MCP server. For any other MCP-capable agent, add it to your MCP config:
```json
{
"mcpServers": {
"polotno-documentation": {
"command": "npx",
"args": [
"crawl-chat-mcp",
"--id=67e312247a822a2303f2b8a7",
"--name=polotno_documentation"
]
}
}
}
```
The server uses stdio transport and indexes both the documentation and the [community forum](https://community.polotno.com/home). Works with Cursor, Cline, and any MCP-compatible editor. Requires Node.js 18+.
The same search powers the AI assistant widget on this docs site and on the community forum — ask questions like "How do I resize a page?" for instant cited answers.
## Machine-readable docs [#machine-readable-docs]
* **Markdown version of any docs page** — append `.md` to the URL, e.g. [`/docs/store.md`](https://polotno.com/docs/store.md). Clean markdown, no navigation or scripts.
* [llms.txt](https://polotno.com/llms.txt) — an LLM-oriented index of Polotno: core concepts, product pages, and documentation links.
* [llms-full.txt](https://polotno.com/llms-full.txt) — the complete documentation as a single markdown document, for agents that want everything in one request.
* [AI info page](https://polotno.com/ai-info-page) — a condensed product brief for AI assistants.
## Alternatives [#alternatives]
[Context7](https://context7.com) also serves up-to-date Polotno documentation via MCP:
```bash
npx ctx7 setup
```
Use flags to target a specific editor: `--cursor`, `--claude`, `--opencode`. A free API key from [context7.com/dashboard](https://context7.com/dashboard) is recommended for higher rate limits.
## What you can ask [#what-you-can-ask]
Once set up, your assistant can answer with sources:
* **API questions** – "What properties does TextElement have?"
* **Code examples** – "How to add a rounded rectangle?"
* **Troubleshooting** – "Why isn't my custom font loading?"
---
# Removing "Powered by Polotno"
URL: https://polotno.com/docs/credit
## Requirements [#requirements]
**Trial keys always show the "Powered by Polotno" credit.** This branding cannot be removed with trial keys, regardless of configuration.
To remove the credit, you must have a **paid license**.
## How to Remove [#how-to-remove]
Follow all three steps:
### 1. Purchase a Paid License [#1-purchase-a-paid-license]
Get a paid license from your [Polotno dashboard](https://polotno.com/login). Trial keys will not work.
### 2. Register Your Domain [#2-register-your-domain]
Add your domain in your [Polotno dashboard](https://polotno.com/login). The API key validates against registered domains.
**Important:** Register the exact domain including subdomain (e.g., `app.example.com` vs `example.com`).
### 3. Use the Correct API Key and Configuration [#3-use-the-correct-api-key-and-configuration]
This is **critical**. You must use the API key from your paid license and set `showCredit: false`:
```ts
import { createStore } from 'polotno/model/store';
const store = createStore({
key: 'YOUR_API_KEY', // Must be from your paid license, not a trial key
showCredit: false,
});
```
For frameworkless integrations:
```ts
import { createDemoApp } from 'polotno/polotno-app';
const { store } = createDemoApp({
container: document.getElementById('root'),
key: 'YOUR_API_KEY',
showCredit: false,
});
```
## Troubleshooting [#troubleshooting]
If the credit still appears:
* **Verify your API key** - Confirm you're using the key from your paid license (check in your [Polotno dashboard](https://polotno.com/login))
* **Check domain registration** - Ensure your exact domain is listed in the dashboard
* **Confirm `showCredit: false`** - Make sure it's explicitly set in your configuration
* **Clear cache** - Hard refresh your browser (Cmd+Shift+R or Ctrl+Shift+R)
The most common issue is using a trial API key instead of a paid license key.
---
# Customizations
URL: https://polotno.com/docs/customizations
First, let's talk about the user interface.
The Polotno editor has several main components:
## Full Control of the Side Panel [#full-control-of-the-side-panel]
You have complete control over the Side Panel. You can remove the default panels, add your own, and customize their content entirely. For instance, you can create a panel that loads images directly from your backend. Check out the [Side Panel Overview](/docs/side-panel-overview) for more information.
## Toolbar, Page Controls, and Tooltip Customization [#toolbar-page-controls-and-tooltip-customization]
You can modify, remove, or add controls and buttons in the [Toolbar](/docs/toolbar), [Page Controls](/docs/workspace), and [Tooltip](/docs/tooltip), allowing you to tailor the interface to your needs.
## Timeline Customization [#timeline-customization]
Currently, the **Timeline** component is not customizable. However, you can remove it and implement your own version using the [Store API](/docs/store).
## Canvas Customization [#canvas-customization]
The Canvas component offers several style customization options. You can modify properties like background color and selection tool colors. For more options, refer to the [Workspace Documentation](/docs/workspace).
## General Style Customization [#general-style-customization]
You can also use [CSS to overwrite](/docs/theme-and-ui-styles) and customize the general styles of the editor.
## Data Loading and Export [#data-loading-and-export]
You have full control over how data is loaded into the editor, saved, and exported.
For example:
* Load custom templates or a specific user's design from your backend.
* Save a JSON file to a local computer with a button click.
* Automatically send requests to your backend with design data on any changes.
* Upload design previews or exports to an S3 bucket.
The [Store API](/docs/store) allows you to implement all of these features, ensuring your data flow fits seamlessly with your infrastructure.
## Building custom UI with `polotno/primitives` [#building-custom-ui-with-polotnoprimitives]
When you add your own panels, toolbar buttons, or dialogs, you can build them with the same components the editor uses, so they match the default UI out of the box. They're exposed at `polotno/primitives`.
```tsx
import { Button, Input, Select, Popover, Tooltip, Slider, Tabs } from 'polotno/primitives';
const MyPanel = () => (
);
```
Available primitives include `Button`, `Input`, `Textarea`, `Select`, `Popover`, `Tooltip`, `Slider`, `Tabs`, `Dialog`, `Switch`, `NumericInput`, and more. They follow the editor's theme, including [dark mode](/docs/theme-and-ui-styles).
## Overriding icons [#overriding-icons]
You can swap any editor icon (or a whole pack) by semantic name with `setIcons`. See [Editor Configuration → How to override icons?](/docs/editor-configuration#how-to-override-icons).
---
# Editor configuration
URL: https://polotno.com/docs/editor-configuration
## How to change image upload behavior? [#how-to-change-image-upload-behavior]
The default [Side Panel](/docs/side-panel-overview) has an Upload tab to import local images into the project. The same upload function also handles images and files pasted from the clipboard onto the canvas. By default, Polotno converts a local file into a Base64 string. These strings are then used by `image` elements. Using Base64 may produce large project JSON because images are fully encoded inside it.
**It is highly recommended to upload images to your server.** This will keep [JSON](/docs/import-and-export) smaller, easier to read, and significantly improve the canvas editor performance.
If you want to upload local images to your server, use:
```ts
import { setUploadFunc } from 'polotno/config';
// define your upload function (implement your API logic here)
async function upload(localFile: File) {
const formData = new FormData();
formData.append('files[]', localFile);
const res = await fetch(yourServerURL, {
method: 'POST',
body: formData,
});
const json = await res.json();
const { url } = json;
// return a simple and short URL
return url as string;
}
// set new function
setUploadFunc(upload);
```
Similar to image uploads, you can customize font uploading behavior in the Text panel:
```ts
import { setFontUploadFunc } from 'polotno/config';
// define your upload function (implement your API logic here)
async function upload(localFile: File) {
const formData = new FormData();
formData.append('files[]', localFile);
const res = await fetch(yourServerURL, {
method: 'POST',
body: formData,
});
const json = await res.json();
const { url } = json;
// return a simple and short URL
return url as string;
}
// set new function
setFontUploadFunc(upload);
```
## How to translate UI? [#how-to-translate-ui]
If you would like to translate the UI into a different language or change some labels, use:
```ts
import { setTranslations } from 'polotno/config';
setTranslations(
{
sidePanel: {
text: 'Текст',
myFonts: 'Мои шрифты',
},
},
{
validate: true, // set to false if you don't need validation
}
);
```
To inspect the full translation schema:
```ts
import { getTranslations } from 'polotno/config';
// log full translations object
console.log(getTranslations());
```
You can also use the same translation API to translate your custom components:
```tsx
import { observer } from 'mobx-react-lite';
import { t } from 'polotno/utils/l10n';
// in your component
const App = observer(() => {
return
{t('sidePanel.yourLabel')}
; // sidePanel.yourLabel is a key in translations object
});
```
If you are working on a translation for any language or changing default labels, please share your results—it helps us improve defaults and provide ready-to-use translations.
## How to override icons? [#how-to-override-icons]
You can replace any editor icon (or a whole pack) by semantic name with `setIcons`. It mirrors the localization pattern: it shallow-merges by key, so call it repeatedly to override more icons, or pass a whole set to restyle the editor. Pass any React component that forwards `className`.
```tsx
import { setIcons } from 'polotno/config';
import { MyBoldIcon, MyItalicIcon } from './my-icons';
setIcons({
bold: MyBoldIcon,
italic: MyItalicIcon,
});
```
## How to configure fonts? [#how-to-configure-fonts]
See the dedicated [Fonts guide](/docs/fonts) for comprehensive documentation on:
* Google Fonts configuration
* Design fonts (per-design custom fonts stored in JSON)
* Global fonts (app-wide fonts not stored in JSON)
* Font upload customization
## Change default preset colors in color picker [#change-default-preset-colors-in-color-picker]
By default, the color picker generates preset colors from (1) used colors in the design and (2) default colors. You can override this logic with your own presets:
```ts
import { setColorsPresetFunc } from 'polotno/config';
// define your own function
setColorsPresetFunc((store) => {
// return array of colors
return ['#000', '#fff'];
});
```
---
# Frameworkless Integration
URL: https://polotno.com/docs/frameworkless-integration
**Polotno** is optimized for integration with the React framework.
However, for scenarios requiring no customization, a “frameworkless” version of Polotno is available. This version can be easily included on a page with simple script tags.
```html
```
---
# iOS Swift
URL: https://polotno.com/docs/ios-swift
## How to integrate Polotno editor into an iOS app? [#how-to-integrate-polotno-editor-into-an-ios-app]
Polotno is a React-based web editor. You can embed it into a native iOS Swift application by bundling the editor as a single-file web app and loading it in a `WKWebView`. This hybrid approach lets you leverage Polotno's full web capabilities while maintaining a native iOS experience.
## Architecture overview [#architecture-overview]
The integration uses a hybrid architecture:
* **Native Layer**: SwiftUI app with `WKWebView` to host the editor
* **Web Layer**: React + Vite app that runs Polotno editor
* **Communication**: JavaScript bridge via `WKScriptMessageHandler` for bidirectional data exchange
The web editor is built as a single-file bundle (using `vite-plugin-singlefile`) to avoid module loading issues when loading from local files in `WKWebView`.
## Prerequisites [#prerequisites]
* Xcode 15+ (iOS 16+ deployment target)
* Node.js 18+ and npm (for building the web editor)
## Integration approach [#integration-approach]
The integration involves two main parts:
1. **Web editor**: Create a React + Vite app with Polotno, configured to build as a single-file bundle using `vite-plugin-singlefile`. This generates one HTML file with all JavaScript and CSS inlined.
2. **Swift wrapper**: Create a `WKWebView` wrapper in Swift that loads the bundled HTML file and handles bidirectional communication via `WKScriptMessageHandler`.
The web editor exposes global functions (e.g., `window.__polotnoReceiveInitialDoc`, `window.saveToNative`) that Swift can call. The editor posts messages back to Swift using `window.webkit.messageHandlers.editor.postMessage()`.
See the [demo repository](https://github.com/polotno-project/polotno-swift) for complete implementation details.
## Core concepts [#core-concepts]
### Single-file bundle [#single-file-bundle]
The editor must be built as a single HTML file with all JavaScript and CSS inlined. This prevents:
* Module loading errors from local file restrictions
* Cross-origin issues with separate asset files
* Network dependency during initial load
Use `vite-plugin-singlefile` to achieve this.
### Message passing [#message-passing]
Communication between Swift and JavaScript uses `WKScriptMessageHandler`:
* **Swift → JavaScript**: Use `webView.evaluateJavaScript()` to call global functions
* **JavaScript → Swift**: Use `window.webkit.messageHandlers.editor.postMessage()` to send data
### Data serialization [#data-serialization]
Design data is passed as JSON strings, base64-encoded for safe transmission. Preview images are base64-encoded PNG data URLs.
## FAQ [#faq]
### Can the editor work offline? [#can-the-editor-work-offline]
The editor code is bundled locally, but Polotno relies on external APIs for stock photos, templates, and fonts. The device must be online for these features.
**Enterprise clients**: Full offline support (bundling all assets and fonts) is available with enterprise licenses. Contact support for details.
### What are the memory considerations? [#what-are-the-memory-considerations]
High-resolution exports or complex designs may consume significant memory. Test on older devices (iPhone 8, iPad Air 2) if targeting a broad user base. Consider:
* Limiting export resolution for previews
* Using lower pixel ratios for preview generation
* Implementing memory warnings and cleanup
* Rendering on the server side
### How do I customize the editor UI? [#how-do-i-customize-the-editor-ui]
Modify the React component in `web-editor/src/App.jsx`:
* Add custom side panels
* Modify toolbar actions
* Change default sections
* Add custom save flows
### Can I export to PDF or other formats? [#can-i-export-to-pdf-or-other-formats]
Yes. Extend the JavaScript bridge to support additional export formats. Use `store.toPDF()` or other export methods, convert the result to base64, and post it to Swift via the message handler.
### How do I handle errors? [#how-do-i-handle-errors]
Implement error handling in both layers:
* **JavaScript**: Wrap Polotno API calls in try-catch blocks
* **Swift**: Validate message payloads and handle decoding errors gracefully
## Demo repository [#demo-repository]
See a complete working example with source code: [https://github.com/polotno-project/polotno-swift](https://github.com/polotno-project/polotno-swift)
The repository includes:
* Complete Xcode project
* React + Vite web editor setup
* SwiftUI integration example
* Build scripts for bundling
---
# Next.js
URL: https://polotno.com/docs/next-js
## How to use Polotno Design editor with Next.js? [#how-to-use-polotno-design-editor-with-nextjs]
Demo repository: [polotno-next](https://github.com/polotno-project/polotno-next)
### 1) Install dependencies [#1-install-dependencies]
```bash
## Next.js 15 or 16 (React 19):
npm install polotno
## Next.js 14 (React 18):
npm install polotno@next-react18
```
The default 4.x line targets React 19 (Next.js 15/16). The 3.x line for React 18 (Next.js 14) is legacy and available for now — see [React Version Support](/docs/react-19-support).
### 2) Create an Editor component (client-only) [#2-create-an-editor-component-client-only]
Create `components/editor.tsx`. Do not place it inside `pages` or `app` to avoid server-side rendering.
```tsx
import React from 'react';
import { PolotnoContainer, SidePanelWrap, WorkspaceWrap } from 'polotno';
import { Toolbar } from 'polotno/toolbar/toolbar';
import { PagesTimeline } from 'polotno/pages-timeline';
import { ZoomButtons } from 'polotno/toolbar/zoom-buttons';
import { SidePanel } from 'polotno/side-panel';
import { Workspace } from 'polotno/canvas/workspace';
import 'polotno/ui.css';
import { createStore } from 'polotno/model/store';
const store = createStore({
key: 'YOUR_API_KEY', // create an API key at https://polotno.com/cabinet/
// you can hide back-link on a paid license
// but it will be good if you can keep it for Polotno project support
showCredit: true,
});
store.addPage();
export const Editor = () => {
return (
);
};
export default Editor;
```
### 3) Use dynamic import in a client component [#3-use-dynamic-import-in-a-client-component]
```tsx
'use client';
import dynamic from 'next/dynamic';
const Editor = dynamic(() => import('../components/editor'), {
ssr: false,
});
export default function Home() {
return ;
}
```
## Disable ReactStrictMode in Next.js 15 or 16 [#disable-reactstrictmode-in-nextjs-15-or-16]
Disable ReactStrictMode in your `next.config.js` file:
```js
module.exports = {
reactStrictMode: false,
};
```
At the current moment Polotno does not support ReactStrictMode in Next.js 15 or 16. It is a known issue and we are working on it.
## Live demo [#live-demo]
---
# Non-React integration
URL: https://polotno.com/docs/non-react-integration
**Polotno** is designed for use with the React framework, requiring a React environment for customization. However, you can still integrate Polotno with other frameworks, such as Vue, Angular, and Svelte. Here is how:
1. Start by creating an isolated project for the editor component.
2. Use Polotno to build and customize the editor as needed.
3. Develop a set of API methods to facilitate communication with the primary project.
4. Compile the project into basic source files, eliminating the need for JSX or transpilers.
5. Incorporate the compiled bundle into the main project.
It is important to note that React proficiency remains essential for customizing the Polotno editor. For demonstration purposes, this guide uses the Parcel bundler.
* Example repository: [polotno-non-react-integration](https://github.com/polotno-project/polotno-docs/tree/main/examples/polotno-non-react-integration)
* Parcel: [parceljs.org](https://parceljs.org/)
### 1. Installations [#1-installations]
First install Parcel, Polotno and React:
```bash
npm install parcel polotno react react-dom
```
### 2. Create project [#2-create-project]
Create a new folder for the editor inside your project root, for example `editor`:
```bash
mkdir editor
```
### 3. Create index.html [#3-create-indexhtml]
Create `editor/index.html`. It will be a template for your editor during isolated development.
Note: this HTML file is not used by your main app; it is only for local development of the editor bundle.
```html
```
### 4. Create index.js [#4-create-indexjs]
Create `editor/index.js`. It is the main entry point for your editor.
```js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { PolotnoContainer, SidePanelWrap, WorkspaceWrap } from 'polotno';
import { Toolbar } from 'polotno/toolbar/toolbar';
import { ZoomButtons } from 'polotno/toolbar/zoom-buttons';
import { SidePanel } from 'polotno/side-panel';
import { Workspace } from 'polotno/canvas/workspace';
import { createStore } from 'polotno/model/store';
const store = createStore({
key: 'nFA5H9elEytDyPyvKL7T', // you can create it here: https://polotno.com/cabinet/
// you can hide back-link on a paid license
// but it will be good if you can keep it for Polotno project support
showCredit: true,
});
const page = store.addPage();
export const App = ({ store }) => {
return (
);
};
// default API of your editor
export const createEditor = ({ container }) => {
const root = ReactDOM.createRoot(container);
root.render();
};
// make API global for simple start in development
window.createEditor = createEditor;
```
### 5. Start development server [#5-start-development-server]
```bash
parcel ./editor/index.html
```
Tip: you can also add this command to your `package.json`:
```json
{
"scripts": {
"dev": "parcel ./editor/index.html"
}
}
```
### 6. Open editor in browser [#6-open-editor-in-browser]
Open `http://localhost:1234` in your browser. That URL is default for Parcel, but it may use a different port—check your terminal output.
### 7. Compile editor [#7-compile-editor]
First tell Parcel where to put the compiled code. Add this line to your `package.json`:
```json
{
"main": "editor.js"
}
```
When you are happy with your editor, compile it into plain JS files:
```bash
parcel build ./editor/index.js --no-source-maps
```
You can also add a build script to `package.json`:
```json
{
"scripts": {
"build:editor": "parcel build ./editor/index.js --no-source-maps"
}
}
```
### 8. Use editor in your project [#8-use-editor-in-your-project]
Now you can use your editor in your project. Add the generated `editor.js` file into your app and import its API:
```js
import { createEditor } from './editor.js';
// somewhere in your code
createEditor({ container: document.getElementById('editor') });
```
Remember to include the Polotno UI styles in your host app:
```html
```
### 9. API [#9-api]
The `createEditor` method shown here is just a simple example. You can extend it with methods to communicate with your main project, pass initial data, and define callbacks.
### Vue example [#vue-example]
[Open Vite + Vue + Polotno example](https://codesandbox.io/s/github/polotno-project/polotno-docs/tree/main/examples/polotno-and-vue-custom?file=/src/components/Editor.vue)
### 10. Pitfalls [#10-pitfalls]
Installing all editor dependencies may conflict with your main project. If that happens, move the whole editor workflow into its own `package.json` inside the `editor` folder and install dependencies there.
---
# Overview
URL: https://polotno.com/docs/overview
**Polotno** is an opinionated JavaScript library and set of React components designed to create canvas editors tailored for various business applications. The SDK provides a complete visual editor framework for building white-label design tools, template systems, and programmatic image generation.
There are many powerful JavaScript frameworks and tools that can help you make a canvas editor. But almost all of them are 'low-level' in nature. Take [Konva.js](https://konvajs.org/) for example – it's a great 2d canvas framework; but you'd need to write **a lot of code** to make a full canvas editor app.
Polotno is a simple tool that solves a focused set of business problems. It is fast and we are committed to keeping the API as light as possible. So you can just drop it on the page and get a full featured design suite.
**Using an AI coding assistant?** Install the official [Polotno skills and docs MCP server](/docs/build-with-ai) for Claude Code, Cursor, Codex, and other agents first — your assistant will know the SDK before you write a line of code.
## Quick Start (React) [#quick-start-react]
### 1. Install polotno package [#1-install-polotno-package]
```bash
npm install polotno
```
This installs the default **4.x** line, which targets **React 19**. Still on React 18? A legacy 3.x line is available for now — see [React Version Support](/docs/react-19-support).
### 2. Create Editor component [#2-create-editor-component]
```tsx
import React from 'react';
import { PolotnoContainer, SidePanelWrap, WorkspaceWrap } from 'polotno';
import { Toolbar } from 'polotno/toolbar/toolbar';
import { PagesTimeline } from 'polotno/pages-timeline';
import { ZoomButtons } from 'polotno/toolbar/zoom-buttons';
import { SidePanel } from 'polotno/side-panel';
import { Workspace } from 'polotno/canvas/workspace';
import 'polotno/ui.css';
import { createStore } from 'polotno/model/store';
const store = createStore({
key: 'YOUR_API_KEY', // you can create it here: https://polotno.com/cabinet/
// you can hide back-link on a paid license
// but it will be good if you can keep it for Polotno project support
showCredit: true,
});
const page = store.addPage();
export const Editor = () => {
return (
);
};
```
### 3. Use the component in your app [#3-use-the-component-in-your-app]
```tsx
import { Editor } from './editor';
const App = () => {
return
}
```
## Quick Start (No Frameworks) [#quick-start-no-frameworks]
```javascript
import { createDemoApp } from 'polotno/polotno-app';
// import Polotno UI styles
// if your bundler doesn't support such an import, you can use the CSS from a CDN (see below)
import 'polotno/ui.css';
const { store } = createDemoApp({
container: document.getElementById('root'),
key: 'YOUR_API_KEY', // you can create it here: https://polotno.com/cabinet/
// you can hide back-link on a paid license
// but it will be good if you can keep it for Polotno project support
showCredit: true,
});
```
Optionally, you can import CSS styles from CDN:
```html
```
## Core Concepts [#core-concepts]
### Store [#store]
The [Store](/docs/store) is the main object to control data and elements of the canvas. It provides API for adding/updating/removing canvas objects, undo/redo, selection changes, zooming, and [export/import operations](/docs/import-and-export).
```javascript
import { createStore } from 'polotno/model/store';
const store = createStore();
const page = store.addPage();
page.addElement({
x: 50,
y: 50,
type: 'text',
fill: 'black',
text: 'hello',
});
```
### Workspace React Canvas Component [#workspace-react-canvas-component]
React component for drawing canvas app on the page; comes with basic functionality for common edits: selection, resize, actual drawing of objects, snapping, etc.
```tsx
import Workspace from 'polotno/canvas/workspace';
const App = () => {
return ;
};
```
### UI Components [#ui-components]
Set of React components for general canvas editor app.
* a toolbar for basic objects manipulations actions (such as align, remove, change objects styles, etc)
* side panels for adding new objects
* zooming buttons
* etc
```tsx
import { PolotnoContainer, SidePanelWrap, WorkspaceWrap } from 'polotno';
import { Toolbar } from 'polotno/toolbar/toolbar';
import { ZoomButtons } from 'polotno/toolbar/zoom-buttons';
import { SidePanel } from 'polotno/side-panel';
import { Workspace } from 'polotno/canvas/workspace';
export const App = ({ store }) => {
return (
);
};
```
### Styles [#styles]
The default Polotno UI is built on its own design system. Import its styles once with `import 'polotno/ui.css'`. If you build your own custom UI or additional components, you can reuse Polotno's building blocks from [`polotno/primitives`](/docs/customizations) so they match the editor, or use any style framework you prefer.
### Reactivity [#reactivity]
The core of Polotno is built using the [MobX](https://mobx.js.org/) library. You can leverage MobX API to add reactivity to your own application. The foundation of Polotno is built with the [MobX](https://mobx.js.org/) library. To incorporate reactivity into your application, you can utilize the MobX API along with the [mobx-state-tree](https://mobx-state-tree.js.org/intro/welcome) library. Within the React framework, the `observer()` function is essential.
```tsx
import { observer } from 'mobx-react-lite';
const App = observer(({ store }) => {
return (
Here we will define our own custom tab.
Elements on the current page: {store.activePage?.children.length}
);
});
```
---
# Polotno 4 Migration
URL: https://polotno.com/docs/polotno-4-migration
Starting with Polotno **3.x (React 18)** and **4.x (React 19)**, the editor ships with a **new default UI** built on its own design system. The previous [Blueprint](https://blueprintjs.com/)-based chrome still ships under `polotno/blueprint/*`, so you can either adopt the new UI or keep the old look.
Pick your version line first: **4.x** targets React 19 (`npm i polotno`), **3.x** targets React 18 (`npm i polotno@next-react18`). See [React Version Support](/docs/react-19-support).
## Path A — Adopt the new UI (recommended) [#path-a--adopt-the-new-ui-recommended]
The component API is unchanged, so this is mostly a CSS swap.
### 1. Swap the CSS import [#1-swap-the-css-import]
```js
// Before
import '@blueprintjs/core/lib/css/blueprint.css';
// After
import 'polotno/ui.css';
```
### 2. Imports stay the same [#2-imports-stay-the-same]
The default `Toolbar`, `SidePanel`, `PagesTimeline`, and `Workspace` are already the new UI — your imports don't change:
```tsx
import { Toolbar } from 'polotno/toolbar/toolbar';
import { SidePanel } from 'polotno/side-panel';
import { PagesTimeline } from 'polotno/pages-timeline';
import { Workspace } from 'polotno/canvas/workspace';
```
### 3. Update dark mode [#3-update-dark-mode]
Dark mode moved to the `dark` class (see [Theme](/docs/theme-and-ui-styles)):
```html
```
### 4. Custom UI [#4-custom-ui]
If you built custom panels, toolbar buttons, or dialogs, rebuild them with [`polotno/primitives`](/docs/customizations) so they match the new design system.
## Path B — Keep the Blueprint UI [#path-b--keep-the-blueprint-ui]
If you're not ready to change the look, import the editor chrome from `polotno/blueprint/*`.
### 1. Install Blueprint v6 [#1-install-blueprint-v6]
The Blueprint chrome relies on Blueprint v6 as a peer dependency (`@blueprintjs/select` is now required), so install all three packages — the same ones your own Blueprint components use:
```bash
npm i @blueprintjs/core@6 @blueprintjs/icons@6 @blueprintjs/select@6
```
### 2. Import the chrome from `blueprint/*` [#2-import-the-chrome-from-blueprint]
`Workspace`, `model/store`, `config`, and `utils/*` stay on their normal paths — only the toolbar, side panel, and timeline change.
```js
// Before
import { Toolbar } from 'polotno/toolbar/toolbar';
import { SidePanel } from 'polotno/side-panel';
import { ImagesGrid } from 'polotno/side-panel/images-grid';
import { PagesTimeline } from 'polotno/pages-timeline';
// After
import { Toolbar } from 'polotno/blueprint/toolbar';
import { SidePanel, ImagesGrid } from 'polotno/blueprint/side-panel';
import { PagesTimeline } from 'polotno/blueprint/pages-timeline';
```
### 3. CSS [#3-css]
`polotno/blueprint.css` bundles the Blueprint base styles and the Polotno chrome in one file.
```js
// Before
import '@blueprintjs/core/lib/css/blueprint.css';
// After
import 'polotno/blueprint.css';
```
### 4. Dark mode → `bp6-dark` [#4-dark-mode--bp6-dark]
The Blueprint chrome is on Blueprint v6, which renamed its CSS prefix from `bp5-` to `bp6-`. Update any hardcoded class strings in your CSS, JSX `className`s, and `index.html` — including the dark-mode class:
```html
```
This is **not** a build error — the theme just silently stops applying. (`Classes.*` constants from `@blueprintjs/core` resolve automatically; only hardcoded `bp5-` strings need changing.)
The `` canvas overlays (page controls, tooltip) and the zoom buttons use the new UI — Blueprint-styled variants of those aren't exposed as separate imports in this version, so keep `Workspace` and `ZoomButtons` on their default paths.
The Blueprint chrome is deprecated and will be removed in a future release. When you're ready, drop it and rebuild custom UI with [`polotno/primitives`](/docs/customizations).
---
# Polotno Button
URL: https://polotno.com/docs/polotno-button
**Polotno Button** is a dedicated script that enables easy and efficient embedding of the Polotno Editor to any website. With just a simple button, you can give your app a new dimension of customization for any design.
## Why Use Polotno Button? [#why-use-polotno-button]
There are several key reasons why implementing a Polotno button can significantly benefit your website and business.
1. **User-Friendly Designing:** Online design tools, such as Polotno, push the boundaries of creativity with a precise and user-friendly UI, enabling even non-professionals to create compelling designs. By incorporating the Polotno button into your application, you give your users the tools they need to go beyond traditional design boundaries.
2. **Seamless Integration:** The Polotno button can be seamlessly incorporated into your website without heavy coding or complex functionality.
3. **Save Time and Resources:** With the Polotno button, users can effortlessly craft designs right within your platform. This can reduce the need for external design platforms or software, thereby saving time and resources.
4. **Boost Engagement:** Providing users with the ability to create and customize designs can lead to increased user engagement on your platform.
## How to integrate Polotno Button? [#how-to-integrate-polotno-button]
1. Add the Polotno Button script to your website.
```html
```
2. Create a button somewhere on your page.
```html
```
3. Initialize Polotno Button with your API key.
```html
```
## Polotno Button Options [#polotno-button-options]
The Polotno Button can be customized to fit your needs.
```javascript
function inchesToPx(inches) {
var dpi = 72; // dots per inch
return inches * dpi;
}
window.createPolotnoEditor({
key: 'my-api-key',
// select sections you want to show
sections: [
'templates',
'photos',
'text',
'elements',
'upload',
'background',
'layers',
'size',
],
defaultSection: 'photos', // what section open by default
// initial size of the canvas
width: inchesToPx(20),
height: inchesToPx(10),
// load template from json file
jsonUrl:
'https://api.polotno.com/templates/2021-10-25-instagram-post-sunday-reminder.json',
// load template from json object
json: {
// json object of the design
},
// change default text of the publish button
publishLabel: 'Save',
onPublish: ({ dataURL, json }) => {
document.getElementById('result').src = dataURL;
console.log('json', json);
},
// optionally you can react to any change in the editor
// for example temporary save design somewhere to restore it later via jsonUrl
// you may need to save JSON manually to your server
onChange: ({ json }) => {
console.log('json', json);
},
// you can pass initial uploads to the editor
// they will be shown in the upload section
uploads: [
{
url: 'https://example.com/image.jpg',
preview: 'https://example.com/image.jpg',
},
],
// you can enable or disable animations
// by default animations are disabled
// warning - editor can't export into video or gif yet
// you have to use Cloud API or your own server for that
animationsEnabled: true,
// also you can translate full UI, ask us for the list of keys
translations: {
sidePanel: {
templates: 'Шаблоны',
},
},
});
```
---
# Theme
URL: https://polotno.com/docs/theme-and-ui-styles
Styling Polotno Design Editor to match your design.
The default Polotno UI ships with its own design system. Load its styles once with `import 'polotno/ui.css'`. You can theme the editor in a few layers, from highest to lowest effort:
1. **Theme variables** — recolor the whole UI by setting a few CSS variables ([see below](#theming-with-css-variables)).
2. **Dark mode** — a single class toggle (below).
3. **Canvas chrome props** — recolor selection, guides, transformers, etc. via `` props ([see below](#theming-the-canvas-chrome)).
4. **CSS overrides** — target Polotno's stable `polotno-*` selectors for finer changes.
If you build custom UI, reuse the editor's own components from [`polotno/primitives`](/docs/customizations) so they stay consistent with the theme.
## Prebuilt themes [#prebuilt-themes]
Polotno ships ready-made themes as plain CSS files. Import one after `polotno/ui.css`:
```ts
import 'polotno/ui.css';
import 'polotno/themes/carbon.css'; // IBM Carbon look (IBM Plex fonts, light + dark)
// or
import 'polotno/themes/blueprint.css'; // classic Blueprint look of older Polotno versions
```
## Theming with CSS variables [#theming-with-css-variables]
The UI's colors, border radius, and font all come from CSS variables. Define them on `:root` (or any element that wraps the editor) to recolor it — you only need to set the ones you want to change; everything else keeps its default.
```css
:root {
/* main accent (primary buttons, active states) */
--primary: #6d28d9;
--primary-foreground: #ffffff;
/* base surface and text */
--background: #ffffff;
--foreground: #1a1a1a;
/* corner roundness and UI font */
--radius: 0.5rem;
--font-sans: 'Inter', sans-serif;
}
```
Values accept any CSS color (hex, `rgb()`, `oklch()`, etc.). If your app already defines a theme with these variables, the editor matches it automatically — no extra setup.
### All variables [#all-variables]
Each color also has a `-foreground` pair (the text/icon color drawn on top of it) where relevant.
| Variable | Controls |
| -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `--background` / `--foreground` | Base editor surface and default text |
| `--card` / `--card-foreground` | Raised surfaces — panels, cards |
| `--popover` / `--popover-foreground` | Popovers, dropdowns, menus |
| `--primary` / `--primary-foreground` | Main accent — primary buttons, active states |
| `--secondary` / `--secondary-foreground` | Secondary buttons and controls |
| `--muted` / `--muted-foreground` | Muted backgrounds and secondary text |
| `--accent` / `--accent-foreground` | Hover / selected highlight |
| `--destructive` / `--destructive-foreground` | Delete and other dangerous actions |
| `--border` | Borders and dividers |
| `--input` | Input field borders |
| `--ring` | Focus ring |
| `--sidebar`, `--sidebar-foreground`, `--sidebar-primary`, `--sidebar-accent`, `--sidebar-border`, `--sidebar-ring` (and `-foreground` pairs) | The side-panel tab rail |
| `--chart-1` … `--chart-5` | Color palette for chart elements |
| `--radius` | Corner roundness of controls and surfaces |
| `--font-sans` | UI font family |
To theme dark mode, set the variables under the `dark` class:
```css
.dark {
--primary: #a78bfa;
--background: #18181b;
--foreground: #fafafa;
}
```
### Isolating the change to the editor [#isolating-the-change-to-the-editor]
The plain variables above are shared — setting `--primary` on `:root` also affects any of your own UI that reads it. To re-theme **only** the editor without touching the rest of your app, set the same names with a `--pn-` prefix instead. They take priority over the plain variables for Polotno, and your app keeps its own values.
```css
:root {
--pn-primary: #6d28d9;
--pn-background: #ffffff;
--pn-radius: 0.5rem;
}
```
Every variable in the table has a `--pn-` counterpart (`--pn-primary`, `--pn-border`, `--pn-radius`, `--pn-font-sans`, …).
## How to enable dark theme? [#how-to-enable-dark-theme]
To enable dark mode, add the `dark` class to any editor container (for example, the `body`).
```html
```
Alternatively, set `data-polotno-theme="dark"` directly on a Polotno container — useful when you can't add a `dark` class to an ancestor.
```html
```
## How to change styles of side panel and toolbar? [#how-to-change-styles-of-side-panel-and-toolbar]
For finer changes, use CSS. Inspect the DOM to find class names. Polotno keeps a set of stable `polotno-*` selectors you can target (prefer these over utility classes, which can change between releases).
```css
/* overwrite colors for side-panel tabs */
.dark .polotno-side-tabs-container .polotno-side-panel-tab:hover,
.dark .polotno-side-tabs-container .polotno-side-panel-tab.active {
background-color: #c8c52d;
color: white;
}
```
## Live demo [#live-demo]
Pick a preset to recolor the editor live, toggle light/dark, and hit **Show code** to copy the CSS variables (with or without the `--pn-` prefix).
## Theming the canvas chrome [#theming-the-canvas-chrome]
All editor canvas chrome — selection rectangles, transformer handles, snap guides, distance guides, crop overlays, loading/error indicators, rulers, and the hover highlighter — is themed via props on the `` component. Each prop is optional and merges with the default.
Example: recolor the editor accent away from the default blue.
```tsx
```
See the [Workspace](/docs/workspace) page for the full list of canvas styling props.
> The legacy `setTransformerStyle`, `setHighlighterStyle`, `setInnerImageCropTransformerStyle`, and `setOuterImageCropTransformerStyle` globals from `polotno/config` do the same thing globally and are deprecated — prefer the per-Workspace props.
---
# Vue.js
URL: https://polotno.com/docs/vue-js
## How to integrate Polotno editor into a Vue app? [#how-to-integrate-polotno-editor-into-a-vue-app]
Polotno is a React-based editor. You can embed it inside a Vue application by rendering a small React component into a Vue container.
### 1) Install dependencies [#1-install-dependencies]
```bash
npm install react react-dom polotno
```
Optionally, install MobX bindings for Vue to react to Polotno store changes in Vue templates:
```bash
npm install mobx-vue-lite
```
### 2) Create the Polotno Editor as a React component [#2-create-the-polotno-editor-as-a-react-component]
Create `components/editor.tsx` and bootstrap the editor. You can customize this file later (side panels, toolbar, etc.). Replace `YOUR_API_KEY` with your actual key from the Polotno cabinet.
```tsx
// components/editor.tsx
import React from 'react';
import { PolotnoContainer, SidePanelWrap, WorkspaceWrap } from 'polotno';
import { Toolbar } from 'polotno/toolbar/toolbar';
import { ZoomButtons } from 'polotno/toolbar/zoom-buttons';
import { SidePanel } from 'polotno/side-panel';
import { Workspace } from 'polotno/canvas/workspace';
import { createStore } from 'polotno/model/store';
// Polotno UI styles
import 'polotno/ui.css';
export const store = createStore({
key: 'YOUR_API_KEY',
showCredit: true,
});
store.addPage();
export const App = () => {
return (
);
};
```
### 3) Mount the React editor from a Vue component [#3-mount-the-react-editor-from-a-vue-component]
In a Vue SFC (e.g., `App.vue`), render the React editor into a div. If you installed `mobx-vue-lite`, you can wrap your template with `Observer` to react to store updates.
```vue
This header is controlled by Vue. Pages: {{ store.pages.length }}
.
```
### 4) Customization [#4-customization]
From `components/editor.tsx` you can customize panels, toolbar, and canvas. From Vue, you can interact with the exported `store` to load a design, modify pages/elements, and export results (images/PDF). See other guides in this section for theming and configuration.
## Live demo [#live-demo]
---
# Cloud Render API
URL: https://polotno.com/docs/cloud-render-api
## What is Cloud Render API? [#what-is-cloud-render-api]
Using Polotno SDK, you can generate images directly on the client. But sometimes you need to generate images on the backend. For example, if you want to generate a 1,000 images with different text on it or if you want to simply offload rendering work from the client.
Polotno Cloud Render API is a managed rendering service that allows you to generate images, PDFs, and videos in the cloud without any backend infrastructure. You can use it to generate exports on the fly or to generate them in bulk for automated design workflows.
### Pricing [#pricing]
Cloud Render API is available for any subscribers at an additional [price](https://polotno.com/sdk/pricing).
## Before you start [#before-you-start]
(!) Important: all finished jobs will expire in 1 week. Such jobs will be deleted from the database and all render artifacts (images, PDF, videos, etc) are removed from file storage. Make sure to download the export result. If you need a persistent file store, please [contact us](https://polotno.com/contact).
## What does it look like? [#what-does-it-look-like]
### 1. Create render job [#1-create-render-job]
Send a POST request to schedule a rendering job.
```ts
const req = await fetch('https://api.polotno.com/api/renders?KEY=YOUR_API_KEY', {
method: 'POST',
headers: {
// it is important to set a json content type
'Content-Type': 'application/json',
},
body: JSON.stringify({
// polotno json from store.toJSON()
design: json,
// here you can pass other render options
pixelRatio: 1,
// see below for full details of options
}),
});
const job = await req.json();
console.log(job);
```
It will return a JSON like this:
```json
{
"id": "fp1f2rtva",
"status": "scheduled",
"output": null,
"error": "",
"created_at": "2024-05-15T01:41:55.913628+00:00",
"completed_at": null,
"started_at": null,
"updated_at": null,
"progress": 0,
"logs": ""
}
```
### Check render job status [#check-render-job-status]
```ts
// replace jobId with real id of the job
const req = await fetch(`https://api.polotno.com/api/renders/jobId?KEY=YOUR_API_KEY`);
const job = await req.json();
console.log(job);
if (job.status === 'done') {
console.log(job.output); // link to file, valid only for 7 days after job complete
}
```
### Options [#options]
* **design**: json (required) — data from Polotno export `store.toJSON()`. You can generate such JSON on the fly on your backend (e.g. replace text dynamically).
* **format**: string — file format of generated result. One of: `png` (default), `jpeg`, `pdf`, `gif`, `mp4`.
* **webhook**: string — URL to receive HTTP POST notifications with the full job payload.
* **pixelRatio**: number — quality modifier. `0.5` reduces width/height by 2; values > 1 increase quality (and size).
* **ignoreBackground**: boolean — remove page background on export (default: false).
* **dpiMetadata**: string — control DPI metadata embedding for images. One of: `auto` (default), `always`, `never`.
* **includeBleed**: boolean — render bleed areas (default: false).
* **skipFontError**: boolean — continue if font loading fails (default: false).
* **skipImageError**: boolean — continue if image loading fails (default: false).
* **textOverflow**: string — overwrite [text overflow](/docs/text-overflow) logic. Default: `change-font-size`. Other values: `resize`, `ellipsis`.
* **vector**: boolean — make `pdf` in vector format (selectable text). Alpha feature (default: false).
* **color**: object — control color space and profile:
```json
{
"color": {
"space": "CMYK",
"profile": "FOGRA39"
}
}
```
### See a list of created jobs [#see-a-list-of-created-jobs]
```ts
const req = await fetch('https://api.polotno.com/api/renders/list?KEY=YOUR_API_KEY&page=1&per_page=100');
const job = await req.json();
console.log(job.renders);
```
### Synchronous request [#synchronous-request]
Use `Prefer: 'wait'` for quick renders (e.g., image). The request may still return before completion for long renders (e.g., video).
```ts
const req = await fetch('https://api.polotno.com/api/renders?KEY=YOUR_API_KEY', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Prefer: 'wait'
},
body: JSON.stringify({
design: json,
pixelRatio: 1,
}),
});
const job = await req.json();
if (job.status === 'error' || job.status === 'done') {
// handle result
} else {
// job may still be running; poll status
}
```
## Live demo [#live-demo]
---
# Import from Figma
URL: https://polotno.com/docs/figma-import
Polotno can import designs from Figma using the [SVG Import](/docs/svg-import) feature. The process is straightforward: export your Figma frame as SVG, then import it into Polotno.
## Step 1: Select a Frame in Figma [#step-1-select-a-frame-in-figma]
In Figma, select the frame you want to export. Make sure everything you need is inside that frame.
## Step 2: Export as SVG [#step-2-export-as-svg]
In the right panel, find the **Export** section and select **SVG** as the format.
Before exporting, click the **...** button to open additional export options.
### Export Options [#export-options]
**"Outline Text" must be OFF** if you want editable text in Polotno. When "Outline Text" is enabled, text is converted into vector paths — it will look visually accurate, but you won't be able to edit the text content. You may still drag, resize, and recolor outlined text elements.
* **Outline Text** — Toggle **OFF** for editable text. Toggle ON if visual fidelity matters more than editability.
* **Include "id" attribute** — Toggle **ON** to preserve layer names from Figma in the imported design.
## Step 3: Import SVG into Polotno [#step-3-import-svg-into-polotno]
Once you have the SVG file, use the `@polotno/svg-import` package to load it into Polotno:
```typescript
import { svgToJson } from '@polotno/svg-import';
async function importFigmaSVG(file: File) {
const svgContent = await file.text();
const json = await svgToJson({ svg: svgContent });
store.loadJSON(json);
}
```
For full details on SVG import options and troubleshooting, see the [SVG Import documentation](/docs/svg-import).
## Tips [#tips]
* **One frame at a time** — Export and import each frame separately for best results.
* **Flatten complex effects** — Figma effects like advanced blurs or blend modes may not translate perfectly. Simplify where possible.
* **Check text after import** — Verify that text elements are editable and display correctly in Polotno.
---
# Large Format & High-Resolution Exports
URL: https://polotno.com/docs/hd-exports
Large format printing requires high-resolution exports that can exceed browser rendering capabilities. This guide covers strategies for exporting designs at print-ready quality for banners, posters, and other large format applications.
## Quick Start (TL;DR) [#quick-start-tldr]
* Decide rendering: **Client** (desktop, ≤8k px/side) or **Server/Vector** (>8k or mobile).
* Client path: set `pixelRatio` 2–4; test on target hardware.
* Print checks: source images must meet `physical inches × target DPI` on each side.
* Vector output options:
* **Client-side**: export **SVG/HTML** (resolution-independent, feature-dependent).
* **Server-side**: export **vector PDF** via **Cloud Render API** (`format: 'pdf'`, `vector: true`) or `@polotno/pdf-export`.
* Swap to high-res assets just before export; keep previews in the editor.
## The Challenge [#the-challenge]
Large format designs often require resolutions that browsers cannot render directly. For example:
* **5 × 10 feet banner at 200 DPI** = 12,000 × 24,000 pixels
* **10 × 10 feet banner at 200 DPI** = 24,000 × 24,000 pixels
Browsers cap canvas size by browser/OS/hardware, typically **4,000–16,000 pixels** per dimension. Polotno doesn’t cap exports; failures come from browser and hardware limits.
## Browser Rendering Limits [#browser-rendering-limits]
Canvas rendering limits depend on:
* **Browser**: Chrome, Firefox, Safari have different maximum canvas sizes
* **Operating system**: macOS, Windows, Linux handle memory differently
* **Hardware**: GPU memory and available RAM affect practical limits
* **Typical range**: 4,000–16,000 pixels per dimension
When exports exceed these limits, browsers may:
* Fail to render the canvas
* Crash the tab or browser
* Produce corrupted or incomplete exports
## Choosing Your Approach [#choosing-your-approach]
Select a rendering strategy based on your use case, target audience, and design complexity.
### Decision Cheatsheet [#decision-cheatsheet]
* **Client (desktop, ≤8k px/side)**: real-time, fast; use `pixelRatio` 2–4.
* **Server/Vector (>8k, mobile, or critical print)**: Cloud Render API, `@polotno/pdf-export`, or SVG/HTML (when supported).
* **Hybrid**: detect device + design complexity; fall back to server/vector when near limits.
### Client-Side Rendering [#client-side-rendering]
**Best for:**
* Designs up to \~8,000 pixels per dimension
* Users with powerful desktop computers
* Smaller print formats (business cards, flyers, small posters)
* Real-time preview and instant exports
**Limitations:**
* May fail on mobile devices or low-end hardware
* Memory-intensive for very large exports
* Export time increases with size
### Server-Side Rendering [#server-side-rendering]
**Best for:**
* Designs exceeding 8,000 pixels per dimension
* Mobile or low-end device users
* Guaranteed consistency across devices
* High-volume automated workflows
* Vector PDF exports (resolution-independent)
**Options:**
1. **Cloud Render API** — managed service, no infrastructure
2. **Node.js with Polotno SDK** — full control, requires server setup
### Hybrid Approach [#hybrid-approach]
Detect device capabilities and route exports accordingly:
```ts
const isHighEndDevice = navigator.hardwareConcurrency >= 8 &&
navigator.deviceMemory >= 8;
const exportDesign = async (store, pixelRatio = 2) => {
const maxSide = Math.max(store.width, store.height) * pixelRatio;
if (maxSide > 8000 || !isHighEndDevice) {
// Use server-side rendering
return await exportViaCloudAPI(store);
} else {
// Use client-side rendering
return await store.saveAsImage({ pixelRatio });
}
};
```
## Client-Side High-Quality Export [#client-side-high-quality-export]
Use `pixelRatio` to increase export resolution without changing canvas dimensions. Higher values produce larger output files with better quality.
### Understanding pixelRatio [#understanding-pixelratio]
`pixelRatio` multiplies the canvas dimensions for export:
```ts
// Canvas: 1200 × 1200 px
// Export with pixelRatio: 2 → Output: 2400 × 2400 px
// Export with pixelRatio: 4 → Output: 4800 × 4800 px
await store.saveAsImage({ pixelRatio: 2 });
```
**Important**: `pixelRatio` increases rendered pixels (quality). `dpi` changes physical sizing (PDF page size and ruler units) and DPI metadata for PNG/JPEG. See [Units and Measures](/docs/units-and-measures) for details.
### Practical Limits [#practical-limits]
Start with `pixelRatio: 2` for high quality. Increase to `4` or `8` for very large formats, but test on target devices:
```ts
// 4ft × 4ft banner at 200 DPI = 9,600 × 9,600 pixels
// Editor canvas: 1,200 × 1,200 px (manageable size)
store.setSize(1200, 1200);
// Export at 8× quality to reach 9,600px output
await store.saveAsImage({ pixelRatio: 8 });
```
**Warning**: Very high `pixelRatio` values (8+) may cause browser crashes on some devices. Test thoroughly and consider server-side rendering for critical workflows.
### Complete Client-Side Example [#complete-client-side-example]
```ts
// Setup: 4ft × 4ft banner at 200 DPI
const widthInches = 48; // 4 feet
const heightInches = 48;
const targetDPI = 200;
// Calculate manageable editor size (use 72 DPI for editor)
const editorWidth = widthInches * 72; // 3,456 px
const editorHeight = heightInches * 72; // 3,456 px
store.setSize(editorWidth, editorHeight);
// Calculate pixelRatio for 200 DPI export
// PDF uses 72 DPI internally, so: pixelRatio = targetDPI / 72
const pixelRatio = targetDPI / 72; // ≈ 2.78
// Export high-quality image
await store.saveAsImage({
pixelRatio: pixelRatio,
});
```
## Editing at Scale (rulers and DPI in the editor) [#editing-at-scale-rulers-and-dpi-in-the-editor]
Use a smaller canvas for stability, but keep rulers showing the real physical size:
```ts
// Target: 10ft × 10ft (120in × 120in), edit at 1:10 scale
const scale = 0.1;
const targetInches = 120;
const editorDPI = 72; // screen DPI
// Proxy canvas: 12in × 12in → 864 × 864 px
store.setSize(targetInches * scale * editorDPI, targetInches * scale * editorDPI);
// Make rulers read 120 inches while canvas is 12 inches
store.setUnit({
unit: 'in',
dpi: editorDPI * scale, // 72 * 0.1 = 7.2; 864px/7.2 ≈ 120in on rulers
});
```
At export time, use the real target DPI (e.g., 200) via `pixelRatio` or server/vector rendering. More on units and DPI: [Units and Measures](/docs/units-and-measures).
### Bitmap PDF export: page size vs quality [#bitmap-pdf-export-page-size-vs-quality]
Client-side PDF export is a **bitmap PDF** (each page is a raster image inside a PDF container). Two parameters matter:
* **Page size (real-world inches)** comes from `dpi`: ( inches = pixels / dpi )
* **Render quality** comes from `pixelRatio`: ( effectiveDPI \approx dpi \times pixelRatio )
#### Simple: export a print-ready bitmap PDF [#simple-export-a-print-ready-bitmap-pdf]
```ts
await store.saveAsPDF({
fileName: 'design.pdf',
pixelRatio: 2,
});
```
#### Large PDF from a proxy canvas (scale edit → scale export) [#large-pdf-from-a-proxy-canvas-scale-edit--scale-export]
If you edit a 10ft × 10ft banner as a 1:10 proxy (864 × 864 px), export a 120-inch PDF page by using the same scaled DPI, then compensate quality via `pixelRatio`:
```ts
const scale = 0.1;
const targetInches = 120;
const editorDPI = 72;
// Proxy canvas: 864 × 864 px
store.setSize(targetInches * scale * editorDPI, targetInches * scale * editorDPI);
store.setUnit({ unit: 'in', dpi: editorDPI * scale });
const targetDPI = 200;
const exportDPI = editorDPI * scale; // 7.2 → 864px/7.2 = 120 inches page size
await store.saveAsPDF({
fileName: 'banner.pdf',
dpi: exportDPI, // controls PDF page size
pixelRatio: targetDPI / exportDPI, // effectiveDPI ≈ 200
});
```
> **Warning**: For large formats this may require a very large `pixelRatio` (and lots of memory). Prefer server-side rendering or vector PDF for reliability.
> **Note**: If the browser fails at the requested quality, Polotno will automatically retry bitmap PDF export at a lower quality to improve success rates.
See [PDF Export](/docs/pdf-export) for bleed, crop marks, and more options.
## Asset Swapping Strategy [#asset-swapping-strategy]
Use low-resolution preview images in the editor for performance, then swap to high-resolution versions before export.
### Store High-Resolution References [#store-high-resolution-references]
Store high-res URLs in element `custom` metadata:
```ts
// When adding images, store both preview and high-res URLs
element.set({
src: 'https://example.com/preview-800px.jpg', // Fast loading for editor
custom: {
highResSrc: 'https://example.com/original-5000px.jpg' // Full resolution for export
}
});
```
### Swap Before Export [#swap-before-export]
Replace preview images with high-resolution versions before exporting:
```ts
// Collect all image elements with high-res sources
const originalSrcs = [];
store.pages.forEach(page => {
page.children.forEach(el => {
if (el.type === 'image' && el.custom?.highResSrc) {
// Store original preview URL
originalSrcs.push({ el, src: el.src });
// Swap to high-resolution
el.set({ src: el.custom.highResSrc });
}
});
});
// Export with high-quality assets
await store.saveAsImage({ pixelRatio: 4 });
// Revert to preview images for editor performance
originalSrcs.forEach(({ el, src }) => {
el.set({ src });
});
```
### Validate Image Resolution [#validate-image-resolution]
Use `getImageSize` to check if an image meets the target DPI and physical size:
```ts
import { getImageSize } from 'polotno/utils/image';
const validateImageResolution = async (src, targetDPI, widthInches, heightInches) => {
const requiredWidth = widthInches * targetDPI;
const requiredHeight = heightInches * targetDPI;
const { width, height } = await getImageSize(src);
const isSufficient = width >= requiredWidth && height >= requiredHeight;
if (!isSufficient) {
console.warn(
`Image resolution (${width}×${height}) may be insufficient ` +
`for ${widthInches}"×${heightInches}" at ${targetDPI} DPI ` +
`(requires at least ${requiredWidth}×${requiredHeight}px)`
);
}
return isSufficient;
};
```
## Vector PDF Export [#vector-pdf-export]
Vector PDFs are resolution-independent and avoid pixel limits. Use Cloud Render API (hosted) or the Node package (self-hosted).
### Cloud Render API [#cloud-render-api]
```ts
// 10ft × 10ft banner - no pixel limits with vector PDF
const json = store.toJSON();
const response = await fetch('https://api.polotno.com/api/renders?KEY=YOUR_API_KEY', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
design: json,
format: 'pdf',
vector: true, // Enable vector output
}),
});
const job = await response.json();
// Poll for completion
const checkStatus = async () => {
const statusRes = await fetch(
`https://api.polotno.com/api/renders/${job.id}?KEY=YOUR_API_KEY`
);
const status = await statusRes.json();
if (status.status === 'done') {
console.log('Download:', status.output);
} else if (status.status === 'error') {
console.error('Export failed:', status.error);
} else {
// Still processing, check again
setTimeout(checkStatus, 2000);
}
};
checkStatus();
```
See [Cloud Render API](/docs/cloud-render-api) for complete API reference.
### Node Package (@polotno/pdf-export) [#node-package-polotnopdf-export]
```ts
import { jsonToPDF } from '@polotno/pdf-export';
const json = store.toJSON();
await jsonToPDF(json, './output.pdf');
```
See [Node Package](/docs/pdf-export) for complete API reference.
## SVG and HTML Export Alternatives [#svg-and-html-export-alternatives]
See dedicated docs for details: [SVG Export](/docs/import-and-export#html-and-svg-export) and [HTML Export](/docs/import-and-export#html-and-svg-export). Use PNG/JPEG or PDF for pixel-perfect fidelity.
## Example: 10ft × 10ft Banner (120 × 120 inches @ 200 DPI) [#example-10ft--10ft-banner-120--120-inches--200-dpi]
**Recommended (vector PDF, server):**
* Set JSON `width/height` to `120 * 72` (PDF points) and call Cloud Render API with `vector: true`.
* Source images must be \~24,000 px per side to stay sharp at 200 DPI.
**Client attempt (use only for testing on strong desktops):**
* Edit at a reduced canvas (e.g., 1,200 × 1,200 px), then export with high `pixelRatio` (e.g., 20). Expect failures/crashes; prefer server/vector.
## Best Practices [#best-practices]
### Editor Performance [#editor-performance]
* **Use smallest images possible** in the editor for fast loading and smooth interaction
* Store high-resolution references in `custom` metadata
* Swap to high-res only during export
* Polotno automatically optimizes images, but smaller source images improve performance
### Asset Management [#asset-management]
* **Store high-res URLs separately** — don't load full-resolution images in the editor
* **Validate resolution** — check image dimensions against target export requirements
* **Warn users early** — notify if uploaded images are insufficient for target DPI
* **Use CDN** — serve high-resolution assets from a content delivery network
### Export Strategy [#export-strategy]
* **Test on target devices** — verify client-side exports work on your users' hardware
* **Set appropriate timeouts** — server exports may take 5+ minutes for very large files
* **Monitor memory usage** — large exports consume significant RAM
* **Provide progress feedback** — show export progress for long-running operations
## Troubleshooting [#troubleshooting]
### Canvas Exceeds Browser Limits [#canvas-exceeds-browser-limits]
**Symptom**: Export fails, browser crashes, or produces corrupted output.
**Solutions**:
* Reduce canvas dimensions or `pixelRatio`
* Use server-side rendering (Cloud Render API or Node.js)
* Use vector PDF/SVG/HTML export (no pixel limits)
### Server Export Timeout [#server-export-timeout]
**Symptom**: Server export requests timeout before completion.
**Solutions**:
* Increase HTTP timeout limits (5+ minutes for very large exports)
* Use async job polling instead of synchronous requests
* Optimize design complexity
* Consider Cloud Render API with webhook notifications
## Related [#related]
* [PDF Export](/docs/pdf-export) — client-side PDF export with quality settings
* [Cloud Render API](/docs/cloud-render-api) — server-side vector PDF export
* [Units and Measures](/docs/units-and-measures) — DPI, pixelRatio, and unit conversion
* [Import and Export](/docs/import-and-export) — overview of all export formats
* [Store API](/docs/store) — complete export API reference
---
# Import and Export
URL: https://polotno.com/docs/import-and-export
Polotno offers comprehensive import and export capabilities for designs, from open JSON format to high-quality images, PDFs, and video exports. The SDK supports both client-side and [server-side rendering](/docs/server-side-image-generation-with-node-js) for automated design generation.
**Import:** JSON (open, developer-friendly format), [SVG](/docs/svg-import), [PDF](/docs/pdf-import), [PSD](/docs/psd-import)
**Export:** JSON, PNG, JPEG, PDF, PPTX, GIF, MP4, HTML, SVG
**Key use cases:** Template systems, programmatic generation, print production, digital graphics, animations, web embedding.
## JSON Import and Export [#json-import-and-export]
Polotno uses an **open JSON format** designed for developer flexibility. You can export, read, modify, and re-import designs programmatically.
**Key features:**
* **Simple, readable format** — JSON structure is straightforward and documented
* **Fully modifiable** — export, modify properties, and re-import
* **Programmatic generation** — create or modify designs in code
* **Template system** — save and load templates, generate variations dynamically
### Export to JSON [#export-to-json]
```ts
const json = store.toJSON();
console.log(json); // readable JSON structure
// modify programmatically
json.pages[0].children[0].text = 'Updated text';
// save or send to backend
localStorage.setItem('design', JSON.stringify(json));
```
### Import from JSON [#import-from-json]
```ts
// load previously saved design
const json = JSON.parse(localStorage.getItem('design'));
store.loadJSON(json);
// import template or generated design
store.loadJSON({
width: 800,
height: 600,
pages: [/* ... */]
});
```
The JSON must follow Polotno's [store structure](/docs/store). See [Design Format](/docs/schema) for the complete field reference, types, and validation.
**Note**: PPTX import isn't first-party yet — for now you can build a converter that emits Polotno JSON, or [contact us](https://polotno.com/contact) to collaborate.
## SVG Import [#svg-import]
Import SVG files into Polotno using the `@polotno/svg-import` package.
```bash
npm install @polotno/svg-import
```
```ts
import { svgToJson } from '@polotno/svg-import';
const json = await svgToJson({ svg: svgContent });
store.loadJSON(json);
```
**Note:** Text converted to paths cannot be restored as editable text. See [SVG Import](/docs/svg-import) for full details and workflow recommendations.
**Try it without code:** [SVG to JSON](/tools/svg-to-json) · [SVG to PDF](/tools/svg-to-pdf) — both run `svgToJson()` in the browser on your own files.
## PDF Import [#pdf-import]
Import PDF files into Polotno using the `@polotno/pdf-import` package.
```bash
npm install @polotno/pdf-import
```
```ts
import { pdfToJson } from '@polotno/pdf-import';
const buffer = await file.arrayBuffer();
const json = await pdfToJson({ pdf: buffer });
store.loadJSON(json);
```
Works both client-side and server-side. See [PDF Import](/docs/pdf-import) for full details.
**Try it without code:** [PDF to JSON](/tools/pdf-to-json) · [PDF to SVG](/tools/pdf-to-svg) · [PDF to HTML](/tools/pdf-to-html) — all three run `pdfToJson()` in the browser on your own files.
## PSD Import [#psd-import]
Import Photoshop `.psd` files into Polotno using the `@polotno/psd-import` package. Layers come through as separate, editable elements: text stays editable text, vector shapes stay vector, raster layers stay images. Masks and unsupported effects are baked per layer so the rest of the document remains separable.
```bash
npm install @polotno/psd-import
```
```ts
import { psdToJson } from '@polotno/psd-import';
const buffer = await file.arrayBuffer();
const json = await psdToJson({ psd: buffer });
store.loadJSON(json);
```
Works both client-side and server-side. See [PSD Import](/docs/psd-import) for full details.
**Try it without code:** [PSD to PNG](/tools/psd-to-png) · [PSD to JPG](/tools/psd-to-jpg) · [PSD to PDF](/tools/psd-to-pdf) · [PSD to SVG](/tools/psd-to-svg) · [PSD to HTML](/tools/psd-to-html) · [PSD to JSON](/tools/psd-to-json) — all six run `psdToJson()` in the browser on your own files.
## Image Export (PNG, JPEG) [#image-export-png-jpeg]
Export designs as PNG or JPEG images. Available client-side and server-side.
```ts
// basic export
await store.saveAsImage();
// with quality control
await store.saveAsImage({
pixelRatio: 2,
mimeType: 'image/png'
});
// with DPI metadata for print workflows
await store.saveAsImage({
dpi: 300,
dpiMetadata: 'auto' // 'auto' | 'always' | 'never'
});
// as data URL or blob
const dataURL = await store.toDataURL();
const blob = await store.toBlob();
```
**DPI Metadata:** PNG and JPEG exports support embedding resolution metadata via the `dpi` parameter (defaults to `store.dpi` value). By default (`dpiMetadata: 'auto'`), metadata is embedded only when DPI differs from 72. Use `dpiMetadata: 'always'` to force embedding or `'never'` to skip it.
## PDF Export [#pdf-export]
Export designs as PDF files. Two paths, both available client-side:
* **Raster** — `store.saveAsPDF()` flattens each page into an image embedded in the PDF.
* **Vector** — `jsonToPDFBlob()` from `@polotno/pdf-export/browser` keeps paths, strokes, and selectable text. Same package supports Node and PDF/X-1a print-ready output.
```ts
// raster — built-in
await store.saveAsPDF({
includeBleed: true,
cropMarkSize: 20,
pixelRatio: 2,
});
// vector — browser entry
import { jsonToPDFBlob } from '@polotno/pdf-export/browser';
const blob = await jsonToPDFBlob(store.toJSON());
```
See [PDF Export](/docs/pdf-export) for the full comparison, bleed/crop marks, PDF/X-1a, spot colors, and the Cloud Render API option.
## GIF Export [#gif-export]
Export animated designs as GIF files. Available client-side and server-side.
```ts
await store.saveAsGIF();
// with custom settings
await store.saveAsGIF({
fps: 30,
pixelRatio: 2
});
// export only specific pages
await store.saveAsGIF({
pageIds: [store.pages[0].id, store.pages[2].id],
});
```
Use `pageId` (single page) or `pageIds` (subset, in order) to limit which pages end up in the GIF. See [Store API](/docs/store#await-storesaveasgif) for the full option list.
## Video Export (MP4) [#video-export-mp4]
Export animated designs as MP4 video files.
**Client-side:** Use `@polotno/video-export` package for browser-based video encoding. See [Video Export](/docs/video-export) for details.
**Server-side:** Use [Cloud Render API](/docs/cloud-render-api).
## HTML and SVG Export [#html-and-svg-export]
Export designs as HTML or SVG for embedding in websites or further processing.
```ts
// export as HTML
const html = await store.toHTML();
await store.saveAsHTML();
// export as SVG
const svg = await store.toSVG();
await store.saveAsSVG();
```
**Note:** May not support all elements or features. For pixel-perfect exports, use PNG/JPEG or PDF.
## PPTX Export (PowerPoint) [#pptx-export-powerpoint]
Export designs as PowerPoint presentations. Multi-page designs become multi-slide presentations.
```ts
await store.saveAsPPTX();
```
See [PPTX Export](/docs/pptx-export) for details.
## Server-Side Rendering [#server-side-rendering]
All export formats can be generated server-side for high-volume or automated workflows.
**Options:**
1. **Node.js with Polotno SDK** — run Polotno in Node.js environment. See [Server-Side Image Generation](/docs/server-side-image-generation-with-node-js).
2. **Cloud Render API** — fully managed rendering service, no infrastructure needed. See [Cloud Render API](/docs/cloud-render-api).
## Complete API Reference [#complete-api-reference]
For all export options and parameters, see [Store API](/docs/store).
---
# PDF Export
URL: https://polotno.com/docs/pdf-export
Polotno offers two PDF export paths:
* **Raster** — `store.saveAsPDF()` / `store.toPDFDataURL()` in the browser, or [`polotno-node`](/docs/server-side-image-generation-with-node-js) on the server. Each page is a flattened image embedded in the PDF.
* **Vector** — [`@polotno/pdf-export`](https://www.npmjs.com/package/@polotno/pdf-export) (browser **or** Node), or the [Cloud Render API](/docs/cloud-render-api) with `vector: true`. Resolution-independent output with selectable text.
**Try it without code:** The [SVG to PDF](/tools/svg-to-pdf) converter exposes both options — it ships **vector by default** via `@polotno/pdf-export/browser` and offers a one-click bitmap fallback. Drop a file and try both before integrating.
## Choosing raster vs vector [#choosing-raster-vs-vector]
| | Raster | Vector |
| -------------------- | ---------------------------------------- | ---------------------------------------------------- |
| **Output** | Flattened image per page | Selectable text, scalable shapes |
| **Visual fidelity** | Identical to canvas | May differ slightly (font shaping, effects) |
| **File size** | Larger at high `pixelRatio` | Smaller, resolution-independent |
| **Print compliance** | Good with high `pixelRatio` | Required for PDF/X-1a, spot colors |
| **Where it runs** | Browser, or `polotno-node` on the server | Browser, Node, or Cloud Render API |
| **Server required** | No (browser path) | No (browser path); PDF/X-1a needs Node + Ghostscript |
Pick **raster** when you need pixel-perfect parity with the editor canvas. Pick **vector** for selectable text, smaller files, or print-shop deliverables (PDF/X-1a, spot inks, foils). Both can run fully client-side now — vector via the `@polotno/pdf-export/browser` entry.
***
## Raster export [#raster-export]
Browser-side export via `store.saveAsPDF()`. Same options apply to `store.toPDFDataURL()`.
### Quick start [#quick-start]
```ts
await store.saveAsPDF({
fileName: 'design.pdf',
includeBleed: true,
cropMarkSize: 20,
pixelRatio: 2,
});
```
### Bleed and crop marks [#bleed-and-crop-marks]
Bleed is the extra ink area beyond the trim edge. Crop marks indicate where the print shop should cut.
```ts
// set bleed on the page
store.activePage.set({ bleed: 35 }); // 35px
// show bleed area on the canvas (optional, for editor preview)
store.toggleBleed(true);
// export
await store.saveAsPDF({
includeBleed: true,
cropMarkSize: 20, // crop mark length in pixels
});
```
See [Page Bleed](/docs/page-bleed) for working with bleed in the editor.
### Quality [#quality]
Use `pixelRatio` to control resolution. `dpi` affects PDF page dimensions, **not** image quality.
```ts
await store.saveAsPDF({ pixelRatio: 2 }); // recommended for print
await store.saveAsPDF({ pixelRatio: 4 }); // ultra-high
```
See [Units and Measures](/docs/units-and-measures) for DPI details.
### Options [#options]
```ts
await store.saveAsPDF({
fileName: 'design.pdf',
includeBleed: true,
cropMarkSize: 20,
pixelRatio: 2,
onProgress: (progress) => console.log(progress),
});
```
See [Store API](/docs/store) for the complete reference.
***
## Vector export [#vector-export]
Three ways to produce vector PDFs. All accept the same Polotno JSON and produce equivalent output for the same input — pick the one that matches your hosting model.
### Fonts [#fonts]
Fonts used in the design — custom fonts registered in the JSON and Google Fonts — are **embedded into the PDF automatically**, as subsetted font files. Text stays selectable and renders identically on any machine; no font installation is needed on the viewer's side. System fonts (Arial, Times New Roman, Courier, …) are the exception: they map to the standard PDF base-14 fonts, which viewers supply themselves, so they are not embedded in a plain vector export. [PDF/X-1a conversion](#pdfx-1a) embeds all fonts, including substitutes for the base-14 ones, as the spec requires.
### Browser package: `@polotno/pdf-export/browser` [#browser-package-polotnopdf-exportbrowser]
Convert a design to a vector PDF entirely in the user's browser — no server, no Ghostscript, no API key.
```bash
npm install @polotno/pdf-export
```
```ts
import { jsonToPDFBlob } from '@polotno/pdf-export/browser';
async function exportToPDF(store) {
const blob = await jsonToPDFBlob(store.toJSON());
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'design.pdf';
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
```
The browser entry also exports `jsonToPDFBytes(json, attrs): Promise` for cases where you want raw bytes (IndexedDB, `navigator.share`, WebTransport, etc.).
#### CORS [#cors]
When the design references remote images, fonts, or SVG sources, the browser fetches those at export time. They must be reachable with CORS:
* **Google Fonts** (`fonts.googleapis.com` and `fonts.gstatic.com`) work out of the box.
* **Custom asset URLs** must be served with `Access-Control-Allow-Origin: *` (or your origin), or pre-fetched and inlined as `data:` URLs in the JSON.
#### Browser limitations [#browser-limitations]
The browser entry **does not** support `attrs.pdfx1a` or `attrs.validate` — those rely on Ghostscript, which has no browser equivalent. Pass them and you'll get a clear runtime error pointing at the Node entry. For PDF/X-1a, spot colors, and Ghostscript-backed validation, see the Node section below.
### Cloud Render API [#cloud-render-api]
Hosted rendering. Send the JSON as a render job with `format: 'pdf'` and `vector: true`.
```ts
const req = await fetch('https://api.polotno.com/api/renders?KEY=YOUR_API_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
design: store.toJSON(),
format: 'pdf',
vector: true,
}),
});
```
See [Cloud Render API](/docs/cloud-render-api) for full options, polling, webhooks, and CMYK color profiles.
### Node package: `@polotno/pdf-export` [#node-package-polotnopdf-export]
Run the conversion fully offline inside Node 18+. Fits CI jobs, backend services, and desktop tools. Required for PDF/X-1a print-ready output and Ghostscript-backed spot colors.
```bash
npm install @polotno/pdf-export
```
```ts
import fs from 'fs/promises';
import { jsonToPDF } from '@polotno/pdf-export';
const data = JSON.parse(await fs.readFile('./polotno.json', 'utf-8'));
await jsonToPDF(data, './output.pdf');
```
`jsonToPDFBytes(data, attrs): Promise` is also exported on the Node entry — useful when you want raw bytes (to upload, attach, etc.) without writing to disk.
#### Bleed and crop marks [#bleed-and-crop-marks-1]
Set `bleed` (pixels) on each page in the JSON, then enable bleed and crop marks at export time.
```ts
// JSON
const data = {
width: 1080,
height: 1080,
pages: [{ background: '...', bleed: 36, children: [/* ... */] }],
};
// export
await jsonToPDF(data, './print-ready.pdf', {
pdfx1a: true,
includeBleed: true,
cropMarkSize: 18, // reserve 18px around the bleed for crop marks
});
```
When `includeBleed` or `cropMarkSize` is set, the output PDF carries correct PDF/X page boxes:
* **MediaBox** — full sheet (trim + bleed + crop-mark margin)
* **BleedBox** — trim + bleed (where ink may cover)
* **TrimBox** — final cut size
* **ArtBox** — same as TrimBox
`TrimBox ⊆ BleedBox ⊆ MediaBox`, per PDF/X spec. RIPs and proofers use these boxes to identify the live area without guessing.
Element coordinates in the JSON stay relative to the trim corner — a `(0, 0)` element still lands at the top-left of the trim. Backgrounds extend automatically into the bleed strip; the crop-mark margin remains unprinted.
#### DPI handling [#dpi-handling]
Coordinates in the JSON are pixels at the specified DPI; PDFs use points (1pt = 1/72 inch). The library converts: `points = pixels × (72 / dpi)`.
```ts
// uses dpi from JSON (or 72 if unspecified)
await jsonToPDF(data, './output.pdf');
// override
await jsonToPDF(data, './output.pdf', { dpi: 150 });
```
A 1920×1080 canvas at 300 DPI exports as 6.4″ × 3.6″; the same canvas at 72 DPI exports as 26.67″ × 15″.
#### PDF/X-1a [#pdfx-1a]
Print-ready output with CMYK conversion, transparency flattening, and full font embedding (every font embedded and subsetted, as PDF/X-1a requires — text is not converted to outlines).
```ts
await jsonToPDF(data, './print-ready.pdf', {
pdfx1a: true,
validate: true, // optional — verify PDF/X-1a compliance
metadata: {
title: 'Catalog Cover',
author: 'Marketing Team',
application: 'Polotno Automation 1.0',
},
});
```
> **Note**: PDF/X-1a conversion requires GhostScript on the host machine — `brew install ghostscript`, `apt-get install ghostscript`, or download from [ghostscript.com](https://www.ghostscript.com/).
#### Spot colors and overprint [#spot-colors-and-overprint]
Map specific fills or strokes to separation inks for foil, Pantone, or varnish workflows. Any element using the matched color is exported with the correct separation ink; CMYK fallbacks ensure predictable previews.
```ts
await jsonToPDF(data, './foil-cover.pdf', {
pdfx1a: true,
spotColors: {
'rgba(255,215,0,1)': {
name: 'Gold Foil',
pantoneCode: 'Pantone 871 C', // optional reference
cmyk: [0, 0.15, 0.5, 0], // 0–1 range, used as fallback
overprint: true,
},
'#C0C0C0': {
name: 'Silver Foil',
cmyk: [0, 0, 0, 0.25],
overprint: true,
},
},
});
```
**Color matching is flexible.** `'#FFD700'`, `'#ffd700'`, `'rgb(255,215,0)'`, and `'rgba(255,215,0,1)'` all match the same color. Works on text, lines, figures (rect, circle, star, blobs), and SVG elements.
**`overprint: true`** tells the press to print the spot ink **on top of** the underlying CMYK rather than knocking it out. Without overprint, a misregistered foil die leaves a visible white halo at the edge. With overprint, CMYK prints continuously through the spot region and the foil stamps over it. This is the standard choice for foil and varnish.
> **Tip**: Verify spot colors in Adobe Acrobat Pro: **Tools → Print Production → Output Preview → Separations**. Use *Simulate Overprinting* to preview how the press will composite the layers.
> **Caveat**: Spot color preservation through PDF/X-1a depends on Ghostscript not flattening transparency in the spot region. Plain figures, lines, text, and simple SVG paths work. SVGs that use `` or `` may have their spot color flattened to CMYK during pdfx1a conversion. For foil regions, prefer plain shapes or simple SVG paths over masked artwork.
#### Full reference [#full-reference]
For all options and the latest changes, see the [`@polotno/pdf-export` README on npm](https://www.npmjs.com/package/@polotno/pdf-export).
***
## Error handling [#error-handling]
Failures throw a plain `Error` with a `code` — `DESIGN_INVALID` (the schema pre-flight every render runs first), `IMAGE_FAILED`, `FONT_FAILED`, `FETCH_FAILED`, or `EXPORT_FAILED` — plus structured `details` such as `elementId`, `mimeType`, and a fine-grained `details.reason`. Branch on the code, not the message text. See [Error Handling](/docs/error-handling) for the full vocabulary.
## Troubleshooting [#troubleshooting]
**Bleed not visible on canvas?** Use `store.toggleBleed(true)`.
**Raster export looks low quality?** Increase `pixelRatio` to 2 or 4. `dpi` controls page dimensions, not quality.
**Wrong page size?** Adjust the JSON `dpi` value (or pass `dpi` to `jsonToPDF`).
**Spot color rendered as CMYK in the foil region?** Check whether the artwork uses `` or `` — Ghostscript may flatten transparency groups during PDF/X-1a conversion. Replace with plain shapes or simple paths.
## Related [#related]
* [Page Bleed](/docs/page-bleed) — working with bleed in the editor
* [Cloud Render API](/docs/cloud-render-api) — hosted rendering
* [Server-side image generation with Node.js](/docs/server-side-image-generation-with-node-js) — `polotno-node` for raster export on the server
* [Units and Measures](/docs/units-and-measures) — DPI and unit conversion
* [Store API](/docs/store) — complete export API reference
---
# PDF Import
URL: https://polotno.com/docs/pdf-import
The `@polotno/pdf-import` package converts PDF files into Polotno JSON format, enabling you to import PDF designs into Polotno for further editing.
**Beta Feature:** This library is new and may not work correctly for all PDF files. Please [report issues](https://community.polotno.com/c/ask) to help us improve it.
**Try it without code:** The [PDF to JSON](/tools/pdf-to-json), [PDF to SVG](/tools/pdf-to-svg), and [PDF to HTML](/tools/pdf-to-html) converters run this exact parser in the browser — drop your own file to test it before integrating.
**Importing other formats?** See [SVG Import](/docs/svg-import) and [PSD Import](/docs/psd-import) for the equivalent packages.
## Installation [#installation]
```bash
npm install @polotno/pdf-import
```
## Basic Usage [#basic-usage]
```typescript
import { pdfToJson } from '@polotno/pdf-import';
const json = await pdfToJson({ pdf: buffer });
// Load into Polotno store
store.loadJSON(json);
```
## Node.js Example [#nodejs-example]
```typescript
import { pdfToJson } from '@polotno/pdf-import';
import fs from 'fs';
const buffer = fs.readFileSync('design.pdf');
const json = await pdfToJson({ pdf: buffer });
store.loadJSON(json);
```
## Browser Example [#browser-example]
```typescript
import { pdfToJson } from '@polotno/pdf-import';
async function handleFileUpload(file: File) {
const buffer = await file.arrayBuffer();
const json = await pdfToJson({ pdf: buffer });
store.loadJSON(json);
}
```
`pdfToJson` returns a complete, valid [Design Format](/docs/schema) document, ready for `store.loadJSON()`. See the reference for every field and type.
## Adobe Illustrator (.ai) Support [#adobe-illustrator-ai-support]
Adobe Illustrator `.ai` files are based on the PDF format, so you can import them using the same `pdfToJson()` function.
## Platform Support [#platform-support]
This package works both **client-side (browser)** and **server-side (Node.js)**. All processing runs locally — no data is sent to any external server.
## Demo [#demo]
## Error Handling [#error-handling]
If a file can't be parsed, the importer throws a plain `Error` with `code: 'IMPORT_FAILED'` and `details.format: 'pdf'`; the low-level parser error is chained as `error.cause`. See [Error Handling](/docs/error-handling).
## Troubleshooting [#troubleshooting]
**Import produces unexpected results:**
* Check if the PDF contains vector elements or rasterized content
* Try re-exporting the PDF from your design tool with different settings
**Missing elements:**
* Some advanced PDF features may not be supported
* Check browser console for warnings
**Need help?** Report issues and get support at [community.polotno.com](https://community.polotno.com/c/ask).
---
# PPTX Export
URL: https://polotno.com/docs/pptx-export
## Overview [#overview]
Polotno supports exporting designs to PowerPoint (PPTX) format, allowing you to create presentation files from your designs. This feature is available through the `@polotno/pptx-export` package.
## Installation [#installation]
First, install the PPTX export package:
```bash
npm install @polotno/pptx-export
```
## Basic Usage [#basic-usage]
Import the package and use the `jsonToPPTX` function to export your design:
```ts
import { jsonToPPTX } from '@polotno/pptx-export';
// Export the current design to PPTX
jsonToPPTX({
json: store.toJSON(),
output: 'my-design.pptx',
});
```
### Dynamic Import [#dynamic-import]
If you prefer to load the package dynamically (e.g., to reduce initial bundle size), you can use dynamic imports:
```ts
import('@polotno/pptx-export').then((module) => {
module.jsonToPPTX({
json: store.toJSON(),
output: 'my-design.pptx',
});
});
```
## API Reference [#api-reference]
### `jsonToPPTX(options)` [#jsontopptxoptions]
Exports a Polotno design to PowerPoint format.
**Parameters:**
* `json` (required): The design JSON from `store.toJSON()`
* `output` (required): The output filename (e.g., `'presentation.pptx'`)
**Example:**
```ts
import { jsonToPPTX } from '@polotno/pptx-export';
const handleExport = () => {
jsonToPPTX({
json: store.toJSON(),
output: 'my-presentation.pptx',
});
};
```
## Use Cases [#use-cases]
* Convert designs into editable PowerPoint presentations
* Share designs with users who prefer working in PowerPoint
* Create templates that can be further edited in presentation software
* Export multi-page designs as presentation slides
## Notes [#notes]
* Each Polotno page becomes a slide in the PowerPoint presentation
* Text elements remain editable in PowerPoint
* Images are embedded in the presentation file
## Limitations [#limitations]
The PPTX export may not support all Polotno effects and features. Some advanced styling, animations, or custom elements might not be fully preserved in the exported PowerPoint file.
If you encounter any issues or have feedback about missing features, please share your experience in the [Polotno Community](https://community.polotno.com/c/ask).
PPTX files do **not** embed fonts inside the file. If you use custom fonts in your design, they will be displayed using system fallback fonts when the presentation is opened on another computer, unless those custom fonts are already installed on that system.
For consistent typography across different devices, consider using common system fonts like Arial, Helvetica, Times New Roman, or Georgia.
## Error Handling [#error-handling]
Export failures throw a plain `Error` with a `code` (`DESIGN_INVALID`, `IMAGE_FAILED`, `EXPORT_FAILED`, `FETCH_FAILED`) and structured `details` such as `elementId`. Branch on the code, not the message text — see [Error Handling](/docs/error-handling).
## Live Demo [#live-demo]
Try out the PPTX export feature in this interactive demo:
---
# PSD Import
URL: https://polotno.com/docs/psd-import
The `@polotno/psd-import` package converts Photoshop `.psd` files into Polotno JSON format, enabling you to import PSD designs into Polotno for further editing — text stays editable text, vector shapes stay vector, raster layers stay images.
**Beta Feature:** This library is new and may not work correctly for all PSD files. Please [report issues](https://community.polotno.com/c/ask) to help us improve it.
**Try it without code:** The [PSD to PNG](/tools/psd-to-png), [PSD to JPG](/tools/psd-to-jpg), [PSD to PDF](/tools/psd-to-pdf), [PSD to SVG](/tools/psd-to-svg), [PSD to HTML](/tools/psd-to-html), and [PSD to JSON](/tools/psd-to-json) converters run this exact parser in the browser — drop your own file to test it before integrating.
## Installation [#installation]
```bash
npm install @polotno/psd-import
```
## Basic Usage [#basic-usage]
`psdToJson` accepts an `ArrayBuffer` or `Uint8Array` of PSD bytes and returns a Polotno-schema JSON document.
```typescript
import { psdToJson } from '@polotno/psd-import';
const json = await psdToJson({ psd: buffer });
// Load into Polotno store
store.loadJSON(json);
```
The returned object matches the [Design Format](/docs/schema): `{ width, height, fonts, pages, unit, dpi }`.
## Browser Example [#browser-example]
Read bytes from a file input:
```typescript
import { psdToJson } from '@polotno/psd-import';
async function handleFileUpload(file: File) {
const buffer = await file.arrayBuffer();
const json = await psdToJson({ psd: buffer });
store.loadJSON(json);
}
```
## Node.js Example [#nodejs-example]
Read bytes from disk:
```typescript
import { readFile } from 'node:fs/promises';
import { psdToJson } from '@polotno/psd-import';
const buffer = await readFile('./design.psd');
const json = await psdToJson({ psd: buffer });
store.loadJSON(json);
```
## How It Works [#how-it-works]
The goal is to keep as many of the PSD's layers as **separate, editable elements** as possible, instead of flattening the file into a single image. Each PSD layer is mapped to the closest equivalent in the Polotno schema:
* **Text layers** stay as editable text — font family, weight, style, size, color, alignment, line height, and letter spacing carry over.
* **Shape and vector layers** become SVG elements with their fills, gradients, and strokes preserved.
* **Raster layers** become images. When a layer uses effects the schema can't represent natively (gradient/color overlays, gradient maps, hue/saturation, brightness/contrast, …), the effect is baked into that layer's pixels so it still looks right while every other element remains separate.
* **Masks** and **group effects** are applied to the layers underneath them, so masked content and folder-level overlays render correctly without collapsing the document.
* **Blend modes** pass through where the schema supports them; unsupported modes are flattened only with the layers they depend on.
The PSD's pre-rendered composite image is intentionally **not** used — every element stays selectable and editable.
## Platform Support [#platform-support]
This package works both **client-side (browser)** and **server-side (Node.js)**. All processing runs locally — no data is sent to any external server.
## Demo [#demo]
## Error Handling [#error-handling]
If a file can't be parsed, the importer throws a plain `Error` with `code: 'IMPORT_FAILED'` and `details.format: 'psd'`; the low-level parser error is chained as `error.cause`. See [Error Handling](/docs/error-handling).
## Related Imports [#related-imports]
* [SVG Import](/docs/svg-import) — `@polotno/svg-import` for SVG files
* [PDF Import](/docs/pdf-import) — `@polotno/pdf-import` for PDF and Adobe Illustrator files
* [Import & Export overview](/docs/import-and-export)
## Troubleshooting [#troubleshooting]
**Import produces unexpected results:**
* Try simplifying the PSD before importing (flatten unused groups, rasterize complex smart objects)
* Some advanced Photoshop features (3D layers, video layers, complex layer comps) aren't supported
* Check browser console for warnings
**Missing fonts:**
* Fonts are referenced by family name in the imported design; if the font isn't installed where the design renders, the editor or browser will fall back. Install or load the font before rendering for pixel-perfect output.
**Large files:**
* PSDs up to a few hundred MB usually parse fine in the browser. For very large files or bulk processing, run `psdToJson()` in Node.js where memory limits are higher.
**Need help?** Report issues and get support at [community.polotno.com](https://community.polotno.com/c/ask).
---
# Server-side Image Generation with Node.js
URL: https://polotno.com/docs/server-side-image-generation-with-node-js
## Is it possible to generate images from Polotno JSON on the backend? [#is-it-possible-to-generate-images-from-polotno-json-on-the-backend]
Yes, you can leverage the [polotno-node package](https://www.npmjs.com/package/polotno-node).
Using polotno-node you can use most of the [Polotno Store API](/docs/store). The common usage is image export from Polotno JSON:
```js
const fs = require('fs');
// import polotno-node API
const { createInstance } = require('polotno-node');
async function run() {
// create working instance
const instance = await createInstance({
// to create your own API key please go here: https://polotno.com/cabinet
key: 'nFA5H9elEytDyPyvKL7T',
});
// load sample json
const json = JSON.parse(fs.readFileSync('polotno.json'));
// here you can manipulate JSON somehow manually
// for example replace some images or change text
// then we can convert json into image
const imageBase64 = await instance.jsonToImageBase64(json); // default is PNG
// write image into local file
fs.writeFileSync('out.png', imageBase64, 'base64');
// also we can export design into lower size
// and change image type
const jpegImage = await instance.jsonToImageBase64(json, {
pixelRatio: 0.5, // make image twice smaller
mimeType: 'image/jpeg',
});
fs.writeFileSync('out.jpg', jpegImage, 'base64');
// enable rich text rendering if your design uses it
const richTextImage = await instance.jsonToImageBase64(json, {
richTextEnabled: true, // enable rich text support
});
fs.writeFileSync('out-rich.png', richTextImage, 'base64');
// close instance
instance.close();
}
run();
```
## Error Handling [#error-handling]
Rendering failures inside the headless browser — broken images, missing fonts, invalid designs — are re-thrown on the Node side as plain `Error`s with a `code` (e.g. `IMAGE_FAILED`, `FONT_FAILED`, `DESIGN_INVALID`) and structured `details` such as `elementId`. Branch on the code, not the message text — see [Error Handling](/docs/error-handling).
## Related Pages [#related-pages]
* [Cloud Render API](/docs/cloud-render-api) - Managed rendering service without infrastructure
* [Import and Export](/docs/import-and-export) - Overview of all export formats
* [Store API](/docs/store) - Complete API reference
* [JSON Schema](/docs/schema) - Structure reference for design JSON
---
# SVG Import
URL: https://polotno.com/docs/svg-import
The `@polotno/svg-import` package converts SVG files into Polotno JSON format, enabling you to import vector designs into Polotno for further editing.
**Beta Feature:** This library is new and may not work correctly for all SVG files. Please [report issues](https://community.polotno.com/c/ask) to help us improve it.
**Try it without code:** The [SVG to JSON](/tools/svg-to-json) and [SVG to PDF](/tools/svg-to-pdf) converters run this exact parser in the browser — drop your own file to test it before integrating.
**Importing other formats?** See [PDF Import](/docs/pdf-import) and [PSD Import](/docs/psd-import) for the equivalent packages.
## Installation [#installation]
```bash
npm install @polotno/svg-import
```
## Basic Usage [#basic-usage]
```typescript
import { svgToJson } from '@polotno/svg-import';
const svg = ``;
const json = await svgToJson({ svg });
// Load into Polotno store
store.loadJSON(json);
```
## Recommended Workflow [#recommended-workflow]
A common use case is importing designs from other design tools:
1. **Export from your design tool** — Export your design as SVG from any vector editor (e.g., Figma, Canva, Adobe Illustrator)
2. **Load the SVG file** — Read the SVG content in your application
3. **Convert to Polotno JSON** — Use `svgToJson()` to convert
4. **Load into Polotno** — Use `store.loadJSON()` to load the converted design
```typescript
import { svgToJson } from '@polotno/svg-import';
// Example: loading from file input
async function handleFileUpload(file: File) {
const svgContent = await file.text();
const json = await svgToJson({ svg: svgContent });
store.loadJSON(json);
}
// Example: loading from URL
async function importFromUrl(url: string) {
const response = await fetch(url);
const svgContent = await response.text();
const json = await svgToJson({ svg: svgContent });
store.loadJSON(json);
}
```
`svgToJson` returns a complete, valid [Design Format](/docs/schema) document, ready for `store.loadJSON()`. See the reference for every field and type.
## Text Conversion Limitations [#text-conversion-limitations]
Some design tools (especially Canva and Illustrator) convert text into SVG paths when exporting. These **outlined texts cannot be converted back into editable text** — they will be imported as image data instead.
**To preserve editable text:**
* **Figma:** Use "Export" with SVG format and make sure **"Outline Text" is OFF** in export options. See the [Figma Import guide](/docs/figma-import) for detailed steps.
* **Illustrator:** Avoid "Create Outlines" before export, or use "Preserve Illustrator Editing Capabilities"
* **Canva:** Limited control — some fonts are automatically outlined
If editable text is critical, check your export settings or consider recreating text elements manually in Polotno after import.
## Platform Support [#platform-support]
This package is designed for **client-side (browser) use only**. It may not work correctly in pure Node.js environments.
For server-side SVG processing, consider pre-processing on the client or using a headless browser environment.
## Demo [#demo]
## Error Handling [#error-handling]
If the input is not valid SVG, the importer throws a plain `Error` with `code: 'IMPORT_FAILED'` and `details.format: 'svg'`. See [Error Handling](/docs/error-handling).
## Troubleshooting [#troubleshooting]
**Import produces unexpected results:**
* Simplify the SVG before importing (flatten groups, expand strokes)
* Check if text was converted to paths in the source application
* Try exporting with different settings from your design tool
**Missing elements:**
* Some advanced SVG features may not be supported
* Check browser console for warnings
**Need help?** Report issues and get support at [community.polotno.com](https://community.polotno.com/c/ask).
---
# Video Export
URL: https://polotno.com/docs/video-export
## Overview [#overview]
Polotno supports exporting animated designs to video (MP4) format using **client-side rendering**. This means all video encoding happens directly in the browser without requiring a server. This feature is available through the `@polotno/video-export` package.
For server-side video generation at scale, see [Cloud Render API](/docs/cloud-render-api).
## Installation [#installation]
First, install the video export package:
```bash
npm install @polotno/video-export
```
## Basic Usage [#basic-usage]
Import the package and use the `storeToVideo` function to export your design:
```ts
import { storeToVideo } from '@polotno/video-export';
import { createStore } from 'polotno/model/store';
const store = createStore({ key: 'YOUR_KEY' });
// Export video
const videoBlob = await storeToVideo({
store,
fps: 30, // Frames per second (default: 30)
pixelRatio: 2, // Pixel ratio for quality (default: 1)
onProgress: (progress, frameTime) => {
console.log(`Progress: ${Math.round(progress * 100)}%`);
console.log(`Frame render time: ${frameTime}ms`);
},
});
// Download the video
const url = URL.createObjectURL(videoBlob);
const link = document.createElement('a');
link.href = url;
link.download = 'video.mp4';
link.click();
```
## API Reference [#api-reference]
### `storeToVideo(options)` [#storetovideooptions]
Exports a Polotno design to video format.
**Parameters:**
* `store` (required): The Polotno store instance
* `fps` (optional): Frames per second for the video (default: `30`)
* `pixelRatio` (optional): Pixel ratio for quality control (default: `1`)
* `onProgress` (optional): Callback function for progress tracking
* `progress`: Number between 0 and 1 representing export progress
* `frameTime`: Time in milliseconds to render the current frame
* `includeAudio` (optional): Include audio tracks from the design in the exported video
* `signal` (optional): An `AbortSignal` to cancel video generation. When aborted, `storeToVideo` rejects with an `AbortError`.
**Returns:** Promise that resolves to a Blob containing the video file
### Cancelling an Export [#cancelling-an-export]
Use an `AbortController` to cancel a long-running export:
```ts
const controller = new AbortController();
// Cancel after 10 seconds
setTimeout(() => controller.abort(), 10_000);
try {
const videoBlob = await storeToVideo({
store,
signal: controller.signal,
});
} catch (err) {
if (err.name === 'AbortError') {
console.log('Export was cancelled');
}
}
```
## Use Cases [#use-cases]
* Create video content from animated designs
* Export presentations as video files
* Generate social media videos
* Create video templates with animations
* Export multi-page designs as video sequences
## Notes [#notes]
All video encoding happens **directly in the browser** using client-side rendering. This means:
* No server required for video processing
* All processing happens on the user's device
* Export speed depends on the user's hardware capabilities
* Large videos or high FPS may take longer to process
## Multi-Page Designs with Different Dimensions [#multi-page-designs-with-different-dimensions]
Video files require a single fixed resolution throughout playback. When your store contains pages with different dimensions, `storeToVideo` determines the output resolution by taking the **maximum width and height** across all pages (rounded to even numbers for H.264 codec compatibility).
During export, all pages are temporarily resized to match this output resolution. The resize is smart — it preserves relative positions and sizes of elements, so the layout is mostly maintained. Original page dimensions are restored after the export completes.
For best results, use pages with **similar dimensions**. If your store mixes very different aspect ratios (e.g. a landscape page and a portrait page), the resized layout may not look as intended.
## Error Handling [#error-handling]
Export failures throw a plain `Error` with a `code` (`VIDEO_FAILED`, `FETCH_FAILED`, `EXPORT_FAILED`) and structured `details` — `details.reason` distinguishes cases like `no-video-track`, `unsupported-codec`, or `zero-duration`. Branch on the code, not the message text — see [Error Handling](/docs/error-handling).
## Common Questions [#common-questions]
### How long does video export take? [#how-long-does-video-export-take]
Export time depends on video length, complexity, and user hardware. Higher FPS and pixel ratios increase render time.
### Can I export videos server-side? [#can-i-export-videos-server-side]
Yes. Use the [Cloud Render API](/docs/cloud-render-api) for server-side video generation with consistent performance.
### Does this work with animations? [#does-this-work-with-animations]
Yes. Enable animations first with `setAnimationsEnabled(true)`. See [Animations and Videos](/docs/animations-and-videos) for details.
## Live Demo [#live-demo]
Try out the video export feature in this interactive demo:
---
# AI Text
URL: https://polotno.com/docs/ai-text
Polotno ships with an optional AI assistant that helps users draft, expand, and rewrite text elements. The feature is opt-in and stays hidden until you enable it in your application configuration.
## Enable AI text tools [#enable-ai-text-tools]
```ts
import { setAiTextEnabled } from 'polotno/config';
setAiTextEnabled(true);
```
AI tools are disabled by default. Call `setAiTextEnabled(true)` during app initialization to expose the AI actions in the text side panel and text toolbar.
## Requirements [#requirements]
* **Subscription**: Valid Polotno API key with AI text access.
* **Network**: Client devices must reach Polotno's API endpoint.
* **Compliance**: Review your legal obligations before allowing users to submit prompts and generated content.
* **Pricing**: Included in the Polotno subscription plan—no additional per-request fees.
## User experience [#user-experience]
After activation, selecting a text element reveals an **AI write** button in the floating toolbar and in the inline text tooltip. Clicking it opens preset prompts such as "Rewrite", "Shorten", or "Continue writing". Generated content updates the currently selected text element immediately, and users can continue editing the result like any other text.
---
# Animations and Videos
URL: https://polotno.com/docs/animations-and-videos
## How to enable animation support? [#how-to-enable-animation-support]
```ts
import { setAnimationsEnabled } from 'polotno/config';
setAnimationsEnabled(true);
```
When you enable animations, Polotno will add additional UI in the toolbar to change animation properties of the selected object, show a “Videos” side panel with a library of stock videos, and adapt the Pages component with scene preview.
## Group animations [#group-animations]
A group can own its own enter, exit, and loop animations directly — selecting a group shows the same animation controls as a single element. Group animations are **composed on top of** each child's own animation, so you can animate a whole group (for example, slide it in) without setting up each child individually, while children keep any animations of their own.
You can preview animations and export an animated scene as a GIF on the client side programmatically:
```ts
store.play();
store.stop();
await store.saveAsGIF();
```
## Video Export Options [#video-export-options]
Polotno supports two methods for exporting animated designs to MP4 video:
### Client-Side Video Export [#client-side-video-export]
For browser-based video generation without a server, use the `@polotno/video-export` package. All encoding happens directly in the user's browser:
```bash
npm install @polotno/video-export
```
```ts
import { storeToVideo } from '@polotno/video-export';
const videoBlob = await storeToVideo({
store,
fps: 30,
pixelRatio: 2,
onProgress: (progress) => {
console.log(`Progress: ${Math.round(progress * 100)}%`);
},
});
// Download the video
const url = URL.createObjectURL(videoBlob);
const link = document.createElement('a');
link.href = url;
link.download = 'video.mp4';
link.click();
```
Export speed depends on user hardware. High FPS or complex designs may take longer to process.
See the full [Video Export documentation](/docs/video-export) for complete API reference and live demo.
### Server-Side Video Export [#server-side-video-export]
For production-grade video rendering with consistent performance, use the [Cloud Render API](/docs/cloud-render-api). This option provides faster rendering and doesn't depend on client device capabilities.
## Audio Support [#audio-support]
Add background music or sound effects to your animated designs. Audio tracks sync with the video timeline and export together with animations.
See the [Audio documentation](/docs/audio) for complete details on adding and managing audio tracks.
## Live demo [#live-demo]
## Related Pages [#related-pages]
* [Video Export](/docs/video-export) - Detailed client-side video export documentation
* [Audio](/docs/audio) - Add background music to animated designs
* [Cloud Render API](/docs/cloud-render-api) - Server-side video rendering
* [Import and Export](/docs/import-and-export) - Overview of all export formats
---
# Audio Support
URL: https://polotno.com/docs/audio
## Overview [#overview]
Polotno supports adding audio tracks to designs for video export. Audio files play during video rendering and can be trimmed, delayed, and volume-adjusted. Multiple audio tracks can be added to a single design.
Audio is stored at the **store level**, not per page. All pages/scenes share the same audio timeline when exporting to video. Combine audio with [animations](/docs/animations-and-videos) for complete multimedia designs.
## Adding Audio from the UI [#adding-audio-from-the-ui]
Users can upload audio files through the default **Upload** side panel:
1. Click the "Upload" section in the side panel
2. Select an audio file (MP3, WAV, OGG, etc.)
3. Click on added audio file or drag it to the workspace to add it to the design
4. The audio track appears in the **PagesTimeline** component at the bottom
5. Click the audio track in the timeline to trim, adjust volume, mute, duplicate, or remove it
## Programmatic API [#programmatic-api]
Add audio tracks programmatically using the Store API:
```ts
import { createStore } from 'polotno/model/store';
const store = createStore({ key: 'YOUR_KEY' });
// Add audio with default settings
store.addAudio({
src: 'https://example.com/audio.mp3',
});
// Add audio with custom properties
store.addAudio({
src: 'https://example.com/background-music.mp3',
volume: 0.5, // 50% volume (0 to 1)
delay: 2000, // Start after 2 seconds of video
startTime: 0.25, // Start from 25% into the audio file (0 to 1)
endTime: 0.75, // End at 75% into the audio file (0 to 1)
});
```
### Accessing and Removing Audio [#accessing-and-removing-audio]
```ts
// List all audio tracks
store.audios.forEach((audio) => {
console.log(audio.id, audio.src, audio.volume);
});
// Remove audio by ID
const audioId = store.audios[0].id;
store.removeAudio(audioId);
```
See the full [Store API documentation](/docs/store#working-with-audio) for complete reference.
## Audio Properties [#audio-properties]
| Property | Type | Range | Description |
| ----------- | ------ | ----- | ---------------------------------------------------- |
| `src` | string | - | URL or data URI of the audio file |
| `volume` | number | 0-1 | Volume level (0 = mute, 1 = full) |
| `delay` | number | 0+ | Milliseconds before audio starts in video timeline |
| `startTime` | number | 0-1 | Relative start point in source file (0.5 = middle) |
| `endTime` | number | 0-1 | Relative end point in source file |
| `duration` | number | - | Duration of source audio in milliseconds (read-only) |
### Use Cases for Audio Properties [#use-cases-for-audio-properties]
**Background music with fade-in timing:**
```ts
store.addAudio({
src: '/music.mp3',
delay: 1000, // Start 1 second into video
volume: 0.3, // Lower volume to not overpower content
});
```
**Use only the middle section of a long audio file:**
```ts
store.addAudio({
src: '/podcast.mp3',
startTime: 0.4, // Skip first 40% of the file
endTime: 0.6, // End at 60% mark
});
```
**Sync audio to specific page timing:**
```ts
const pageDuration = store.pages[0].duration; // in ms
store.addAudio({
src: '/narration.mp3',
delay: pageDuration, // Start when page 2 begins
});
```
## Timeline Controls [#timeline-controls]
When you click an audio track in the **PagesTimeline**, the following controls are available:
* **Volume slider** -- adjust the playback volume of the track (0-100%)
* **Mute/unmute** -- quickly toggle audio on or off without changing the volume setting
* **Duplicate** -- create a copy of the audio track with the same settings
* **Trim** -- drag the edges of the track to set start and end points
* **Remove** -- delete the audio track from the design
These controls work the same way for programmatically added tracks and user-uploaded tracks.
## Export with Audio [#export-with-audio]
Audio tracks are included when exporting to video formats. The audio mixes down to the final video output.
For client-side video generation in the browser, use the [Video Export](/docs/video-export) package. For server-side rendering with consistent performance, use the [Cloud Render API](/docs/cloud-render-api). Both methods process all audio tracks and mix them into the final video.
## Audio Schema [#audio-schema]
Audio tracks are serialized in the design JSON. See the [Audio schema reference](/docs/schema/audio) for the complete data structure.
```json
{
"audios": [
{
"id": "audio_1",
"src": "https://example.com/audio.mp3",
"duration": 180000,
"startTime": 0,
"endTime": 1,
"volume": 1,
"delay": 0
}
]
}
```
## Live Demo [#live-demo]
Try adding audio to a design with this interactive example:
## Notes [#notes]
Most browsers require **user interaction** (click, tap) before playing audio. Programmatic `store.play()` calls may be silent until the user interacts with the page.
Audio files uploaded via the Upload panel are embedded as **base64 data URIs** by default. For production, implement a custom upload panel that stores files on your server and references them by URL. See [Upload Panel documentation](/docs/upload-panel).
---
# Canvas Rulers
URL: https://polotno.com/docs/canvas-rulers
Rulers in Polotno SDK's canvas editor ensure consistent alignment and precision, enhancing design cohesion. By defining guidelines and constraints, they streamline the design process, enabling users to create polished compositions efficiently.
### How to enable rulers? [#how-to-enable-rulers]
To control rulers from code, use the [Store API](/docs/store).
```ts
// hide/show rulers
store.toggleRulers();
// check if rulers are currently visible
console.log(store.rulersVisible);
```
## Ruler styling [#ruler-styling]
Customize the ruler appearance through [Workspace](/docs/workspace) props:
```tsx
```
All six props are optional and fall back to the built-in theme when omitted.
## Live demo [#live-demo]
---
# Custom Elements
URL: https://polotno.com/docs/custom-elements
**Important:** this is an experimental feature, please proceed with caution. We're kindly asking you to report any bugs and follow the [changelog](https://community.polotno.com/c/changelog) for the most recent updates.
Custom elements allow you to create your own shapes and add them to the canvas.
By design Polotno supports several main types of elements: `text`, `image`, `svg`, `line`, `figure`, `video`, `table`, `group`. In some cases you may want to create your own custom elements.
Before you start, please note that:
1. Custom elements are not supported in [Cloud Render API](/docs/cloud-render-api).
2. By default [polotno-node](/docs/server-side-image-generation-with-node-js) (for backend rendering) does not support custom elements. But with some [extra configuration](https://github.com/polotno-project/polotno-node#your-own-client) you can make it work.
3. You will need to know some Polotno internals to support animations for custom elements. Please write us if you need help.
4. In the future there will be more options for design export (e.g. to SVG, print‑ready PDF, etc). You will need to write some adapters to make it work with custom elements.
5. If possible, try to use built‑in elements. For example you can draw some complex shapes using `svg` element (you can generate `src` at runtime).
## How to create custom shapes with Polotno? [#how-to-create-custom-shapes-with-polotno]
As a demonstration, we will create a custom star element. Creating new elements consists of three main steps.
### 1. Create model for your element [#1-create-model-for-your-element]
First define any additional attributes for the new element. All the basic attributes such as `id`, `x`, `y`, `rotation`, filters attributes, etc. are defined by default. You only need to define extra fields and their defaults:
```ts
import { unstable_registerShapeModel } from 'polotno/config';
unstable_registerShapeModel(
// define properties
{
type: 'star',
radius: 100,
fill: 'black',
numPoints: 6,
},
// optional extend function
(starModel) => {
// starModel is a model from mobx-state-tree
// we can define some additional methods here
// and return it back
return starModel.actions((self) => {
return {
setNumPoints(numPoints) {
self.numPoints = numPoints;
},
};
});
}
);
```
Now Polotno store knows that we can define a `star` model.
### 2. Create React component for new element [#2-create-react-component-for-new-element]
Define how to display the model. Create a React component with react‑konva shapes.
```tsx
// polotno is made with mobx library
// we will need its tools to make reactive components
import { observer } from 'mobx-react-lite';
// import Konva components
import { Star } from 'react-konva';
import { unstable_registerShapeComponent } from 'polotno/config';
// now we need to define how element looks on canvas
export const StarElement = observer(({ element, store }) => {
const ref = React.useRef(null);
const handleChange = (e: any) => {
const node = e.currentTarget;
const scaleX = node.scaleX();
// Konva.Transformer changes scale by default; reset it
node.scaleX(1);
node.scaleY(1);
// save changes back to the model
element.set({
x: node.x(),
y: node.y(),
rotation: e.target.rotation(),
radius: element.radius * scaleX,
});
};
// Important: element.x and element.y must define top-left corner of the shape
// so positions are consistent across all elements
return (
);
});
// register new component to draw our star
unstable_registerShapeComponent('star', StarElement);
```
### 3. Create custom top toolbar (optional) [#3-create-custom-top-toolbar-optional]
A custom toolbar can be defined to change star properties.
```tsx
import React from 'react';
import { observer } from 'mobx-react-lite';
import { NumericInput, Navbar } from 'polotno/primitives';
import ColorPicker from 'polotno/toolbar/color-picker';
import { unstable_registerToolbarComponent } from 'polotno/config';
const StarToolbar = observer(({ store }) => {
const element = store.selectedElements[0];
return (
element.set({
fill,
})
}
store={store}
/>
{
element.set({ radius });
}}
value={element.radius}
min={1}
max={200}
/>
);
});
unstable_registerToolbarComponent('star', StarToolbar);
```
## Live demo [#live-demo]
---
# Design Validation
URL: https://polotno.com/docs/design-validation
You can use the [Store API](/docs/store) to inspect each element's properties on the page, enabling thorough design checks and validation to custom standards. Define your own validation logic and display it however best fits your use case.
### Why Use Design Validation? [#why-use-design-validation]
Here are several common use cases where design validation can enhance the design workflow and final output:
1. **Compliance with Brand Guidelines**
Ensure elements meet brand standards (e.g., font, color, size) by validating attributes against predefined values. For example, check if all text elements use a specific font or approved color palette.
2. **Image Quality Control**
Validate image resolutions to confirm they meet print or screen quality standards, avoiding low‑quality images in high‑resolution exports.
3. **Layout Checks for Export**
Review layout constraints to ensure elements are within the visible canvas and aligned correctly, preventing cut‑off or misplaced content in the final export.
4. **Content Accessibility**
Check text elements for minimum font size, contrast, and positioning to ensure readability and accessibility compliance.
### Getting Started with the Store API [#getting-started-with-the-store-api]
The Store API gives you direct access to every element's properties on the canvas, making it easy to build custom validation checks for your specific needs:
```ts
const errors: { message: string; elementId: string }[] = [];
// Use "find" to iterate through all elements
store.find((item) => {
// Perform validation checks on element properties
if (item.type === 'text') {
if (item.text.length > 300) {
errors.push({
message: 'Too long text',
elementId: item.id,
});
}
}
});
// then surface errors in your UI (e.g., toolbar, toast, side panel)
```
With Polotno’s Store API, you gain powerful control over your design quality, ensuring that each element aligns with your project standards before exporting or publishing.
## Demo [#demo]
In the demo there is a simple validation for position, font size, and image quality. The toolbar shows validation status. Click an issue to select the related element on the canvas.
---
# Drawing Tool
URL: https://polotno.com/docs/drawing
The drawing tool allows users to create freehand drawings directly on the canvas. Drawings are automatically converted into SVG elements that can be edited, styled, and manipulated like any other element.
## How it works [#how-it-works]
**Default mode**: Polotno operates in `selection` mode, where users can click and select elements on the canvas.
**Drawing mode**: When activated, the canvas becomes a drawing area:
* All objects become non-clickable
* The entire canvas acts as a drawing surface
* Each stroke creates a new SVG element
* Users can change colors and properties after creation
## Activating drawing mode [#activating-drawing-mode]
Use the [Store API](/docs/store) to control the drawing tool programmatically.
```ts
// enable drawing mode
store.setTool('draw');
// return to selection mode
store.setTool('selection');
```
## Configuring drawing options [#configuring-drawing-options]
Before or during drawing mode, configure stroke properties:
```ts
store.setToolOptions({
strokeWidth: 50, // brush size in pixels
stroke: 'red', // stroke color (any CSS color)
opacity: 0.4 // opacity from 0 to 1
});
```
## Full example [#full-example]
```ts
import { createStore } from 'polotno/model/store';
const store = createStore({ key: 'YOUR_API_KEY' });
// configure drawing options
store.setToolOptions({
strokeWidth: 10,
stroke: '#0066ff',
opacity: 1
});
// enable drawing
store.setTool('draw');
// later, return to selection mode
store.setTool('selection');
```
## Editing drawn elements [#editing-drawn-elements]
After drawing, each stroke becomes an SVG element that users can:
* Select and move
* Resize and rotate
* Change fill color via properties panel
* Delete or duplicate
* Apply filters and effects
## Checking current tool [#checking-current-tool]
```ts
// check which tool is currently active
console.log(store.tool); // 'draw' or 'selection'
// check current tool options
console.log(store.toolOptions);
```
## Use cases [#use-cases]
* **Annotations**: Mark up designs with freehand notes
* **Signatures**: Capture digital signatures
* **Sketching**: Quick concept sketches over designs
* **Highlighting**: Draw attention to specific areas
* **Creative effects**: Add hand-drawn elements to designs
---
# Element Locking
URL: https://polotno.com/docs/element-locking
Element locking provides granular control over which aspects of an element users can modify. Polotno offers **6 independent locking properties** for precise interaction control.
**Quick start:** Use [User Roles](/docs/user-roles) with `store.setRole('admin')` for built-in locking UI. If you need custom controls, this page shows how to build your own.
## Locking properties [#locking-properties]
| Property | Controls |
| ----------------- | ------------------------------------ |
| `selectable` | Can element be selected and clicked? |
| `draggable` | Can element be moved or rotated? |
| `resizable` | Can element be resized? |
| `removable` | Can element be deleted? |
| `contentEditable` | Can element's content be changed? |
| `styleEditable` | Can element's style be changed? |
All properties default to `true`.
## Basic usage [#basic-usage]
Lock an element programmatically:
```ts
element.set({
draggable: false, // Cannot move
resizable: false, // Cannot resize
removable: false, // Cannot delete
contentEditable: false, // Cannot edit content
styleEditable: false, // Cannot change style
});
console.log(element.locked); // true
```
## Common patterns [#common-patterns]
**Template placeholder** — Users edit text, designer controls style:
```ts
element.set({
contentEditable: true, // Can edit text
styleEditable: false, // Cannot change appearance
draggable: false,
resizable: false,
});
```
**Non-interactive overlay** — Watermark, guide, or decoration:
```ts
element.set({
selectable: false, // Clicks pass through
draggable: false,
removable: false,
alwaysOnTop: true,
});
```
**Fixed background** — Cannot move or delete, but can replace:
```ts
element.set({
draggable: false,
removable: false,
contentEditable: true, // Can change image
});
```
## Custom locking UI demo [#custom-locking-ui-demo]
This example demonstrates how to build custom lock controls without using admin mode. Select elements and use the 🔒 Locks tab to control their properties:
## Related [#related]
* [User Roles](/docs/user-roles) — Admin mode for accessing locked elements
* [Overlays and Watermarks](/docs/overlays-and-watermarks) — Using `selectable`, `alwaysOnTop`, and `showInExport`
* [Element API](/docs/element) — Complete element property reference
---
# Fonts
URL: https://polotno.com/docs/fonts
Polotno supports three distinct font systems, each serving different purposes:
| Type | Stored in JSON | User can remove | Use case |
| ---------------- | -------------- | --------------- | ----------------------- |
| **Google Fonts** | No | No | Default font library |
| **Design Fonts** | Yes | Yes | Per-design custom fonts |
| **Global Fonts** | No | No | App-wide custom fonts |
## Google Fonts [#google-fonts]
By default, Polotno provides access to a large library of [Google Fonts](https://fonts.google.com/). These fonts appear in the font dropdown in the Text side panel.
### Customize the Google Fonts list [#customize-the-google-fonts-list]
```ts
import { setGoogleFonts } from 'polotno/config';
// limit to specific fonts only
setGoogleFonts(['Roboto', 'Open Sans', 'Lato']);
// disable Google Fonts entirely
setGoogleFonts([]);
// restore the full default list
setGoogleFonts('default');
```
### Customize font variants [#customize-font-variants]
By default, Polotno loads regular, italic, bold, and bold italic styles. You can customize which variants to load:
```ts
import { setGoogleFontsVariants } from 'polotno/config';
// default variants
setGoogleFontsVariants('400,400italic,700,700italic');
// load only regular and thin
setGoogleFontsVariants('400,100');
```
### Get Google Fonts API endpoints [#get-google-fonts-api-endpoints]
```ts
import { getGoogleFontsListAPI, getGoogleFontImage } from 'polotno/config';
// get URL to fetch the fonts list
const listUrl = getGoogleFontsListAPI();
// get preview image URL for a specific font
const previewUrl = getGoogleFontImage('Roboto');
```
```tsx
// example usage in JSX
```
## Design Fonts [#design-fonts]
Design fonts are custom fonts attached to a specific design (JSON file). When you export with `store.toJSON()`, these fonts are included in the `fonts` array of the output.
**Key characteristics:**
* Stored in `store.fonts` and exported with `store.toJSON()`
* Visible in the fonts dropdown in the Text side panel
* Users can remove them via the default UI (removes the reference from JSON)
* Ideal for per-design or per-user custom fonts
### Add a design font [#add-a-design-font]
```ts
// simple usage with URL
store.addFont({
fontFamily: 'MyCustomFont',
url: 'https://example.com/fonts/MyFont.ttf',
});
// full control over font styles
store.addFont({
fontFamily: 'MyCustomFont',
styles: [
{
src: 'url("https://example.com/fonts/MyFont-Regular.ttf")',
fontStyle: 'normal',
fontWeight: 'normal',
},
{
src: 'url("https://example.com/fonts/MyFont-Bold.ttf")',
fontStyle: 'normal',
fontWeight: 'bold',
},
{
src: 'url("https://example.com/fonts/MyFont-Italic.ttf")',
fontStyle: 'italic',
fontWeight: 'normal',
},
],
});
// register a font already loaded via CSS
store.addFont({
fontFamily: 'MyCustomFont',
});
```
### Remove a design font [#remove-a-design-font]
```ts
store.removeFont('MyCustomFont');
```
### Load a font for rendering [#load-a-font-for-rendering]
Text elements on the canvas load fonts automatically. Use this method when you need to render fonts elsewhere in your UI (e.g., font picker preview):
```ts
await store.loadFont('MyCustomFont');
```
## Global Fonts [#global-fonts]
Global fonts are custom fonts registered at the application level. They are **not** stored in the design JSON, which keeps exports smaller. You control when and how to register these fonts.
**Key characteristics:**
* NOT included in JSON export (smaller file size)
* Developer controls when to add/remove them
* Can serve all users or specific users (your logic decides)
* Users cannot remove them via the default UI
* Must be manually added to JSON for [Cloud Render](/docs/cloud-render-api)
### Add a global font [#add-a-global-font]
```ts
import { addGlobalFont } from 'polotno/config';
// simple usage with URL
addGlobalFont({
fontFamily: 'BrandFont',
url: 'https://example.com/fonts/BrandFont.ttf',
});
// full control over font styles
addGlobalFont({
fontFamily: 'BrandFont',
styles: [
{
src: 'url("https://example.com/fonts/BrandFont-Regular.ttf")',
fontStyle: 'normal',
fontWeight: 'normal',
},
{
src: 'url("https://example.com/fonts/BrandFont-Bold.ttf")',
fontStyle: 'normal',
fontWeight: 'bold',
},
],
});
// register a font already loaded via CSS on the page
addGlobalFont({
fontFamily: 'BrandFont',
});
```
### Remove a global font [#remove-a-global-font]
```ts
import { removeGlobalFont } from 'polotno/config';
removeGlobalFont('BrandFont');
```
### Replace all global fonts [#replace-all-global-fonts]
```ts
import { replaceGlobalFonts } from 'polotno/config';
replaceGlobalFonts([
{ fontFamily: 'Font1', url: 'https://example.com/font1.ttf' },
{ fontFamily: 'Font2', url: 'https://example.com/font2.ttf' },
]);
```
### Frameworkless integration [#frameworkless-integration]
For iframe or frameworkless integrations, use the global config object:
```ts
window.polotnoConfig.addGlobalFont({
fontFamily: 'BrandFont',
url: 'https://example.com/fonts/BrandFont.ttf',
});
```
## Global Fonts and Cloud Render [#global-fonts-and-cloud-render]
Global fonts are **not** included in the JSON export. If you use [Cloud Render API](/docs/cloud-render-api), designs using global fonts will fail to render correctly unless you manually include the fonts.
Before sending JSON to Cloud Render, add the required global fonts to the `fonts` array:
```ts
const json = store.toJSON();
// manually add global fonts used in this design
json.fonts = [
...json.fonts,
{
fontFamily: 'BrandFont',
url: 'https://example.com/fonts/BrandFont.ttf',
},
];
// send to Cloud Render
await fetch('https://api.polotno.com/api/render', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
design: json,
outputFormat: 'png',
// ... other options
}),
});
```
## When to Use Each Font Type [#when-to-use-each-font-type]
| Scenario | Recommended type |
| ---------------------------------------- | -------------------------------- |
| Default fonts for all users | Google Fonts or Global Fonts |
| Brand fonts for your app | Global Fonts |
| User-uploaded custom fonts | Design Fonts |
| Fonts that must persist in exported JSON | Design Fonts |
| Fonts for specific users (e.g., premium) | Global Fonts (add conditionally) |
| Keeping JSON files small | Global Fonts |
## Font Upload Configuration [#font-upload-configuration]
By default, users can upload fonts from the Text side panel. These are converted to Base64 and stored as design fonts.
To upload fonts to your server instead:
```ts
import { setFontUploadFunc } from 'polotno/config';
async function uploadFont(localFile: File): Promise {
const formData = new FormData();
formData.append('file', localFile);
const res = await fetch('https://your-server.com/upload-font', {
method: 'POST',
body: formData,
});
const { url } = await res.json();
return url; // return the URL where the font is hosted
}
setFontUploadFunc(uploadFont);
```
## Related [#related]
* [Fonts & Text Consistency](/docs/fonts-consistency) — troubleshoot rendering differences across browsers and platforms
* [Store API](/docs/store#working-with-fonts) — full API reference for `store.addFont`, `store.removeFont`, `store.loadFont`
* [Cloud Render API](/docs/cloud-render-api) — server-side rendering with custom fonts
---
# Image Downscaling
URL: https://polotno.com/docs/image-downscaling
By default, Polotno uses an enhanced downscaling algorithm when images are scaled down on the canvas. This prevents common rendering artifacts that occur with the browser's default image scaling.
## The Problem [#the-problem]
When you scale down high-resolution images using the HTML5 canvas element, browsers use simple bilinear interpolation by default. This can produce visual artifacts like:
* Pixelation and jagged edges
* Moiré patterns on detailed images
* Loss of sharpness and clarity
This is especially noticeable when scaling images down significantly (e.g., displaying a 2000×2000px image at 200×200px).
## The Solution [#the-solution]
Polotno applies a multi-step downscaling algorithm that progressively reduces image size, producing smoother and higher-quality results. This technique is widely recommended for canvas-based image manipulation.
## Configuration [#configuration]
The downscaling feature is **enabled by default**. You can disable it if needed:
```ts
import { setDownScalingEnabled } from 'polotno/config';
// disable enhanced downscaling (use browser default)
setDownScalingEnabled(false);
// re-enable enhanced downscaling (default)
setDownScalingEnabled(true);
```
## When to Disable [#when-to-disable]
Consider disabling downscaling if:
* Performance is critical and you need faster rendering
* You're working with small images that don't benefit from the algorithm
* You prefer the browser's native rendering behavior
## Performance Impact [#performance-impact]
Enhanced downscaling uses additional CPU time during image rendering. For most use cases, the quality improvement outweighs the performance cost. On slower devices with many large images, disabling this feature may improve responsiveness.
---
# Overlays and watermarks
URL: https://polotno.com/docs/overlays-and-watermarks
In graphic design, controlling the behavior of elements is crucial for creating professional and efficient designs. Special properties like `selectable: false`, `alwaysOnTop: true`, and `showInExport: false` provide flexibility to manage overlays, watermarks, and guides seamlessly within your projects.
Common use cases:
* **Watermarks**: Protect designs while keeping them non-interactive.
* **Overlays**: Display information or graphics that stay visible without interfering with design interaction.
* **User Tips**: Provide guidance within the design that doesn't export with the final product.
* **Cutting Edges**: Keep guide marks visible only during the design process.
Here is how you can create such elements:
```ts
store.activePage.addElement({
type: 'image',
src: 'path-to-your-watermark-or-overlay.png',
// make it non-selectable
selectable: false,
// keep it always on top
alwaysOnTop: true,
// hide it in export
showInExport: false,
});
```
## Example [#example]
In the demo we add a dashed red shape on top of the design and make it non-selectable, always on top, and not visible in export. A regular user can't change it, but it is visible on the screen.
To control these properties via UI, use [User Roles](/docs/user-roles) admin mode or build [custom locking controls](/docs/element-locking).
## Live demo [#live-demo]
---
# Page Bleed
URL: https://polotno.com/docs/page-bleed
In printing, bleed is printing that goes beyond the edge of where the sheet will be trimmed.
Polotno has native support for bleed. Every page has its own `bleed` property. By default bleeds are not visible on the workspace and export. To show bleed on the canvas:
```ts
store.activePage.set({ bleed: 20 }); // set bleed in pixels
// show bleed area on the
store.toggleBleed(true);
```
The canvas will have special space around the page where elements can extend beyond the trim line.
Users can also change bleed from the built-in **Size** side panel.
## Per-side bleed [#per-side-bleed]
Use `bleedTop`, `bleedRight`, `bleedBottom`, and `bleedLeft` to override the uniform `bleed` value for individual sides:
```ts
store.activePage.set({
bleed: 20, // base value for all sides
bleedBottom: 40, // override just the bottom side
});
```
Per-side values are honored on the canvas and in all exports.
## Exporting with Bleed [#exporting-with-bleed]
By default, exports do not include the bleed area. Use `includeBleed` option to export with bleed:
```ts
// export PDF with bleed
await store.saveAsPDF({ includeBleed: true });
// export image with bleed
await store.saveAsImage({ includeBleed: true });
// as data URL
await store.toDataURL({ includeBleed: true });
```
The built-in **Download** button includes bleed in its PDF exports automatically.
For detailed information on PDF exports with bleed and crop marks, see [PDF Export](/docs/pdf-export).
## Live demo [#live-demo]
---
# Remove Image Background
URL: https://polotno.com/docs/remove-image-background
Polotno provides built-in UI for removing image backgrounds using AI. This feature integrates with the [Remove Background API](/docs/remove-background-api) and appears in the image toolbar once enabled.
## Enable the toolbar button [#enable-the-toolbar-button]
```ts
import { setRemoveBackgroundEnabled } from 'polotno/config';
setRemoveBackgroundEnabled(true);
```
The "Remove Background" button is **disabled by default** for all plans. Call `setRemoveBackgroundEnabled(true)` during app initialization to expose it.
* **Pricing**: Additional usage charges apply. See [pricing details](https://polotno.com/sdk/pricing) for rates
## Disable the Feature [#disable-the-feature]
If you want to hide the "Remove Background" button later:
```tsx
import { Toolbar } from 'polotno/toolbar/toolbar';
null,
}}
/>
```
## Requirements [#requirements]
1. **API Key**: Valid Polotno API key with Business plan (or Team plan with feature enabled)
2. **Network Access**: Internet connectivity required to call the cloud API
## How It Works [#how-it-works]
Once enabled, users will see a "Remove Background" button in the image element toolbar. Clicking it:
1. Sends the image to Polotno's cloud API
2. Processes the image with AI background removal
3. Returns a transparent PNG
4. Updates the canvas element with the result
For programmatic background removal without the UI, use the [Remove Background API](/docs/remove-background-api) directly:
```ts
const req = await fetch(
'https://api.polotno.com/api/remove-image-background?KEY=YOUR_API_KEY',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: 'path-to-image' }),
}
);
const res = await req.json();
// returns base64 data URL with transparent background
const newImageURL = res.url;
```
**Image Storage**: By default, the API returns a base64-encoded image which is saved directly into your design JSON. This increases JSON size significantly. **It is strongly recommended** to upload the processed image to your own cloud storage and store only the URL in the design JSON.
Example with cloud upload:
```ts
// after getting the base64 result
const res = await req.json();
const base64Image = res.url;
// upload to your cloud storage
const uploadedURL = await uploadToYourCloud(base64Image);
// update the element with the cloud URL instead of base64
element.set({ src: uploadedURL });
```
## Limitations [#limitations]
* Internet connection required
* Processing time varies based on image size and complexity
* Works best with clear subject separation from background
## Error Handling [#error-handling]
If the API call fails (network error, quota exceeded, invalid key), users will see an error message in the UI. Ensure your API key is valid and your subscription is active.
## Security & Privacy [#security--privacy]
**Data Storage**: Polotno does not store processed images. All images are processed transiently and deleted immediately after processing.
**Third-Party Service**: Background removal uses [Replicate](https://replicate.com/) as the processing provider. By using this feature, processed images are sent to Replicate's infrastructure.
**Terms**: Review [Replicate's Terms of Service](https://replicate.com/terms) if you have compliance requirements about where image data is processed.
---
# Rich Text
URL: https://polotno.com/docs/rich-text
A single text element can hold **multiple fonts, sizes, colors, weights, styles, and decorations** per span — you can style parts of the text independently. Internally, rich text is stored as an HTML string in `element.text`.
```ts
store.activePage.addElement({
type: 'text',
text: 'Hello from richtextsupport!',
y: 300,
x: store.width / 2 - 200,
fontSize: 80,
width: 400,
});
```
**Rich text now works out of the box — there is nothing to enable.** As of Polotno 3.x / 4.x, the new text rendering engine is the default and renderer switching is gone. The old `setRichTextEnabled` toggle is now a **no-op**, kept only so existing code doesn't break. The legacy `unstable_useHtmlTextRender` flag and the `htmlTextRenderEnabled` parameter are deprecated too.
## Reading and writing text format [#reading-and-writing-text-format]
The recommended way to read and change formatting is the headless format API. It works on any text element without touching the editor's internals.
```ts
import { getTextFormat, applyTextFormat } from 'polotno/utils/text-format';
// read the effective format of an element
const format = getTextFormat(element);
// each key reports whether the value is uniform or mixed across spans:
// { value, mixed, values }
console.log(format.fontWeight.value); // numeric weight, e.g. 400 or 700; undefined if mixed
console.log(format.fill.mixed); // true when the element uses more than one color
// apply a GLOBAL change — sets the element-level prop and strips inline
// per-span overrides so the new value shows uniformly
applyTextFormat(element, { fontWeight: 700 }); // any numeric weight; 'bold'/'normal' work as aliases
// works on an array too (batched into a single undo step)
applyTextFormat(store.selectedElements, { fill: '#ff0000', fontSize: 48 });
```
The formattable keys are `fontSize`, `fontFamily`, `fill`, `fontWeight`, `fontStyle`, and `textDecoration`.
### Build a format toolbar (React) [#build-a-format-toolbar-react]
The `useTextFormat` hook subscribes to the active selection and re-renders on change. When a text element is in edit mode, `applyTextFormat` applies to the current in-editor selection; otherwise it applies to the whole selected element(s).
```tsx
import { observer } from 'mobx-react-lite';
import { useTextFormat } from 'polotno/utils/text-format-state';
const FormatBar = observer(({ store }) => {
const { format, enabled, applyTextFormat } = useTextFormat(store);
if (!enabled) return null;
const isBold = format.fontWeight.value >= 600;
return (
);
});
```
### Vue / vanilla JS [#vue--vanilla-js]
For non-React UIs, use `getTextFormatState` to read once, or `observeTextFormatState` to subscribe (it fires immediately and on every relevant change, and returns an unsubscribe function).
```ts
import {
getTextFormatState,
observeTextFormatState,
} from 'polotno/utils/text-format-state';
// read once
const { format, enabled, applyTextFormat } = getTextFormatState(store);
// or subscribe
const unsubscribe = observeTextFormatState(store, (state) => {
// update your UI from state.format / state.enabled
// call state.applyTextFormat({ ... }) to change formatting
});
```
## Font size and inline overrides [#font-size-and-inline-overrides]
A text element has a base `fontSize`, and individual spans may carry their own inline sizes. How you change the size depends on what you want to happen to those inline sizes:
**Make the whole element one uniform size** — `applyTextFormat` sets the base size and strips inline per-span sizes:
```ts
import { applyTextFormat } from 'polotno/utils/text-format';
applyTextFormat(element, { fontSize: 24 });
```
**Scale every span proportionally** — keep the relative differences between spans and scale them all by a factor with the `scaleRichTextFontSizesInHtml` helper, updating `fontSize` and `text` together:
```ts
import { scaleRichTextFontSizesInHtml } from 'polotno/utils/rich-text-html';
const factor = 24 / element.fontSize;
element.set({
fontSize: 24,
text: scaleRichTextFontSizesInHtml(element.text, factor),
});
```
**Change only the base size, leaving inline sizes untouched** — a plain `set`:
```ts
element.set({ fontSize: 24 });
```
## Advanced: direct Quill access [#advanced-direct-quill-access]
In most cases the [format API above](#reading-and-writing-text-format) is all you need. For lower-level control, Polotno uses [Quill](https://quilljs.com/) under the hood. When a text element is in "edit mode" (the user double‑clicks it) you can reach the live editor:
The Quill `bold` format used in the examples below is deprecated. It still works as an alias, but prefer the numeric `font-weight` format, e.g. `quill.format('font-weight', '700')`.
```ts
import { quillRef } from 'polotno/canvas/html-element';
// will log observable object with format of current selection
console.log(quillRef.currentFormat);
// will log current Quill editor instance. if text is not in edit mode, it will return null
console.log(quillRef.editor.instance);