# 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
``` ### 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: Polotno Editor 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 ``` ### 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] --- # Add Signature to the Canvas URL: https://polotno.com/docs/add-signature-to-the-canvas In **Polotno** you cannot draw directly on the canvas. But you can create your own custom UI for drawing operations, and then add the result to the canvas using `image` or `svg` elements. In this demo, we create a custom side panel to add a signature to the document. ## Live demo [#live-demo] --- # Advanced Syncing Design Elements URL: https://polotno.com/docs/advanced-syncing-design-elements Sometimes you want multiple elements in your design to stay perfectly in sync. For example, repeating headers, labels, or dynamic fields (like `{pageNumber}`) that should update automatically across multiple pages or locations. This guide explains how to implement real-time syncing between elements based on a shared `custom.syncId` property. When one element is edited or removed, all others with the same syncId will update or be deleted accordingly. ## Key Concepts & Tips [#key-concepts--tips] 1. **Assign a `custom.syncId`** to every element that should be linked. 2. Use **MobX reactions** to detect when editing starts and ends. 3. When editing ends, **propagate updated props** to all other synced elements. 4. Use a **throttled change handler** to avoid unnecessary sync calls. 5. When a synced element is deleted, **automatically remove all its peers**. ## Custom Metadata for Syncing [#custom-metadata-for-syncing] In this guide, we use the `custom` property of Polotno elements to store metadata for syncing and variables replacement. > The `custom` field is not used internally by Polotno. It’s reserved for your own purposes and is safe to use for attaching any extra information (like flags, references, or identifiers). ### 1. Syncing element properties by syncId [#1-syncing-element-properties-by-syncid] This function scans the store for elements with the same syncId as the currently selected one. It then copies over all properties (except the id) from the source to the others. ```ts function syncElements(store: any) { const syncElement = store.selectedElements.find((el: any) => el.custom?.syncId); if (!syncElement) return; store.find((item: any) => { if (item !== syncElement && item.custom?.syncId === syncElement.custom.syncId) { const props = { ...syncElement.toJSON() }; delete props.id; // Never copy over unique IDs const currentProps = item.toJSON(); Object.keys(props).forEach((key) => { const from = currentProps[key]; const to = props[key]; // Only update if values are different if (JSON.stringify(from) !== JSON.stringify(to)) { item.set({ [key]: to }); } }); } }); } ``` > Place this logic inside a reaction or a throttled change listener to keep synced elements updated after user edits. ### Handling Page Number Placeholders [#handling-page-number-placeholders] Polotno does **not** update `{pageNumber}` automatically. To support dynamic text like `Page {pageNumber}`, you need to explicitly store the template string and update it when needed. The logic below shows how to: * Save the original text into `custom.text` when editing ends. * Use `custom.text` as a template to insert the correct page number. #### 1. Track Editing and Store Template [#1-track-editing-and-store-template] This function sets up a MobX reaction to watch when editing starts and ends. When editing ends, it saves the current text into `custom.text`, which acts as a template for future updates. ```ts // Watch for changes in edit mode and trigger sync when edit ends const setupEditReaction = (store: any) => { let lastEditElement: any = null; reaction( () => store.find((item: any) => item._editModeEnabled === true), (editElement: any) => { if (editElement) { // Restore original template if available editElement.set({ text: editElement.custom?.text || editElement.text, }); lastEditElement = editElement; } else { // Save the current text as template if (lastEditElement) { lastEditElement.set({ custom: { ...lastEditElement.custom, text: lastEditElement.text, }, }); } syncElements(store); // Also triggers page number update } } ); }; ``` #### 2. Replace `{pageNumber}` with Real Value [#2-replace-pagenumber-with-real-value] Call this function to replace `{pageNumber}` in the visible text using the current page index. It uses `custom.text` (if available) as the source of truth. ```ts // Replace {pageNumber} in text elements with their actual page index const updatePageNumber = (element: any) => { const store = element.store; const page = element.page; const index = store.pages.indexOf(page); const baseText = element.custom?.text || element.text; element.set({ text: baseText.replaceAll('{pageNumber}', index + 1 as any), }); }; ``` We can call `updatePageNumber` inside the `change` event to keep all in sync. ### Deleting all synced elements when one is removed [#deleting-all-synced-elements-when-one-is-removed] This function ensures cleanup: if a user deletes one synced element, all others with the same `syncId` will also be deleted. This prevents orphaned elements that were meant to be linked. ```ts // Observe element removals and notify callback const onElementRemove = (store: any, callback: (el: any) => void) => { let lastIds: Record = {}; store.on( 'change', throttle(() => { const newIds: Record = {}; store.find((item: any) => { newIds[item.id] = item; }); Object.keys(lastIds).forEach((id) => { if (!newIds[id]) { callback(lastIds[id]); } }); lastIds = newIds; }) ); }; // Remove all elements with the same syncId as the deleted one const setupSyncDeletion = (store: any) => { onElementRemove(store, (element) => { const syncId = element.custom?.syncId; if (!syncId) return; const idsToDelete: string[] = []; store.find((item: any) => { if (item.custom?.syncId === syncId) { idsToDelete.push(item.id); } }); store.deleteElements(idsToDelete); }); }; ``` ## Cloning and Syncing Elements Across All Pages [#cloning-and-syncing-elements-across-all-pages] You can add a custom button (e.g., inside your editor’s toolbar) to let users **duplicate an element across all pages** while keeping those copies in sync. Each cloned element will receive the same `custom.syncId`, so changes to one will be automatically applied to the rest. ```tsx const ApplyToAllPages = ({ store, element, elements }: any) => { return ( ); }; ``` Then add it into the toolbar: ```tsx ``` ## Demo [#demo] 1. Select a text element on the canvas. 2. Click **β€œApply to all pages”** to clone the element to every page. All copies will be linked. 3. Type `{pageNumber}` in the text. Each copy will show the correct page number. 4. Edit the element. All copies will update automatically. 5. Delete the element. All linked copies will also be removed. ## Live demo [#live-demo] --- # AI Design Generation URL: https://polotno.com/docs/ai-design This feature is experimental and is in active development. The API and its behavior may change as we refine the model. For any feedback, please write a direct message on the forum: [https://community.polotno.com/u/a13e0cdd](https://community.polotno.com/u/a13e0cdd) Use the Polotno AI API to generate complete designs from text prompts. This demo shows how to build a custom side panel that integrates with the AI design generation endpoint. ## Features [#features] * **Text prompt input** β€” describe the design you want * **JSON design output** β€” the API returns a `{ design }` object with a complete Polotno JSON design ## How it works [#how-it-works] 1. User enters a design prompt in the custom AI Design side panel 2. The app sends a POST request to the Polotno AI API 3. The API returns a `{ design }` object, and the `design` value is loaded via `store.loadJSON(design)` ## API [#api] The demo uses the AI design creation endpoint: ``` POST https://api.polotno.com/api/ai/design/create?KEY= ``` The API accepts a `prompt` field and returns an object with a `design` field: Request body: ```json { "prompt": "A business card with blue gradient" } ``` Response: ```json { "design": { ...Polotno JSON design... } } ``` Load the returned design with `store.loadJSON(data.design)`. Each AI design generation request will be billed at **$0.20 per design**. During the experimental phase, the API is **free to use**. ## Live demo [#live-demo] --- # Bar Codes URL: https://polotno.com/docs/bar-codes There are several tools that will help you display and change bar codes in Polotno Editor: 1. Using [JsBarcode](https://github.com/lindell/JsBarcode) (or a similar library), generate an SVG image of a bar code. 2. With an `svg` element, display the generated SVG image on the canvas. 3. Use the `custom` attribute of the `svg` shape to save any additional data. You can store the bar code data string for future reference. *Instructions: try changing the content of a selected bar code. Try to deselect the element to create new bar codes.* ## Live demo [#live-demo] --- # Book Cover Editor URL: https://polotno.com/docs/book-cover-editor ### How to make a book cover design editor? [#how-to-make-a-book-cover-design-editor] The demo illustrates how to display a product preview using the export feature of Polotno. ## Live demo [#live-demo] --- # Carousel in HTML Export URL: https://polotno.com/docs/carousel-in-html-export Polotno's HTML export is fully customizable via the `elementHook` callback. This demo shows how to transform image elements into animated carousels during export. ## Key Concepts [#key-concepts] 1. **Custom Properties**: Store carousel images and timing in `element.custom` object 2. **Element Hook**: Transform elements during export using `store.toHTML({ elementHook })` 3. **Virtual DOM**: Return modified DOM structure from the hook to change export output The element hook receives `{ dom, element }` and returns a modified DOM structure. You can inject custom HTML, add JavaScript for interactivity, and alter element rendering completely. ## Example: Transform Image to Carousel [#example-transform-image-to-carousel] ```tsx const html = await store.toHTML({ elementHook: ({ dom, element }) => { // Check if element has carousel images if (element.custom?.images && Array.isArray(element.custom.images)) { // Remove original children dom.children = []; // Create carousel container with slides return { type: 'div', props: { class: 'polotno-carousel-container', 'data-carousel': 'true', 'data-timeout': element.custom.carouselTimeout || 3000, style: { ...dom.props.style, overflow: 'hidden' } }, children: element.custom.images.map((src, i) => ({ type: 'img', props: { src, class: 'carousel-slide', style: { position: 'absolute', width: '100%', height: '100%', display: i === 0 ? 'block' : 'none' } } })) }; } return dom; } }); ``` The exported HTML includes carousel JavaScript that rotates images at the specified interval. A custom side panel lets users configure carousel images and timing before export. ## Live Demo [#live-demo] --- # Charts Rendering URL: https://polotno.com/docs/charts-rendering Showing line, bar, pie charts on the canvas. There are several tools that will help you display and change charts in Polotno Editor: 1. Using an SVG library to generate an SVG image of a chart. 2. With the `svg` element you can show the generated SVG image on the canvas. 3. Use the `custom` attribute of the `svg` shape to save any additional data into the element. You can use it to store chart data for future reference. ## Live demo [#live-demo] --- # Dynamic Template Variables URL: https://polotno.com/docs/dynamic-template-variables Leverage the capabilities of the Polotno SDK to craft templates that incorporate dynamic variables. This feature is useful for generating a variety of similar designs that vary in text or imagery. Dynamic Variables You have full flexibility to build your own template system and even UI for it if needed. The workflow is simple: 1. Create a design with some variables in it. 2. Export design as JSON. 3. Save this JSON to the database/backend. 4. When you need to create a new design or export it, load JSON from the database and replace variables with real data. 5. Load or export the generated JSON. Every design in Polotno can be represented as JSON via `store.toJSON()` function. Now it is up to you to decide how to create variations of this JSON. Here are several examples. ## Simple text replacement [#simple-text-replacement] As a part of your application convention you can use `{variable}` syntax to mark places where you want to replace text. Users of the editor may manually type this text or you can provide some UI for it. ```ts store.activePage.addElement({ type: 'text', // lets use name variable text: 'Hello {name}!', }); ``` When you want to generate a new design or export it, you can replace `{name}` with a real name. Here is a pseudo code for the backend: ```ts const json = loadJSONFromDatabase(); const names = ['John', 'Mike', 'Anna']; // do bulk export for (const name of names) { // we need to replace {name} with real name // ideally you should loop over all text elements and replace text there // but we can do a simple string replace for this example const jsonString = JSON.stringify(json); const newJson = JSON.parse(jsonString.replace('{name}', name)); // export newJson await convertJSONToImage(newJson); } ``` ## Image replacement [#image-replacement] You can set up a similar workflow for images. To mark some images as variable you can use `custom` attribute of `image` element. It is a free-form object that you can use to store any data. You can make your own UI where users can change this data. ```ts store.activePage.addElement({ type: 'image', src: 'https://example.com/placeholder.png', custom: { // lets use avatar variable variable: 'avatar', }, }); ``` Then on the backend you can replace `src` of this image with a real image URL based on information from the `custom` attribute. ```ts // util function to handle deep traversal of json const forEveryChild = (node: any, cb: (n: any) => void) => { if (node.children) { node.children.forEach((child: any) => { cb(child); forEveryChild(child, cb); }); } }; const json = loadJSONFromDatabase(); const avatars = [ 'https://example.com/avatar1.png', 'https://example.com/avatar2.png', ]; // do bulk export for (const avatar of avatars) { // find all avatar images in the json json.pages.forEach((page: any) => { forEveryChild(page, (child) => { if (child.type === 'image' && child.custom?.variable === 'avatar') { child.src = avatar; } }); }); // export json await convertJSONToImage(json); } ``` The demo below shows how to replace text in the design. The demo specifies only one variable `{name}` but you can add more variables according to your application logic. Click the "Preview" button to see how generation works. ## Live demo [#live-demo] --- # Full Canvas Editor URL: https://polotno.com/docs/full-canvas-editor This is the default starting point for integrating Polotno. It mounts the core **Workspace** together with all standard UI blocksβ€”side panel, toolbar (with download), zoom buttons and pages timelineβ€”giving you a ready-to-ship editor in one component tree. Copy the snippet, add your own API key, and you can: * keep the layout as-is for a quick launch, or * swap, hide, or extend any of the UI components while the canvas engine stays untouched. ```tsx import React from 'react'; import ReactDOM from 'react-dom/client'; 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: '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 ( ); }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render(); ``` Use this demo as your boilerplate and build up from here. ## Live demo [#live-demo] --- # Grid UI URL: https://polotno.com/docs/grid-ui **Polotno** doesn't offer built-in support for a grid API or UI directly. However, you can utilize Polotno's existing elements to construct a grid UI. Additionally, invisible elements can be placed on the canvas to facilitate snapping to specific positions. Another approach is to overlay an `svg` element on the canvas to display a grid. This requires dynamically generating the `svg` content based on the desired grid size. ## Live demo [#live-demo] --- # Highlight headings with a banner URL: https://polotno.com/docs/highlight-headings-with-a-full-width-banner-in-polotno A client recently asked: β€œCan we drop a stripe behind a heading that always stretches the full width of the design? Even when the text moves or resizes?” The answer is yes, and it takes only a tiny helper component and one change listener. ## How it works [#how-it-works] * **Button** – When a text element is selected, the **Backgroundβ€―Fill** button toggles a custom flag on that element. ```tsx const TextFullWidthBackground = observer(({ element }) => ( )); ``` * **Listener** – A debounced `store.on('change')` handler checks every flagged text element, creates (or removes) a rectangle beneath it, and keeps the rectangle in sync with any edits. ```ts const checkBackground = () => { // first make sure we remove unrelated backgrounds const idsToDelete: string[] = []; store.find((element) => { const attachedTo = element.custom?.attachedTo; if (!attachedTo) return; const el = store.getElementById(attachedTo); const noBack = !el?.custom?.fullWidthBackground; if (!el || noBack) { idsToDelete.push(element.id); } }); store.deleteElements(idsToDelete); store.find((element) => { if (!element.custom?.fullWidthBackground) return; let backgroundEl = store.find((el) => el.custom?.attachedTo === element.id); if (!backgroundEl) { backgroundEl = element.page.addElement({ type: 'figure', subType: 'rect', fill: 'grey', selectable: false, draggable: false, resizable: false, custom: { attachedTo: element.id }, }); } const elementIndex = element.parent.children.indexOf(element); const backgroundIndex = backgroundEl.parent.children.indexOf(backgroundEl); if (elementIndex !== backgroundIndex + 1) { backgroundEl.page.setElementZIndex(backgroundEl.id, elementIndex); } backgroundEl.set({ x: 0, y: element.y, width: store.width, height: element.height, }); }); }; let timeout: any = null; const requestChange = () => { if (timeout) return; timeout = setTimeout(() => { checkBackground(); timeout = null; }, 10); }; store.on('change', () => { requestChange(); }); ``` ## Demo [#demo] * Click β€œBackground Fill” on a selected text element * See how the background rectangle reacts to text changes ## Live demo [#live-demo] --- # Lightweight SVG Preview URL: https://polotno.com/docs/lite-svg-render ## Use case [#use-case] You need a small preview (e.g., in a templates list) for a json, but you don't want to mount an interactive Polotno editor in the DOM. ## Why store.toDataURL() fails head-less [#why-storetodataurl-fails-head-less] `store.toDataURL()` is using mounted `` component for rendering. While it is perfect for high-quality export or preview generation of the current store, it is hard to use such an approach if you want to generate a preview of a design, that is not loaded inside the editor. ## Lightweight alternative: JSON ➜ SVG ➜ Data URL [#lightweight-alternative-json--svg--data-url] Polotno ships a utility that walks the JSON tree and serializes it directly to SVG. While SVG render is in beta stage and may produce inconsistent result comparing to canvas rendering, it may still give ideal result for preview. ```tsx 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,… } // Example React component function TemplateThumb({ json }) { const [url, setUrl] = React.useState(null); React.useEffect(() => { getThumbnailURL(json).then(setUrl); }, [json]); return url ? preview : '…'; } ``` ## Demo [#demo] Take a look into Variations side panel. You can click "Generate variations" to create many svg images using changed svg export of a design. --- # One-click page rotation URL: https://polotno.com/docs/one-click-page-rotation Need to flip a design from portrait to landscape (or back) without redrawing everything? The snippet below adds a single **Rotate** button that swaps the canvas width/height and spins every element by 90Β° so the layout stays intact. ```tsx const PageRotate = ({ store }) => ( ); ``` ## How it works [#how-it-works] * **Swap the canvas size.** `store.setSize(newW, newH)` turns portrait into landscape. * **Translate each object to the old center, rotate it, then translate to the new center.** That keeps relative positions unchanged. * **Add 90Β° to each element’s rotation** so arrows, text, etc. remain upright. ### When is this handy? [#when-is-this-handy] * Quickly test both orientations of a social graphic. * Offer users an instant β€œmake it landscape” button in a print template. Drop `PageRotate` into your toolbar: ```tsx ``` ## Live demo [#live-demo] --- # Page Name URL: https://polotno.com/docs/page-name Showing page name in page controls. You can customize PageControls components to show the page name on top of the canvas. You can also add the ability to edit that name and save it to the store. ## Live demo [#live-demo] --- # Page Numbers on Canvas URL: https://polotno.com/docs/page-numbers-on-canvas What if you want to automatically add page numbers to your document? Usually most elements in the store are controlled by the user. But with Polotno you can create your own elements and fully control them programmatically. In this demo we add a special hook to show page numbers directly on the canvas. Instructions: Check the number on the first page. Try to add/reorder/remove pages, and you will see page numbers updated automatically. ## Live demo [#live-demo] --- # Print for T‑shirts URL: https://polotno.com/docs/print-for-t-shirts A lot of merch platforms need a simple, white‑label canvas where customers can drop graphics on a shirt, then download a print‑ready file. Polotno lets you ship that in a day. Below is the approach we used in the live demo. For t‑shirt background, we use image elements like this: ```ts frontPage.addElement({ type: 'svg', src: '/front.svg', width: 2500, height: 2500, x: -750, y: -400, showInExport: false, selectable: false, }); ``` That way, the background image is not selectable and will be excluded from the final export. ## Live demo [#live-demo] --- # Programmatic Transform URL: https://polotno.com/docs/programmatic-transform When building custom UI for Polotno, you may need to transform elements programmatically β€” changing position, size, or rotation through input fields or buttons rather than the canvas transformer. While Polotno provides an on-canvas transformer that handles grouped and multi-selected elements automatically, there's no direct API to perform the same bulk operations programmatically. This guide shows how to implement your own transform logic. ## Understanding Polotno Limitations [#understanding-polotno-limitations] Polotno groups don't have their own `x`, `y`, `width`, `height`, or `rotation` properties. A group is just a container β€” only its children have transform properties. When you transform a group on canvas, Polotno internally transforms each child element. To replicate this behavior via API, you need to: 1. Extract all leaf elements (non-group children) from your selection 2. Calculate the combined bounding box 3. Apply transforms to each element individually ## Required Utilities [#required-utilities] Polotno provides math utilities that make this easier: ```ts import { getTotalClientRect, getCenter, rotateAroundPoint, } from 'polotno/utils/math'; import { forEveryChild } from 'polotno/model/group-model'; ``` * `getTotalClientRect(shapes)` β€” returns the axis-aligned bounding box of elements, accounting for rotation * `getCenter(rect)` β€” returns the center point of a rectangle * `rotateAroundPoint(shape, angleDelta, center)` β€” rotates a shape around a point * `forEveryChild(group, callback)` β€” iterates through all children of a group (including nested) > **Why use `getTotalClientRect` for the center?** A simple calculation like `x + width/2` only works for non-rotated elements. When an element is already rotated, its visual center shifts. `getTotalClientRect` returns the correct bounding box accounting for rotation, so `getCenter` gives you the true visual center. ## Extracting Shapes from Selection [#extracting-shapes-from-selection] First, get all leaf elements from your selection, handling groups properly: ```ts function getShapes(elements) { const shapes = []; elements.forEach((el) => { if (el.type === 'group') { forEveryChild(el, (child) => { if (child.type !== 'group') { shapes.push(child); } }); } else { shapes.push(el); } }); return shapes; } // Usage const shapes = getShapes(store.selectedElements); ``` ## Transforming a Single Element [#transforming-a-single-element] For a single non-group element, you can set properties directly: ```ts const element = store.selectedElements[0]; // Position element.set({ x: 100, y: 200 }); // Size (with locked aspect ratio) const ratio = element.height / element.width; element.set({ width: 300, height: 300 * ratio }); // Rotation around center (use bounding box for correct center even when rotated) const bbox = getTotalClientRect([element]); const center = getCenter(bbox); const targetRotation = 45; const delta = targetRotation - (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); ``` ## Transforming Multiple Elements [#transforming-multiple-elements] For multiple selected elements, calculate the bounding box and transform each element relative to it: ```ts const shapes = getShapes(store.selectedElements); const bbox = getTotalClientRect(shapes); // Move all elements by delta function moveElements(deltaX, deltaY) { shapes.forEach((shape) => { shape.set({ x: shape.x + deltaX, y: shape.y + deltaY }); }); } // Scale all elements proportionally from top-left function scaleElements(newWidth) { const scale = newWidth / bbox.width; const originX = bbox.x; const originY = bbox.y; shapes.forEach((shape) => { shape.set({ x: originX + (shape.x - originX) * scale, y: originY + (shape.y - originY) * scale, width: shape.width * scale, height: shape.height * scale, }); }); } // Rotate all elements around bounding box center function rotateElements(angleDelta) { const center = getCenter(bbox); shapes.forEach((shape) => { const newShape = rotateAroundPoint( { x: shape.x, y: shape.y, width: shape.width, height: shape.height, rotation: shape.rotation || 0, }, angleDelta, center ); shape.set(newShape); }); } ``` ## Transforming Groups [#transforming-groups] Groups work the same as multiple selections β€” extract the children and transform them: ```ts const group = store.selectedElements[0]; // assuming it's a group const shapes = []; forEveryChild(group, (child) => { if (child.type !== 'group') { shapes.push(child); } }); const bbox = getTotalClientRect(shapes); const center = getCenter(bbox); // Rotate group children by 15 degrees shapes.forEach((shape) => { const newShape = rotateAroundPoint( { x: shape.x, y: shape.y, width: shape.width, height: shape.height, rotation: shape.rotation || 0, }, 15, // angle delta center ); shape.set(newShape); }); ``` ## Handling Rotation State [#handling-rotation-state] When building UI controls for rotation, you need to track the "current rotation" of a group or multi-selection. Since there's no single rotation value, use a local state that: 1. Initializes to the common rotation of all shapes (or 0 if mixed) 2. Resets when selection changes 3. Syncs when rotation changes externally (e.g., via canvas handles) ```ts const [localRotation, setLocalRotation] = React.useState(0); const prevSelectionRef = React.useRef(null); // Reset on selection change React.useEffect(() => { const selectionKey = elements.map((e) => e.id).join(','); if (selectionKey !== prevSelectionRef.current) { prevSelectionRef.current = selectionKey; // Initialize to current rotation const rotations = shapes.map((el) => el.rotation || 0); const allSame = rotations.every((r) => r === rotations[0]); setLocalRotation(allSame ? rotations[0] : 0); } }, [elements]); ``` ## Live demo [#live-demo] --- # QR Codes URL: https://polotno.com/docs/qr-codes QR code shapes are not a built-in feature of Polotno. But you can easily render them via `svg` elements. You can also use the `custom` property of an element to save additional information, such as QR code content for future reference. You can use [`qrcode`](https://www.npmjs.com/package/qrcode) (or another similar library) to generate the content of a QR code. The tutorial below helps you create an initial version that you can later modify to fit your needs. ## Step 1. Create a side section to generate and edit QR codes [#step-1-create-a-side-section-to-generate-and-edit-qr-codes] In a separate file add this code: ```tsx import React from 'react'; import { observer } from 'mobx-react-lite'; import { SectionTab } from 'polotno/side-panel'; import QRCode from 'qrcode'; import * as svg from 'polotno/utils/svg'; import ImQrcode from '@meronex/icons/im/ImQrcode'; import { Button, Input } from 'polotno/primitives'; // create svg image for QR code for input text export async function getQR(text: string) { return new Promise((resolve) => { QRCode.toString( text || 'no-data', { type: 'svg', color: { dark: '#00F', // Blue dots light: '#0000', // Transparent background }, }, (err, string) => { resolve(svg.svgToURL(string)); } ); }); } // define the new custom section export const QrSection = { name: 'qr', Tab: (props) => ( ), // we need observer to update component automatically on any store changes Panel: observer(({ store }) => { const [val, setVal] = React.useState(''); const el = store.selectedElements[0]; const isQR = el?.name === 'qr'; // if selection is changed we need to update input value React.useEffect(() => { if (el?.custom?.value) { setVal(el?.custom.value); } }, [isQR, el]); // update image src when we change input data React.useEffect(() => { if (isQR) { getQR(val).then((src) => { el.set({ src, custom: { value: val, }, }); }); } }, [el, val, isQR]); return (
{isQR &&

Update select QR code:

} {!isQR &&

Create new QR code:

} { setVal(e.target.value); }} placeholder="Type qr code content" value={val} style={{ width: '100%' }} />
); }), }; ``` ## Step 2. Add the created section into the Polotno side panel [#step-2-add-the-created-section-into-the-polotno-side-panel] ```tsx import { SidePanel, DEFAULT_SECTIONS } from 'polotno/side-panel'; import { QrSection } from './qr-section'; // this file is created on step 1 // add a new custom section const sections = [QrSection, ...DEFAULT_SECTIONS]; // then in render of side panel: ``` ## Live demo [#live-demo] --- # Real-time Collaboration with PartyKit URL: https://polotno.com/docs/real-time-collaboration-with-partykit Polotno’s SDK is single‑user by design, but because the document state lives in a MobX‑State‑Tree store you can stream patches and replay them elsewhere. That’s exactly what [PartyKit](https://www.partykit.io/) does in the demo below: every user who joins the same room sees edits appear in under a second. Sample code: [https://github.com/polotno-project/polotno-partykit](https://github.com/polotno-project/polotno-partykit) Demo page: [https://polotno-partykit.lavrton.partykit.dev/](https://polotno-partykit.lavrton.partykit.dev/) ## How it works [#how-it-works] 1. **Each client** joins a PartyKit room (example-room). 2. On first load it asks the room for the current state: `{ type: 'request-state' }`. 3. The first peer to answer sends back a full snapshot: `{ type: 'reset-state', state: … }`. 4. After that we only ship incremental MobX patches: `{ type: 'patch', patch: … }`. 5. Every receiver applies the patch with `applyPatch(store.pages, patch)` and the canvas updates instantly. The tiny guard `ignorePathRef` prevents echo loops while a patch is replaying. ```ts const socket = usePartySocket({ room: 'example-room', onMessage(evt) { const event = JSON.parse(evt.data); if (event.type === 'patch') { console.log('patch received'); ignorePathRef.current = true; applyPatch(store.pages, event.patch); ignorePathRef.current = false; } if (event.type === 'reset-state') { console.log('reset-state received'); ignorePathRef.current = true; store.loadJSON(event.state); ignorePathRef.current = false; } if (event.type === 'request-state') { console.log('request-state received'); socket.send( JSON.stringify({ type: 'reset-state', state: store.toJSON(), }) ); } }, }); React.useEffect(() => { // ask for the current state socket.send(JSON.stringify({ type: 'request-state' })); // stream local edits onPatch(store.pages, (patch) => { if (ignorePathRef.current) return; socket.send(JSON.stringify({ type: 'patch', patch })); }); }, []); ``` *** ### A few notes [#a-few-notes] * **Not part of core SDK** – Polotno doesn’t ship server code or conflict resolution; we simply forward MobX patches. * **Latency‑tolerant** – patches are small JSON objects, so even high‑latency networks feel snappy. * **Conflict strategy** – the demo is last‑write‑wins. For richer docs you might add per‑element locks or OT/CRDT. --- # Replace placeholder with user upload URL: https://polotno.com/docs/replacing-a-placeholder-image-with-a-user-uploaded-image Use this approach when you want to give users the ability to quickly replace a default placeholder image in their design with a custom photo. This is useful for flyers, posters, and other templates where large images can be swapped out. ### 1. Mark an Image as a Placeholder [#1-mark-an-image-as-a-placeholder] ```ts const width = 600; const height = 400; // Add a placeholder image element // you can make you own with your own text // placehold.co is just an example, better to use your own image store.activePage.addElement( { type: 'image', src: 'https://placehold.co/600x400?text=Click+to+add+image', x: (store.width - width) / 2, y: (store.height - height) / 2, width, height, // custom property to identify placeholder custom: { isPlaceholder: true, }, }, { skipSelect: true } ); ``` ### 2. Detect When the Placeholder Is Selected [#2-detect-when-the-placeholder-is-selected] Watch for changes in `store.selectedElements`. If a placeholder image is selected, show an β€œUpload Your Picture” button or open a file picker immediately. ### 3. Replace the Placeholder Image [#3-replace-the-placeholder-image] Once a user selects an image file, set the element’s `src` to the URL of that file and clear out `custom.isPlaceholder`. ### Implementation Example [#implementation-example] Below is a minimal React-based implementation. The key points are: * A hidden file input used to pick images * A MobX reaction or state check to detect when the user selects the placeholder * Replacing the placeholder image with the uploaded file ```tsx import React from 'react'; import { reaction } from 'mobx'; import { getImageSize, getCrop } from 'polotno/utils/image'; export function usePlaceholderSelection(store: any) { const fileInputRef = React.useRef(null); // react to selected elements React.useEffect(() => { // Dispose reaction when component unmounts const dispose = reaction( () => store.selectedElements, (selectedElements: any[]) => { // Check if at least one selected element is a placeholder const placeholder = selectedElements.find( (el) => el.custom?.isPlaceholder ); if (placeholder) { // Trigger the hidden file input fileInputRef.current?.click(); } } ); return () => dispose(); }, [store]); const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (loadEvent) => { // we will use dataURL to set the image // but it is recommended to upload image to the server first // for better performance and smaller JSON export const dataURL = loadEvent.target?.result as string; // Find the currently selected placeholder element const placeholder = store.selectedElements.find( (el: any) => el.custom?.isPlaceholder ); if (!placeholder) return; // Get new image dimensions and calculate crop to fill placeholder (cover) const { width, height } = await getImageSize(dataURL); const crop = getCrop(placeholder, { width, height }); placeholder.set({ src: dataURL, ...crop, custom: { isPlaceholder: false }, }); }; reader.readAsDataURL(file); // Reset the file input so selecting the same file triggers onChange again e.target.value = ''; }; // Return a hidden file input to be triggered by the reaction return ( ); } ``` ### 3. Use the Hook in Your App [#3-use-the-hook-in-your-app] ```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'; export const App = ({ store }: { store: any }) => { const fileInput = usePlaceholderSelection(store); return ( {fileInput} ); }; ``` ### Optional: Control How New Images Fit [#optional-control-how-new-images-fit] If aspect ratios differ and you want predictable results, use `getImageSize` and `getCrop` from `polotno/utils/image`. `getCrop` gives a CSS-like **cover** behavior (fills the placeholder and may crop edges). For a **contain** effect, resize the element itself using `scale = Math.min(boxW / imgW, boxH / imgH)` and center the result (keep `cropX = cropY = 0`). ```ts import { getImageSize, getCrop } from 'polotno/utils/image'; // After you get the uploaded file as dataURL const { width, height } = await getImageSize(dataURL); const crop = getCrop(placeholder, { width, height }); placeholder.set({ src: dataURL, ...crop, custom: { isPlaceholder: false }, }); ``` ### Alternative UI Approaches [#alternative-ui-approaches] * **Manual Button**: Display a button in the [Toolbar](/docs/toolbar) or [Tooltip](/docs/tooltip) that opens the file dialog. * **Side Panel Section**: Create a custom side panel tab that appears only when a placeholder is selected. ### Tips [#tips] * Use `custom.isPlaceholder` or any custom property to mark replaceable images. * `getCrop` provides "cover" behavior (fills the placeholder, may crop edges). * Crop values (`cropX`, `cropY`, `cropWidth`, `cropHeight`) are normalized 0–1 values. * For best results, upload images to a server first instead of using data URLs. ## Live demo [#live-demo] --- # Single-page view with arrow navigation URL: https://polotno.com/docs/single-page-view-with-arrow-navigation By default, Polotno shows a tall stack of pages that you scroll through vertically. For some products (pitch‑deck builder, kiosk app), you may want a cleaner workflow: keep one page in view and let the user flip with arrows. Use the `renderOnlyActivePage` property on ``, and extend default page controls to show arrows for navigation: ```tsx import { observer } from 'mobx-react-lite'; import { PageControls as DefaultPageControls } from 'polotno/canvas/page-controls'; import { Workspace } from 'polotno/canvas/workspace'; const PageControls = observer((props) => { const activeIndex = store.pages.indexOf(store.activePage); const canGoBack = activeIndex > 0; const canGoForward = activeIndex < store.pages.length - 1; const height = store.scale * props.page.computedHeight; return ( <> ); }); // in render ``` ## Demo [#demo] * Use arrow navigation near page content to switch between pages ## Live demo [#live-demo] --- # Templates Library URL: https://polotno.com/docs/templates-library You can use Polotno tools and [side panel customization](/docs/side-panel-overview) to build your own template library with design presets. In order to make a templates library you will need: 1. Create a design in the Polotno Editor 2. Use `store.toJSON()` to [export the design](/docs/import-and-export) as a `.json` file 3. (Optional) Use `store.saveAsImage()` to save a preview image of a design 4. Repeat steps 1–3 as many times as you need in order to make a library of templates 5. If you skipped step 3 you can use [Cloud Render API](/docs/cloud-render-api) to generate previews automatically 6. Make a server‑side API to access a list of templates 7. Create a [custom side panel](/docs/side-panel-overview) to display a list of templates from that API 8. Use `store.loadJSON()` to import design into canvas ## (1–4) Making designs library [#14-making-designs-library] You will need to use Polotno editor to create designs. Importing designs from other software is not directly supported. Note: if you need to import from SVG files, please [contact us](https://polotno.com/contact). ## (5) Generating previews [#5-generating-previews] You can use the [Polotno Cloud API](/docs/cloud-render-api) to generate previews with your own backend language. ```ts import fs from 'fs'; import path from 'path'; import { createInstance } from 'polotno-node'; // we will look for local directory for templates const FOLDER_NAME = 'templates'; async function run() { // create working instance const instance = await createInstance({ // this is a demo key just for that project // (!) please don't use it in your projects // to create your own API key please go here: https://polotno.com/cabinet key: 'nFA5H9elEytDyPyvKL7T', }); // read all files in the directory const files = fs.readdirSync(FOLDER_NAME); for (const file of files) { // if it is not a json file - skip it if (!file.endsWith('.json')) continue; // load file const data = fs.readFileSync(path.join(FOLDER_NAME, file)).toString(); const json = JSON.parse(data); // convert JSON into image preview const imageBase64 = await instance.run(async (json) => { store.loadJSON(json); await store.waitLoading(); // set width of previews (we usually don't need original size) const maxWidth = 200; const scale = maxWidth / store.width; const url = await store.toDataURL({ pixelRatio: scale }); return url.split('base64,')[1]; }, json); // save images locally into the same folder fs.writeFileSync( path.join(FOLDER_NAME, file.split('.')[0] + '.png'), imageBase64, 'base64' ); console.log(`Finished ${FOLDER_NAME} ${file}`); } // close instance instance.close(); } run(); ``` ## (6) Making backend API to load templates [#6-making-backend-api-to-load-templates] This step is out of **Polotno** scope. You can use any backend stack you like. Most likely, you will need an API to get a list of available templates. For every design, you will have to share a public path to **json** file and **png** preview. ## (7 and 8) Displaying templates on side panel [#7-and-8-displaying-templates-on-side-panel] As soon as you have a backend API to get a list of templates, you can use [Side Panel Customization](/docs/side-panel-overview) to display that list on the page. In the demo we will use `` and the `useInfiniteAPI` hook ([docs](/docs/utils-api)). You don't have to use them in your app, but they can save time for this use case. ```tsx import React from 'react'; import { observer } from 'mobx-react-lite'; import { useInfiniteAPI } from 'polotno/utils/use-api'; import { SectionTab } from 'polotno/side-panel'; import MdPhotoLibrary from '@meronex/icons/md/MdPhotoLibrary'; import { ImagesGrid } from 'polotno/side-panel/images-grid'; export const TemplatesPanel = observer(({ store }) => { // load data const { data, isLoading } = useInfiniteAPI({ getAPI: ({ page }) => `templates/page${page}.json`, }); return (
d.items).flat()} getPreview={(item) => `/templates/${item.preview}`} isLoading={isLoading} onSelect={async (item) => { // download selected json const req = await fetch(`/templates/${item.json}`); const json = await req.json(); // inject it into store store.loadJSON(json); }} rowsNumber={1} />
); }); // define the new custom section export const TemplatesSection = { name: 'custom-templates', Tab: (props) => ( ), // we need observer to update component automatically on any store changes Panel: TemplatesPanel, }; ``` ## Live demo [#live-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 Roboto font preview ``` ## 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 rich text support!', 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); ); }); // also we can disable default Undo/Redo buttons via History component const History = () => null; const App = ({ store }) => { return (
); }; ``` ## How to remove a specific button? [#how-to-remove-a-specific-button] Create a React component that renders nothing (returns null) and overwrite the component in the toolbar. ```tsx const None = () => null; ``` ## How to overwrite all inputs at once? [#how-to-overwrite-all-inputs-at-once] Following [Custom Element Example](/docs/custom-elements), you can make your own React component for any available element. Recommendation: build your own UI with components from [`polotno/primitives`](/docs/customizations) so it matches the editor, and use `` to group inputs. Also remember to wrap your component in `observer` from `mobx-react-lite` library to add automatic reactivity of your components. ```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 TextToolbar = observer(({ store }) => { const element = store.selectedElements[0]; return ( element.set({ fill, }) } store={store} /> { element.set({ fontSize: fontSize }); }} value={element.fontSize} min={1} max={40} /> ); }); unstable_registerToolbarComponent('text', TextToolbar); ``` ## How to overwrite Download button [#how-to-overwrite-download-button] On the right side of the toolbar, Polotno has 'Action Controls' section. You can use `components` prop to overwrite this section. Recommendation: keep 'Action Controls' as small as possible. `` component already has a lot of tools. So it is better give it as much available width as possible. You can put 'Action Controls" somewhere else in the UI of your application. For example, take a look into [https://studio.polotno.com/](https://studio.polotno.com/). Download button is placed on the top of the app instead of the ``. ```tsx import { Toolbar } from 'polotno/toolbar/toolbar'; import { Button } from 'polotno/primitives'; import { DownloadButton } from 'polotno/toolbar/download-button'; // it is important to define component onside of `MyToolbar` render function const ActionControls = ({ store }) => { return (
); }; const MyToolbar = ({ store }) => { return ( ); }; ``` ## Live demo [#live-demo] --- # Tooltip URL: https://polotno.com/docs/tooltip **Tooltip**, a UI component similar to the [Toolbar](./toolbar), is designed to change elements on the canvas, reorder and align them. It is rendered next to a selected element β€” a behavior you can modify. ## How to customize Tooltip? [#how-to-customize-tooltip] Similar to the [Toolbar](./toolbar), Tooltip supports a special `components` property to add/change/remove most of its UI parts. Pass a component using the name format `TypeName`, where `Type` refers to the element `type`. For example: `TextFill`. You can use built-in element types like `Text`, `Image`, `svg`. You can also use the `Many` prefix when several elements are selected. Additionally, you can define your own components for any element type, e.g. `ImageAlertButton`. ```tsx import { observer } from 'mobx-react-lite'; import { Workspace } from 'polotno/canvas/workspace'; import { Tooltip } from 'polotno/canvas/tooltip'; const MyColorPicker = observer(({ store, element, elements }) => { // store - main polotno store object // elements - array of selected elements. Same as store.selectedElements // element - first selected element. Same as store.selectedElements[0] return (
{ element.set({ fill: e.target.value, }); }} />
); }); const App = ({ store }) => { return (
); }; ``` Tip for images: prefix the component name with `Image`, for example `ImageActionButton: MyComponent`. ## How to disable Tooltip? [#how-to-disable-tooltip] Pass a `Tooltip` component that renders `null`. ```tsx import { Toolbar } from 'polotno/toolbar/toolbar'; import { Workspace } from 'polotno/canvas/workspace'; const Tooltip = () => null; const App = ({ store }) => { return (
); }; ``` ## Live demo [#live-demo] --- # Workspace URL: https://polotno.com/docs/workspace **Workspace** is an essential part of **Polotno**. It is the main **React** component used to display drawings and interact with them. You can change canvas and its content programmatically using the [Store API](/docs/store). ```tsx import { Workspace } from 'polotno/canvas/workspace'; 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 App = () => { return (
); }; ``` The `Workspace` will automatically take the full width and height of its parent, so you don't have to adjust its size manually. Set the size of the parent `div` with CSS. ## Layout orientation [#layout-orientation] Control whether pages are stacked vertically (default) or horizontally in the workspace: ```tsx // Default vertical layout (pages stack top to bottom) // Horizontal layout (pages arranged left to right) ``` **Use cases:** * `vertical` (default): Standard multi-page documents, presentations, social media carousels * `horizontal`: Timeline-based designs, panoramic layouts, storyboards **Demo:** Switch between vertical and horizontal orientation in this live editor. The store renders three pages so you can evaluate the timeline spacing. ## Customize page controls [#customize-page-controls] You can customize page controls of your `` component. Remove buttons, add your own, etc. You are in complete control over what is shown around the page. ```tsx (
My controls here...
), }} /> ``` ## Hide page controls [#hide-page-controls] Optionally, you can hide UI to add/remove/duplicate pages. ```tsx null }} /> ``` ## Workspace styling [#workspace-styling] Optionally, change some styles of the workspace. ```tsx ``` > **Deprecated.** `setTransformerStyle`, `setHighlighterStyle`, > `setInnerImageCropTransformerStyle`, and > `setOuterImageCropTransformerStyle` from `polotno/config` do the same > thing globally and still work for backwards compatibility, but prefer > the per-`` props above. The setters will be removed in the > next major. ## Read-only view [#read-only-view] To render the workspace as a non-editable preview, switch the store to the `viewer` role. Selection, drag, resize, and inline editing are disabled while the canvas continues to render normally. ```tsx store.setRole('viewer'); ``` See [User Roles](/docs/user-roles) for the full behavior of each role. ## Group selection [#group-selection] `groupSelectionMode` controls how clicks on grouped elements behave inside the canvas. ```tsx // default β€” clicking a child inside a group selects that child // legacy β€” clicking anywhere inside a group selects the whole group ``` * `"drill"` (default): the first click on a group selects the group as a whole. A second click on the already-selected group drills in and selects the clicked child. * `"group"`: the group is always selected as a single unit, no matter how many times you click. Use this for the previous (pre-drill) behavior where children can only be reached via the layers panel or `store.selectElements`. The mode does not affect programmatic selection β€” `store.selectElements([childId])` works the same way regardless of which mode is active. ## Select through stacked elements [#select-through-stacked-elements] Hold `Cmd` (macOS) or `Ctrl` (Windows/Linux) while clicking to cycle the selection through elements stacked at the click point. This is useful for picking an element that sits behind another. ## Show only the current page in Workspace [#show-only-the-current-page-in-workspace] Use the `renderOnlyActivePage` property to show only the active page. ```tsx ``` ## No pages UI [#no-pages-ui] If your `store` has no pages, `` will show a simple UI with a button to create a new page. You can overwrite this UI with your own. ```tsx
The project has no pages...
, }} /> ``` ## Overwrite keyboard handler [#overwrite-keyboard-handler] If you don't like default keyboard shortcuts, you can overwrite them with your own. ```tsx import { handleHotkey } from 'polotno/canvas/hotkeys'; { if (e.key === 'Escape') { store.selectElements([]); return; } // optionally call the default handler handleHotkey(e, store); }} />; ``` Do not call `e.preventDefault()` for `Ctrl/Cmd + C/X/V` in a custom `onKeyDown` handler. Copy, cut, and paste are handled through the browser's native clipboard events (which also power pasting external images and text onto the canvas), and preventing the default keydown action suppresses those events entirely β€” copy/paste will silently stop working. --- # Asset Loading Configuration URL: https://polotno.com/docs/asset-loading Polotno provides methods to control how assets (images, fonts) are loaded, including timeout settings and error handlers. ## Asset Load Timeout [#asset-load-timeout] Configure how long Polotno waits before timing out asset loads (images, SVGs). Default: 30 seconds. ```ts import { setAssetLoadTimeout } from 'polotno/config'; // set timeout to 10 seconds setAssetLoadTimeout(10000); ``` ## Font Load Timeout [#font-load-timeout] Configure timeout specifically for font loading. Default: 6 seconds. ```ts import { setFontLoadTimeout } from 'polotno/config'; // set font timeout to 15 seconds setFontLoadTimeout(15000); ``` ## Error Handling [#error-handling] Register a global error handler for asset loading failures. The handler receives an `Error` with a `code` property and structured `details`, so you can react differently per failure type: ```ts import { onLoadError } from 'polotno/config'; onLoadError((error) => { if (error.code === 'IMAGE_FAILED') { console.error( `Image failed to load (element ${error.details?.elementId}):`, error.message ); } else if (error.code === 'FONT_FAILED') { console.error(`Font "${error.details?.family}" failed to load`); } else { console.error('Failed to load asset:', error.message); } }); ``` See [Error Handling](/docs/error-handling) for the full list of codes and the `details` they carry. The same codes are reported when rendering through [polotno-node](/docs/server-side-image-generation-with-node-js). Before polotno 4.3.0 the handler received a plain string. It now receives the `Error` above β€” `error.message` contains the old string. ## Custom Image Loader (Advanced) [#custom-image-loader-advanced] **Advanced Feature**: This is an experimental API for complex scenarios. Not supported in [Cloud Render API](/docs/cloud-render-api) or [polotno-node](/docs/server-side-image-generation-with-node-js). Override the default image loading mechanism with a custom React hook. The hook must follow the [react-konva useImage hook](https://konvajs.org/docs/react/Images.html) pattern, returning `[image, status]`. ```tsx import React from 'react'; import { unstable_setImageLoaderHook } from 'polotno/config'; function useCustomImageLoader(url, crossOrigin) { const [image, setImage] = React.useState(null); const [status, setStatus] = React.useState('loading'); React.useEffect(() => { if (!url) return; setStatus('loading'); const img = document.createElement('img'); const onLoad = () => { setStatus('loaded'); setImage(img); }; const onError = () => { setStatus('failed'); setImage(null); }; img.addEventListener('load', onLoad); img.addEventListener('error', onError); if (crossOrigin) { img.crossOrigin = crossOrigin; } img.src = url; return () => { img.removeEventListener('load', onLoad); img.removeEventListener('error', onError); }; }, [url, crossOrigin]); return [image, status]; } // register the custom hook unstable_setImageLoaderHook(useCustomImageLoader); ``` ### Use Cases [#use-cases] * **Authentication**: Modify URLs or add auth tokens before loading * **S3 Link Revalidation**: Refresh expired pre-signed URLs * **Custom CDN Logic**: Route requests through specific endpoints * **Retry Logic**: Implement automatic retries on failure --- # Error Handling URL: https://polotno.com/docs/error-handling Every Polotno package reports failures the same way: a plain `Error` with a `code` property and a structured, JSON-serializable `details` object. Consuming code branches on `error.code` β€” the same pattern Node.js itself uses (`err.code === 'ENOENT'`): ```ts import { jsonToPDFBytes, type PolotnoError } from '@polotno/pdf-export'; try { await jsonToPDFBytes(design); } catch (e) { const error = e as PolotnoError; if (error.code === 'IMAGE_FAILED') { // which element failed: console.log(error.details.elementId); if (error.details.reason === 'unsupported-format') { // e.g. 'application/pdf' β€” an embedded PDF passed as an image console.log(error.details.mimeType); } } else { throw e; } } ``` There are no error classes and no `instanceof` checks. A property check works across bundle copies, worker threads, and serialization boundaries β€” codes even survive the headless-browser boundary inside [`polotno-node`](/docs/server-side-image-generation-with-node-js), where thrown errors are rehydrated with `code` and `details` intact. ## Codes vs. messages [#codes-vs-messages] Branch on `error.code` and `details.reason`, not on `error.message`. Codes are meant to stay stable across releases; messages are human-readable text and may be reworded at any time β€” log them, display them, but don't match on them. ## Error codes [#error-codes] `details.reason` carries the fine grain within a code; the other `details` fields tell you what exactly failed. | Code | When | Key `details` | | ---------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DESIGN_INVALID` | Design JSON failed validation (schema check, `store.loadJSON`, unknown element type) | `errors` β€” array of `{ path, message }` | | `EXPORT_FAILED` | An export could not run or an element failed to render | `reason`: `page-not-found`, `workspace-not-mounted`, `unmounted-during-export`, `invalid-options`, `element`, `busy`, `pdfx` β€” plus `pageId`, `option`, `label`, `elementId` | | `IMAGE_FAILED` | An image could not be loaded, decoded, or embedded | `reason`: `unsupported-format`, `load`, `decode-failed`, `fetch-failed`, `invalid-data-url`, `timeout` β€” plus `mimeType`, `supported`, `src`, `elementId` | | `FONT_FAILED` | A font could not be resolved or loaded | `reason`: `not-found`, `timeout`, `fetch-failed`, `decode-failed` β€” plus `family`, `src` | | `VIDEO_FAILED` | A video could not be loaded, decoded, or encoded | `reason`: `load`, `no-video-track`, `unsupported-codec`, `cannot-decode`, `no-encoder`, `zero-duration`, `invalid-dimensions`, `timeout` β€” plus `src` | | `FETCH_FAILED` | A network request for an asset failed (after retries, where applicable) | `url`, `status`, `attempts` | | `IMPORT_FAILED` | An imported file (PSD / SVG / PDF) could not be parsed | `format`: `psd`, `svg`, `pdf` | | `UNKNOWN` | An unclassified failure reported through `onLoadError` | β€” | The original low-level error, when there is one, is chained as `error.cause`. ## Where errors come from [#where-errors-come-from] Each package throws only the codes relevant to it: | Package | Codes you can catch | | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `polotno` (editor) | `DESIGN_INVALID`, `EXPORT_FAILED` from store methods; `IMAGE_FAILED`, `VIDEO_FAILED`, `FONT_FAILED`, `UNKNOWN` via [`onLoadError`](#onloaderror-in-the-editor) | | `polotno-node` | Everything the editor reports, rehydrated across the browser boundary, plus `FETCH_FAILED` and `EXPORT_FAILED` (`reason: 'busy'`) | | `@polotno/pdf-export` | `DESIGN_INVALID`, `IMAGE_FAILED`, `EXPORT_FAILED`, `FONT_FAILED`, `FETCH_FAILED` | | `@polotno/pptx-export` | `DESIGN_INVALID`, `IMAGE_FAILED`, `EXPORT_FAILED`, `FETCH_FAILED` | | `@polotno/video-export` | `VIDEO_FAILED`, `FETCH_FAILED`, `EXPORT_FAILED` | | `@polotno/psd-import`, `@polotno/svg-import`, `@polotno/pdf-import` | `IMPORT_FAILED` | | `@polotno/schema` | `DESIGN_INVALID` | ## `onLoadError` in the editor [#onloaderror-in-the-editor] Exports and imports **throw**. The editor canvas is different: a broken image or font inside the editor should not crash rendering, so those failures are **reported** instead β€” to the global `onLoadError` handler. The error object has exactly the same shape as thrown errors: ```ts import { onLoadError } from 'polotno/config'; onLoadError((error) => { if (error.code === 'IMAGE_FAILED') { notifyUser(`An image could not be loaded (element ${error.details?.elementId})`); } else if (error.code === 'FONT_FAILED') { notifyUser(`Font "${error.details?.family}" could not be loaded`); } }); ``` See [Asset Loading Configuration](/docs/asset-loading) for timeouts and the rest of the loading configuration. ## TypeScript [#typescript] Every package exports the `PolotnoError` interface and a `PolotnoErrorCode` union of the codes that package throws, so comparing `error.code` against a misspelled code is a compile-time error: ```ts import type { PolotnoError, PolotnoErrorCode } from '@polotno/pdf-export'; // in the editor: import type { PolotnoError } from 'polotno/utils/errors'; ``` --- # Fonts & Text Consistency URL: https://polotno.com/docs/fonts-consistency This page covers **rendering consistency** issues. For general font configuration (adding fonts, Google Fonts, etc.), see the [Fonts guide](/docs/fonts). Polotno renders text using the browser's native text rendering engine. Different browsers and operating systems may produce slightly different results. This guide covers best practices to minimize these differences. ## The Problem [#the-problem] Ever seen text look different between browsers? Or your server-side export doesn't match what users see? Common causes: * Font files with incorrect metadata (weight class, vertical metrics) * Mismatched family names or weight declarations * Browsers synthesizing missing bold/italic faces differently *** ## How to Declare Fonts [#how-to-declare-fonts] How you declare fonts depends on whether your font file includes multiple variations: ### Single file with all variations [#single-file-with-all-variations] If your font file includes **all variations** (normal, bold, italic, bold+italic)β€”such as variable fonts: ```json { "fontFamily": "FiraSans", "url": "url('/fonts/FiraSans-VariableFont.ttf')" } ``` ### Separate files for each weight/style [#separate-files-for-each-weightstyle] If you have **separate files** (most common), register them as one family: ```json { "fontFamily": "FiraSans", "styles": [ { "src": "url('/fonts/FiraSans-Regular.ttf')", "fontStyle": "normal", "fontWeight": 400 }, { "src": "url('/fonts/FiraSans-Bold.ttf')", "fontStyle": "normal", "fontWeight": 700 }, { "src": "url('/fonts/FiraSans-Italic.ttf')", "fontStyle": "italic", "fontWeight": 400 } ] } ``` **❌ Don't do this:** ```json // Don't register bold as a separate family! { "fontFamily": "FiraSans-Bold", // Wrong "styles": [...] } ``` *** ## Font File Quality [#font-file-quality] For consistent rendering, font files need proper metadata: **Weight class matches declaration** * Internal `usWeightClass` should match your `fontWeight` value * Example: Bold file should have `usWeightClass=700` and `fontWeight: 700` **Consistent vertical metrics** * All faces in a family (Regular, Bold, Italic) must share the same ascender, descender, and line-gap * Mismatched metrics cause baseline shifts and spacing differences **Aligned naming tables** * Use consistent family/subfamily names across all faces * Example: "Fira Sans" and "FiraSans" might be treated as different families **Web-ready format** * Use TTF, WOFF, or WOFF2 formats * Keep identical metrics across all format conversions *** ## When to Normalize [#when-to-normalize] Normalize fonts if you see: * Different line spacing on macOS vs Windows * Bold weight looks different across browsers * Server exports don't match preview * Inconsistent third-party fonts **Font normalization** fixes the metadata issues above. You'll need to build your own server-side process using tools like: * **gftools + fonttools** (Python-based, good for automation) * **FontForge** (GUI or scriptable) * **Online converters** like [Transfonter](https://transfonter.org/) (for testing) Font normalization requires server-side implementation. All faces in a family must share identical vertical metrics. *** ## Validate Uploaded Fonts [#validate-uploaded-fonts] If users can upload custom fonts, validate them to catch issues early: **Check on upload:** * Weight class matches user selection * Vertical metrics are consistent across font faces * No duplicate or conflicting family names **Handle issues:** * Warn if metadata doesn't match declarations * Optionally auto-normalize and store processed versions * Use font parsing libraries to extract metadata --- # How to Resolve CORS Issues URL: https://polotno.com/docs/how-to-resolve-cors-issues In order to load images into the canvas element, Polotno sets the `crossOrigin` attribute to `anonymous` by default. In some configurations you may see an error like this: *Access to image at '[http://example.com/image.jpg](http://example.com/image.jpg)' from origin '[http://your-domain.com](http://your-domain.com)' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.* Here's the fix: ## 1. Make sure assets are served with CORS headers [#1-make-sure-assets-are-served-with-cors-headers] First, ensure that the server hosting your images allows CORS requests. If you control the server, add the header `Access-Control-Allow-Origin: *` to the response. If you use an S3 bucket, enable CORS in the bucket settings. See AWS docs: [https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html](https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html) ## 2. Make sure all images have `crossOrigin` attribute [#2-make-sure-all-images-have-crossorigin-attribute] Before adding images into the canvas, you may want to display them somewhere else in your application UI (e.g., a side panel or pre‑editor step). Make sure that all images have the `crossOrigin` attribute set to `anonymous`. For example, if you use an `img` tag: ```html ``` Explanation: if you load the same image in the same browsing session without the `crossOrigin` attribute, and later load it with `crossOrigin`, the browser will enforce CORS because it was initially fetched without the expected setting. Always include `crossOrigin="anonymous"` consistently. --- # Mobile Support URL: https://polotno.com/docs/mobile-support ## Does Polotno support mobile and touch devices? [#does-polotno-support-mobile-and-touch-devices] Yes. You can use it on mobile and touch devices to create beautiful designs. Polotno adapts to mobile and touch environments, ensuring a smooth, optimized design experience on any screen size. Mobile Note: Polotno SDK is primarily designed to work in web environments (not native apps). However, you can still use it in your app where a WebView is available. ## How to toggle mobile mode on large screens? [#how-to-toggle-mobile-mode-on-large-screens] In some cases you may want to force compact/mobile view on a large screen (e.g., when embedding the editor in a sidebar). Add the `polotno-mobile` class to any parent container of the Polotno editor, such as the `body` tag: ```html
``` ## Live demo [#live-demo] --- # React Version Support URL: https://polotno.com/docs/react-19-support Polotno targets **React 19** by default. Just install the package: ```bash npm i polotno ``` This is the **4.x** line and the recommended choice for new and existing projects. ## React 18 (legacy, available for now) [#react-18-legacy-available-for-now] A separate **3.x** line still supports React 18, published under the `next-react18` tag: ```bash npm i polotno@next-react18 ``` Both lines track the same features today, but **React 18 support is being phased out**. If you're on React 18, plan to upgrade to React 19 so you can move to the default `4.x` line β€” we recommend not starting new projects on the React 18 line. **Migrating from Polotno 2?** The default UI changed in 3.x / 4.x. See the [Polotno 4 Migration](/docs/polotno-4-migration) guide. ## Troubleshooting [#troubleshooting] If you see a peer dependency error like this during install: > npm error Could not resolve dependency: > > npm error peer react@"^18.x" from polotno …your React version doesn't match the installed line. On React 19 use `npm i polotno`; if you're still on React 18, use `npm i polotno@next-react18`. --- # Reactivity and Events URL: https://polotno.com/docs/reactivity-and-events Polotno's state is an [**mobx-state-tree store**](https://mobx-state-tree.js.org/), so every change is observable through [MobX](https://mobx.js.org/). In React, you get automatic UI updates by wrapping your components in `observer()`: ```tsx import { observer } from 'mobx-react-lite'; // the component will be automatically updated when number of children is changed export const App = observer(({ store }) => (

Elements on the current page: {store.activePage?.children.length}

)); ``` You can also use any data from the store as dependency to react hooks: ```tsx const App = observer(({ store }) => { React.useEffect(() => { console.log('width of the store is changed'); }, [store.width]); return <>; }) ``` ### React hooks + MobX reactions [#react-hooks--mobx-reactions] If you want logic that runs outside the render cycle, you can combine `useEffect` with `mobx.reaction` (or `autorun`): ```tsx import { reaction } from 'mobx'; import { useEffect } from 'react'; export function useElementCounter(store) { useEffect(() => { // fires every time the number of elements on the active page changes const dispose = reaction( () => store.activePage?.children.length, (length) => { console.log('Element count changed:', length); } ); return dispose; // clean-up on unmount }, [store]); // deps } ``` Under the hood, MobX tracks exactly what you read inside the first function and re-runs the second function only when that value changesβ€”no manual listeners, no polling. ## Events [#events] `store.on('change', handler)` is the only event we fire. It triggers on any mutation: add, remove, move, resize, undo, redo, you name it. ```tsx const unsubscribe = store.on('change', () => { console.log('Something in the store changed'); }); ``` ### Detecting specific actions [#detecting-specific-actions] Because every action funnels through the same event, you filter manually. **Example: Simple tracking of adding/removing elements** ```tsx let prevCount = store.activePage?.children.length ?? 0; const unsub = store.on('change', () => { const newCount = store.activePage?.children.length ?? 0; if (newCount > prevCount) { console.log('Element added'); } else if (newCount < prevCount) { console.log('Element removed'); } prevCount = newCount; }); ``` **Example: deeper tracking of adding/removing elements** If you need more gradual control, you can save more references to previous data to have a deeper diff. ```tsx let lastIds = {}; store.on('change', () => { const newIds = {}; store.find(item => { newIds[item.id] = item; }) for (const id in lastIds) { const deleted = !newIds[id]; if (deleted) { console.log(id, 'deleted'); } } for (const id in newIds) { const added = !lastIds[id]; if (added) { console.log(id, 'added'); } } lastIds = newIds; }); ``` **Example: Changes on one element** ```tsx import { reaction } from 'mobx'; function watchElement(element) { return reaction( () => ({ x: element.x, y: element.y, width: element.width, height: element.height, rotation: element.rotation, }), (coords) => { console.log('Element changed', coords); } ); } // later const dispose = watchElement(store.activePage.children[0]); ``` **Example: Page-level changes (title, background, etc.)** ```tsx reaction( () => ({ title: store.activePage?.name, bg: store.activePage?.background, }), (data) => console.log('Page updated', data) ); ``` ### Performance [#performance] `change` event can fire **dozens of times per second** during drag/resize. Avoid heavy work inside the handler - wrap it in throttle, or debounce. ```tsx // define function to save design to backend const saveDesign = async () => { // export the design const json = store.toJSON(); // save it to the backend await fetch('https://example.com/designs', { method: 'POST', body: JSON.stringify(json), }); } // write a function for throttle saving // it will call save no more then 1 time per second let timeout = null; const requestSave = () => { // if save is already requested - do nothing if (timeout) { return; } // schedule saving to the backend timeout = setTimeout(() => { // reset timeout timeout = null; saveDesign(); }, 1000); }; // request saving operation on any changes store.on('change', requestSave); ``` --- # Remove Background API URL: https://polotno.com/docs/remove-background-api ## How to use the Remove Background feature programmatically? [#how-to-use-the-remove-background-feature-programmatically] As part of the Business plan, you can use the API to remove backgrounds from images. To call the API from your app: ```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-or-base64-dataurl' }), } ); if (req.status !== 200) { throw new Error('Error while removing background'); } const res = await req.json(); // returns base64 data URL return res.url; ``` --- # Saving custom data into a design URL: https://polotno.com/docs/saving-custom-data-into-a-design ## How to save additional metadata into a design? [#how-to-save-additional-metadata-into-a-design] In some use cases, you may want to save your own data into the store/page/element. You may want to save: * Name of the design * Design id * Custom page properties * Element metadata For such use cases, you can use the `custom` attribute. It is available for all nodes in the store: the store itself, any page, any element. ```ts // set custom data element.set({ custom: { // optionally, keep previous custom data ...element.custom, // save new data price: 10, }, }); // read custom data // notice that by default element.custom will be null const price = element.custom?.price; // also you can save metadata into a page page.set({ custom: { ...page.custom, reference: 'sample', }, }); // or into store store.set({ custom: { productId: 'some-id', }, }); ``` `custom` must be an object `{}`. You can write any data into that object. Polotno never uses it for rendering, so it is under your control. By default, `custom` will be `null`, so when you read some data from `custom`, make sure it is defined. The `custom` attribute is included in [JSON export](/docs/import-and-export) via `store.toJSON()`, making it perfect for saving metadata that travels with your design templates. ## Use Cases [#use-cases] * **Template variables**: Store variable names for [dynamic template generation](/docs/dynamic-template-variables) * **Design metadata**: Save author, version, or creation date * **Element IDs**: Track external database references * **QR code data**: Store encoded content for [QR code elements](/docs/qr-codes) --- # Store Observer and Reactions URL: https://polotno.com/docs/store-observer-and-reactions Polotno has a rich [store API](/docs/store) that you can use to observe and control canvas content. The store object is created using the MobX library. You can use MobX utilities to build reactive components. ## Observable component [#observable-component] To create a component that updates automatically when the canvas changes, use the `observer` API from `mobx-react-lite`: ```tsx import { observer } from 'mobx-react-lite'; import { createStore } from 'polotno/model/store'; const store = createStore({ key: 'YOUR_API_KEY', // you can create it here: https://polotno.com/cabinet/ }); // this react component is wrapped into observer // it will be automatically updated when number of elements on active page changes const App = observer(() => { return (

Elements on the current page: {store.activePage?.children.length}

); }); ``` ## Store reaction [#store-reaction] The `store` object has a `change` event triggered when canvas content is modified. Selecting/deselecting elements will NOT trigger the `change` event. ```ts store.on('change', () => { console.log('something is changed'); }); ``` In some cases, you may want to react to selection or other data changes in the store. Use MobX `reaction` or `autorun`: ```ts import { reaction } from 'mobx'; const dispose = reaction( () => { const firstElement = store.selectedElements[0]; const isTextSelected = firstElement?.type === 'text'; return isTextSelected; }, (isTextSelected) => { // here we can do something when text is selected console.log('text is selected', isTextSelected); } ); // later when no longer needed dispose(); ``` --- # Units and Measures URL: https://polotno.com/docs/units-and-measures ## What metrics are using in Polotno? [#what-metrics-are-using-in-polotno] By default Polotno is using CSS **pixels** as units with **1pt = 1px** and **72dpi**. Everything you see in the **store** is in **pixels** – **width**, **height** or **fontSize**; Polotno will use these values in UI. You can change units displayed in UI using **setUnits** method. ```ts store.setUnit({ unit: 'mm', // mm, cm, in, pt, px dpi: 300, }); ``` ## How to display units in custom UI? [#how-to-display-units-in-custom-ui] Also you can use special functions to convert units to pixels and back: ```ts import { pxToUnitRounded, unitToPx, pxToUnit } from 'polotno/utils/unit'; // convert 100 pixels to mm with 300 DPI var mm = pxToUnit({ px: 100, unit: 'mm', dpi: 300, }); // do the same, but with 2 digits rounding, so number doesn't look ugly long in UI var mm = pxToUnitRounded({ px: 100, unit: 'mm', precious: 2, dpi: 300, }); // convert 30 mm to pixels with 300 DPI var pixels = unitToPx({ unit: 'mm', dpi: 300, unitVal: 30, }); ``` ## Exports and DPI [#exports-and-dpi] When you [export your design to PDF](/docs/pdf-export), Polotno will use 72 DPI to determine the size of the exported pages. If you increase just the dpi, it will not change quality of the exported PDF. It will just produce a PDF file with smaller page sizes. You have two options to increase quality of your exported designs: 1. Use high **pixelRatio** attribute when you do the export. `store.saveAsPDF({ pixelRatio: 2 })` 2. Change DPI and change ALL pixels values in the store. ### DPI Metadata in PNG and JPEG Exports [#dpi-metadata-in-png-and-jpeg-exports] For PNG and JPEG exports, Polotno can embed DPI resolution metadata directly into the image file. This is useful for print workflows where downstream software needs to know the intended print resolution. ```ts // Embed 300 DPI metadata await store.saveAsImage({ dpi: 300 }); // Control when metadata is embedded await store.toDataURL({ dpi: 300, dpiMetadata: 'auto' // 'auto' | 'always' | 'never' }); ``` **Behavior:** * `dpiMetadata: 'auto'` (default) β€” Embeds DPI only when it differs from 72 * `dpiMetadata: 'always'` β€” Always embeds DPI metadata * `dpiMetadata: 'never'` β€” Never embeds DPI metadata The `dpi` parameter defaults to `store.dpi` if not specified. DPI metadata embedding only works with PNG and JPEG formats, not PDF or other formats. ## Common Questions [#common-questions] ### How do I create print-ready designs? [#how-do-i-create-print-ready-designs] Use `store.setUnit({ unit: 'mm', dpi: 300 })` to work in print units. For export quality, increase `pixelRatio` in [PDF export](/docs/pdf-export). ### Why doesn't changing DPI improve export quality? [#why-doesnt-changing-dpi-improve-export-quality] DPI only affects UI display and page dimensions, not render quality. Use `pixelRatio` in [export functions](/docs/import-and-export) to increase quality. ### Can I mix units in a design? [#can-i-mix-units-in-a-design] The canvas editor internally uses pixels. Units are for display only. Use conversion functions to work with different units programmatically. --- # Background side panel customization URL: https://polotno.com/docs/background-side-panel-customization You can change the default search query used in the background side panel: ```ts import { setDefaultQuery } from 'polotno/side-panel/background-panel'; setDefaultQuery('city'); ``` You can't change other aspects of the default background side panel. If you need more customizations, remove the default panel and implement your own version for full control. You can change the default color preset using this: ```ts import { setBackgroundColorsPreset } from 'polotno/side-panel/background-panel'; setBackgroundColorsPreset(['red', 'green']); ``` ## Live demo [#live-demo] --- # Custom images panel URL: https://polotno.com/docs/custom-images-panel ## How to load custom photos in the side panel? [#how-to-load-custom-photos-in-the-side-panel] Using the customization API, you can add a new section to the Side Panel to display images from any remote API or your own backend. You can write a custom panel that loads images from an API: ```tsx import React from 'react'; import { observer } from 'mobx-react-lite'; import { SearchInput } from 'polotno/primitives'; import { SectionTab } from 'polotno/side-panel'; import { ImagesGrid } from 'polotno/side-panel/images-grid'; import { getImageSize } from 'polotno/utils/image'; import MdPhotoLibrary from '@meronex/icons/md/MdPhotoLibrary'; export const PhotosPanel = observer(({ store }) => { const [images, setImages] = React.useState>([]); async function loadImages() { // implement your API requests here setImages([]); // emulate network request await new Promise((resolve) => setTimeout(resolve, 3000)); // demo data; in a real app use an API response setImages([ { url: './carlos-lindner-zvZ-HASOA74-unsplash.jpg' }, { url: './guillaume-de-germain-TQWJ4rQnUHQ-unsplash.jpg' }, ]); } React.useEffect(() => { loadImages(); }, []); return (
{ loadImages(); }} style={{ marginBottom: 20 }} />

Demo images:

{/* you can create your own custom component here */} {/* but we will use built-in grid component */} image.url} onSelect={async (image, pos, element, event) => { // image - an item from your array // pos - relative mouse position on drop. undefined if user just clicked // element - model from your store if image was dropped on an element // event - can contain additional data const { width, height } = await getImageSize(image.url); store.activePage?.addElement({ type: 'image', src: image.url, width, height, x: pos?.x || 0, y: pos?.y || 0, }); }} rowsNumber={2} isLoading={!images.length} loadMore={false} />
); }); // define the new custom section export const CustomPhotos = { name: 'photos', Tab: (props) => ( ), Panel: PhotosPanel, }; ``` ## Live demo [#live-demo] --- # Customizing Resize Panel URL: https://polotno.com/docs/customizing-resize-panel ## How to set your own default page sizes? [#how-to-set-your-own-default-page-sizes] You can make your own panel from scratch and define new Sizes section. For example: ```tsx import React, { useState, useEffect } from 'react'; import { observer } from 'mobx-react-lite'; import { Button, NumericInput, Select, SelectTrigger, SelectContent, SelectItem, SelectValue, } from 'polotno/primitives'; import { pxToUnitRounded, unitToPx } from 'polotno/utils/unit'; const MIN_PX = 10; const PRESETS = [ { label: 'IG Post', w: 1080, h: 1080, unit: 'px' }, { label: 'IG Story', w: 1080, h: 1920, unit: 'px' }, { label: 'Full HD', w: 1920, h: 1080, unit: 'px' }, { label: 'A4', w: 21, h: 29.7, unit: 'cm' }, { label: 'Letter', w: 8.5, h: 11, unit: 'in' }, ]; export const ResizePanel = observer(({ store }) => { const [w, setW] = useState(0); const [h, setH] = useState(0); useEffect(() => { setW( pxToUnitRounded({ px: store.width, unit: store.unit, dpi: store.dpi }) ); setH( pxToUnitRounded({ px: store.height, unit: store.unit, dpi: store.dpi }) ); }, [store.width, store.height, store.unit, store.dpi]); const applyResize = (unitW = w, unitH = h) => { const widthPx = unitToPx({ unitVal: unitW, unit: store.unit, dpi: store.dpi, }); const heightPx = unitToPx({ unitVal: unitH, unit: store.unit, dpi: store.dpi, }); if (widthPx >= MIN_PX && heightPx >= MIN_PX) store.setSize(widthPx, heightPx, true); }; const Row = ({ children }) => (
{children}
); return (
Width
Height
Units
{/* preset buttons, one per row */} {PRESETS.map(({ label, w: pw, h: ph, unit }) => ( ))}
); }); ``` ## Live demo [#live-demo] --- # Hidden panels URL: https://polotno.com/docs/hidden-panels ## Built-in hidden panels [#built-in-hidden-panels] Polotno includes several internal hidden panels that are not shown in the tab bar but open programmatically via `store.openSidePanel(name)`. These panels are triggered by toolbar buttons (e.g. clicking "Effects" in the image toolbar opens the `effects` panel). | Panel name | Description | Triggered by | | ------------ | ---------------------------------------- | -------------------- | | `effects` | Image/element filters and effects editor | Toolbar "Effects" | | `animation` | Animation settings for selected element | Toolbar "Animate" | | `image-clip` | Image clipping / mask editor | Toolbar "Apply mask" | ```ts // Open the built-in effects panel for the selected element store.openSidePanel('effects'); // Open the animation panel store.openSidePanel('animation'); // Open the image clip/mask panel store.openSidePanel('image-clip'); ``` ## How to implement your own hidden panel? [#how-to-implement-your-own-hidden-panel] You can create custom hidden panels that don't appear in the tab bar but open based on user actions, similar to the built-in ones above. To implement a hidden panel: ```tsx // define the new custom section const CustomSection = { name: 'custom-text', // use "empty" tab that will render nothing Tab: () => null, // we need observer to update component automatically on any store changes Panel: observer(({ store }) => { const text = store.selectedElements[0]?.text; return (
{ store.selectedElements[0].set({ text: e.target.value }); }} />
); }), }; // add new section to side bar const sections = [...DEFAULT_SECTIONS, CustomSection]; // use mobx reaction to react to selection changes reaction( () => { const textSelected = store.selectedElements[0]?.type === 'text'; return textSelected; }, // we can use result returned from reaction (textSelected) => { if (textSelected) { store.openSidePanel('custom-text'); } } ); export const App = () => { return ( ); }; ``` For the demo, try to create a text element. As soon as it is selected, you will see a new side panel opened. ## Live demo [#live-demo] --- # Pexels photos URL: https://polotno.com/docs/pexels-photos You can use customization of Polotno's Side Panel to add your own panels with your own assets. ## How to integrate photos from Pexels.com API? [#how-to-integrate-photos-from-pexelscom-api] First, set up your own backend proxy to the Pexels API. See the official docs: [https://www.pexels.com/api](https://www.pexels.com/api) Example using Node.js: ```js module.exports = async (req, res) => { var i = req.url.indexOf('?'); var query = req.url.substr(i + 1); // if no query provided, let's return a list of curated photos const searchPrefix = req.query.query ? 'search/' : '/curated'; const r = await fetch( `https://api.pexels.com/v1/${searchPrefix}?query=${encodeURIComponent( req.query.query )}&page=${req.query.page}&per_page=${req.query.per_page}`, { headers: { Authorization: 'YOUR_API_KEY_FROM_PEXELS', }, } ); if (r.status !== 200) { return res.status(r.status).send(await r.text()); } let result = await r.json(); res.status(r.status).json(result); }; ``` After creating your API endpoint, create a new Side Panel section and show it in Polotno Editor. Example section using the proxy: ```tsx import React from 'react'; import { SearchInput } from 'polotno/primitives'; import { ImagesGrid } from 'polotno/side-panel/images-grid'; import { getImageSize, getCrop } from 'polotno/utils/image'; import { SectionTab } from 'polotno/side-panel'; import { useInfiniteAPI } from 'polotno/utils/use-api'; import { t } from 'polotno/utils/l10n'; import SiPexels from '@meronex/icons/si/SiPexels'; // this is a demo key just for that project // (!) please don't use it in your projects // to create your own API key please go here: https://polotno.com/login const key = 'nFA5H9elEytDyPyvKL7T'; // use Polotno API proxy into Pexels // WARNING: don't use on production! Use your own proxy and Pexels API key const API = 'https://api.polotno.com/api'; const getPexelsAPI = ({ query, page }: { query: string; page: number }) => `${API}/get-pexels?query=${query}&per_page=20&page=${page}&KEY=${key}`; export const PexelsPanel = ({ store }: { store: any }) => { const { setQuery, loadMore, isReachingEnd, data, isLoading, error } = useInfiniteAPI({ defaultQuery: '', getAPI: ({ page, query }) => getPexelsAPI({ page, query }), getSize: (lastResponse) => lastResponse.total_results / lastResponse.per_page, }); return (
) => { setQuery(e.target.value); }} style={{ marginBottom: '20px' }} />

Photos by{' '} Pexels

item.photos).flat().filter(Boolean)} getPreview={(image: any) => image.src.medium} onSelect={async (image: any, pos?: { x: number; y: number }, element?: any) => { // get url of image const src = image.src.large; // if we dropped image into svg element, apply mask for it if (element && element.type === 'svg' && element.contentEditable) { element.set({ maskSrc: src }); return; } // get image size const { width, height } = await getImageSize(src); // if dropped into another image, recalculate crop and apply new image if (element && element.type === 'image' && element.contentEditable) { const crop = getCrop(element, { width, height }); element.set({ src, ...crop }); return; } // otherwise create new image const x = (pos?.x || store.width / 2) - width / 2; const y = (pos?.y || store.height / 2) - height / 2; store.activePage?.addElement({ type: 'image', src, width, height, x, y, }); }} isLoading={isLoading} error={error} loadMore={!isReachingEnd && loadMore} getCredit={(image: any) => ( Photo by{' '} {image.photographer} {' '} on{' '} Pexels )} />
); }; // define the new custom section export const PexelsSection = { name: 'pexels', Tab: (props: any) => ( ), // we need observer to update component automatically on any store changes Panel: PexelsPanel, }; ``` When you created a new section in the side panel, pass it into ``: ```tsx import { SidePanel, DEFAULT_SECTIONS } from 'polotno/side-panel'; import { PexelsSection } from './pexels-section'; const sections = [PexelsSection, ...DEFAULT_SECTIONS]; // in render: ``` ## Live demo [#live-demo] --- # Remove side panel URL: https://polotno.com/docs/remove-side-panel ## How to remove the side panel? [#how-to-remove-the-side-panel] You can control side panels in Polotno SDK. If you don't need some of the panels, remove them by name. ```tsx import { PolotnoContainer, SidePanelWrap, WorkspaceWrap } from 'polotno'; import { SidePanel, DEFAULT_SECTIONS } from 'polotno/side-panel'; import { Toolbar } from 'polotno/toolbar/toolbar'; import { ZoomButtons } from 'polotno/toolbar/zoom-buttons'; import { PagesTimeline } from 'polotno/pages-timeline'; import { Workspace } from 'polotno/canvas/workspace'; // first you can log all sections into console to see what sections are defined const names = DEFAULT_SECTIONS.map((section) => section.name); console.log(names); // if you don't need a specific section, remove it by its name // for example, remove the upload section const sections = DEFAULT_SECTIONS.filter((section) => section.name !== 'upload'); export const App = () => { return ( ); }; ``` ## Live demo [#live-demo] --- # Side Panel overview URL: https://polotno.com/docs/side-panel-overview **SidePanel** provides a default set of components for adding new elements to the canvas, changing page sizes, and more. ```tsx import { SidePanel } from 'polotno/side-panel'; const MyPanel = () => { return (
); }; ``` The `SidePanel` will automatically use the full width and height of its parent; you don't have to manually adjust its size. Set the size of the parent `div` with CSS. ## How to hide the side panel on initial load? [#how-to-hide-the-side-panel-on-initial-load] Use the `defaultSection` prop to control which section is open when the component first mounts. Pass an empty string to start with the panel closed: ```tsx ``` **Note:** `store.openSidePanel('')` will not work for this purpose because it is called before `` mounts, and the component overrides it with its default on mount. ## How to customize side panel tabs? [#how-to-customize-side-panel-tabs] You can pass the `sections` property to the `` component to specify all available tabs manually. Default UI: ```tsx import { SidePanel, DEFAULT_SECTIONS } from 'polotno/side-panel'; const MyPanel = () => { return (
); }; ``` Define sections manually: ```tsx import { observer } from 'mobx-react-lite'; import { SidePanel } from 'polotno/side-panel'; // import existing section import { TextSection } from 'polotno/side-panel'; // import default tab component import { SectionTab } from 'polotno/side-panel'; // import our own icon import FaShapes from '@meronex/icons/fa/FaShapes'; // define the new custom section const CustomSection = { name: 'custom', Tab: (props) => ( ), // we need observer to update component automatically on any store changes Panel: observer(({ store }) => { return (

Here we will define our own custom tab.

Elements on the current page: {store.activePage?.children.length}

); }), }; // we will have just two sections const sections = [CustomSection, TextSection]; const CustomSidePanel = () => { return ; }; ``` ## Live demo [#live-demo] ## Related Pages [#related-pages] * [Custom Images Panel](/docs/custom-images-panel) - Load images from your own backend * [Pexels Photos](/docs/pexels-photos) - Integrate Pexels API for stock photos * [Upload Panel](/docs/upload-panel) - Customize file upload functionality * [Background Side Panel Customization](/docs/background-side-panel-customization) - Customize background selection panel * [Hidden Panels](/docs/hidden-panels) - Hide specific default panels * [Remove Side Panel](/docs/remove-side-panel) - Remove the side panel entirely --- # Upload panel URL: https://polotno.com/docs/upload-panel **Important:** the default upload panel has the most basic setup. It can't keep uploaded images across user sessions, and it doesn't return images to the backend. Instead it injects images as base64 string directly into the design, which may affect performance. It is highly recommended to use your own version of an upload panel. ## How to implement upload section? [#how-to-implement-upload-section] Upload section may work very similar to the [Images Section](/docs/custom-images-panel). You just need to implement several API endpoints on your backend to allow: 1. Loading list of user images 2. Uploading new image 3. Deleting image The example below doesn't use a real server to implement this function. It just uses local storage to store images. But you can use it as a starting point. ## Live demo [#live-demo] --- # Utils API URL: https://polotno.com/docs/utils-api When building a custom side panel feel free to choose any React components you prefer. You can build your own one from scratch as well. Displaying a grid of media previews is a common requirement inside custom side panels. You can reuse `` to cover layout, lazy loading, and drag-and-drop without rebuilding those behaviors yourself. ## ImagesGrid component [#imagesgrid-component] Use `` to render any collection as clickable previews. You provide the data array and the preview accessor, and the component renders responsive columns, shows loading states, and wires drag events into the canvas. Items can be clicked or dragged, letting you decide how to create or update elements in the workspace. * Render arbitrary data as image thumbnails with lazy loading * Trigger canvas updates on click or drag * Request more data automatically when the list runs low ### Basic usage [#basic-usage] Start with a minimal panel that passes the image collection, a preview getter, and an `onSelect` callback. The handler receives the original item plus optional drop coordinates and the element that was targeted on the canvas. ```tsx import { ImagesGrid } from 'polotno/side-panel/images-grid'; const images = [ { id: '1', url: 'https://picsum.photos/seed/1/600/800' }, { id: '2', url: 'https://picsum.photos/seed/2/600/800' }, ]; export const TemplatesPanel = ({ store }) => ( item.url} getAlt={(item) => item.alt} onSelect={(image, pos, element) => { const width = 200; const height = 200; const x = (pos?.x ?? store.width / 2) - width / 2; const y = (pos?.y ?? store.height / 2) - height / 2; store.activePage?.addElement({ type: 'image', src: image.url, width, height, x, y, }); }} isLoading={false} /> ); ``` > **Tip**: The component calls your `onSelect` handler for both click and drop interactions. Use the optional `element` argument to detect when a user drops onto an existing node and branch your logic. ### Infinite loading [#infinite-loading] `loadMore` fires when the scroll position approaches the bottom or when the grid finishes rendering but still cannot fill the viewport. Pair it with `isLoading` and optional API helpers to keep results flowing. ```tsx import { useInfiniteAPI } from 'polotno/utils/use-api'; const { data, isLoading, loadMore } = useInfiniteAPI({ getAPI: ({ page }) => `https://example.com/images?page=${page}`, getSize: (firstResult) => firstResult.total_pages, }); const images = data?.flatMap((page) => page.results) ?? []; item.src} getAlt={(item) => item.alt} // optional onSelect={(image) => addImageToStore(image)} isLoading={isLoading} loadMore={loadMore} hideNoResults={false} />; ``` > **Tip**: `loadMore` receives no arguments, so keep pagination state outside the component. Call `loadMore` only when `isLoading` is `false` to avoid overlapping requests. ### Drag and drop behavior [#drag-and-drop-behavior] Dragging an item registers a temporary DOM drop listener through Polotno’s `registerNextDomDrop`. You receive canvas coordinates and the optional target element, enabling both β€œcreate new” and β€œreplace existing” flows with a single handler. The component clears the listener after each drag cycle so you do not need manual cleanup. > **Note**: If you build a custom drop zone outside of the canvas, call `registerNextDomDrop(null)` when the drag ends to avoid dangling handlers. ### Styling options [#styling-options] Control the layout with `rowsNumber` (columns), explicit `itemHeight`, and the `shadowEnabled` flag. Every image is responsive by default, and credits can be shown via `getCredit` to comply with provider requirements. > **Note**: On mobile breakpoints the credit overlay stays visible, so keep the element short and avoid interactive children. ### Prop reference [#prop-reference] | Prop | Type | Default | Description | | | | | ------------------- | ---------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------ | | `images` | \`ImageType\[] | undefined\` | `[]` | Collection rendered as thumbnails; pass an empty array while loading to keep layout stable. | | | | `getPreview` | `(image: ImageType) => string` | β€” | Returns the image URL used for the thumbnail and drag payload. | | | | | `getAlt` | `(image: ImageType) => string` | `undefined` | Returns the image alt text used for the thumbnail. | | | | | `onSelect` | `(image: ImageType, pos?, element?, event?) => void` | β€” | Handles both click and drop; receive drop coordinates and target element when available. | | | | | `isLoading` | `boolean` | `false` | Shows the spinner at the end of each column and blocks automatic `loadMore` triggers. | | | | | `loadMore` | \`() => void | false | null | undefined\` | `undefined` | Called near the bottom of the scroll area or when the grid cannot fill the viewport. | | `getCredit` | `(image: ImageType) => React.ReactNode` | `undefined` | Renders a credit overlay over the image; hidden on desktop hover until active. | | | | | `getImageClassName` | `(image: ImageType) => string` | `undefined` | Adds custom class names to the `` tag for further styling. | | | | | `rowsNumber` | `number` | `2` | Sets the number of columns; the grid distributes items evenly by index. | | | | | `crossOrigin` | `string` | `'anonymous'` | Controls the `crossOrigin` attribute on the `` tag to support canvas exports. | | | | | `shadowEnabled` | `boolean` | `true` | Toggles the drop shadow around image containers. | | | | | `itemHeight` | \`number | string\` | `'auto'` | Fixes thumbnail height while preserving width; defaults to auto sizing. | | | | `error` | `any` | `undefined` | When truthy, the grid shows the localized error message instead of thumbnails. | | | | | `hideNoResults` | `boolean` | `false` | Suppresses the default β€œno results” label when the data array is empty. | | | | ## How to use `useInfiniteAPI` hook? [#how-to-use-useinfiniteapi-hook] In scenarios where an API is called to display a list of images within a side panel grid, various React tools are at your disposal. This includes utilizing fetch methods within hooks or leveraging any suitable library for the task. Additionally, Polotno provides the `useInfiniteAPI` hook as a convenient option. The `useInfiniteAPI` hook serves as a wrapper around the [swr](https://swr.vercel.app/) library, simplifying the process of fetching data. ```ts import { useInfiniteAPI } from 'polotno/utils/use-api'; export const SidePanel = () => { const { data, isLoading, loadMore, isReachingEnd, hasMore, reset, error } = useInfiniteAPI({ // a function that will return a URL to request getAPI: ({ page, query }) => `https://example.com/api?page=${page}&query=${query}`, // default search query for the first call call defaultQuery: '', // timeout before making a new call when you change search query, useful for debouncing timeout: 500, // a function that should return number of pages available from the first API response // usually API response has a "totalPages", "size" or other field that tells you how many pages are available getSize: (firstResult) => firstResult.total_pages, // a function to make an API request // here is example of default fetch function // you can customize it to add for example headers fetchFunc: (url) => fetch(url).then((r) => r.json()), }); // data - is an array of responses from the API // each item in the array corresponds to a page // loadMore - function to be called when you want to request for more data // ImagesGrid will use it when you scrape the bottom of the list // isReachingEnd - true if you are at the end of the list // hasMore - true if you can request for more data // reset - function to be called when you want to reset the list // error - error object if something went wrong }; ``` ## How to drop elements from side panel into workspace? [#how-to-drop-elements-from-side-panel-into-workspace] If you prefer not to use `` component, you will have to handle drag\&drop of DOM elements implementation. However, Polotno has some tools to listen to drop events on the workspace. You can use this: ```tsx import { unstable_registerNextDomDrop } from 'polotno/config'; // then in your components inside side panel you can do something like this: { registerNextDomDrop((pos, element) => { // "pos" - is relative mouse position of drop // "element" - is element from your store in case when DOM object is dropped on another element // you can just create new element on drop position // or you can update existing element // for example we can drop image from side panel into existing 'image' element in the workspace if (element && element.type === 'image') { // you can update any property you want, src, clipSrc, border, etc element.set({ src: url }); return; } // or we can just create a new element store.activePage.addElement({ type: 'image', src: url, x: pos.x, y: pos.y, width: 100, height: 100, }); }); }} onDragEnd={() => { registerNextDomDrop(null); }} />; ``` --- # Sandpack Demo Component URL: https://polotno.com/docs/components/_sandpack-demo ## Sandpack Demo Component [#sandpack-demo-component] The `SandpackDemo` component allows you to create interactive code sandboxes with Polotno pre-configured. It's perfect for demonstrating code examples in your documentation. ## Basic Usage [#basic-usage] Here's a basic Polotno editor: ## Custom Dependencies [#custom-dependencies] You can add custom dependencies beyond the default ones: ## Custom Code Example [#custom-code-example] You can provide custom files to show specific examples: ## Component Props [#component-props] The `SandpackDemo` component accepts the following props: ### `dependencies` [#dependencies] * **Type**: `Record` * **Default**: `{}` * **Description**: Custom dependencies to include alongside the default ones (polotno, react, react-dom, react-scripts) ### `files` [#files] * **Type**: `Record` * **Default**: `{}` * **Description**: Custom files to include in the sandbox. If not provided, uses a default App.js with basic Polotno setup ### `options` [#options] * **Type**: `object` * **Default**: `{}` * **Description**: Custom options for the Sandpack instance (showNavigator, showTabs, etc.) ### `height` [#height] * **Type**: `number | string` * **Default**: `400` * **Description**: Height of the sandbox editor ### `theme` [#theme] * **Type**: `'light' | 'dark' | 'auto'` * **Default**: `'light'` * **Description**: Theme for the sandbox ## Advanced Example [#advanced-example] --- # Brand Kit Side Panel URL: https://polotno.com/docs/components/brand-kit The Brand Kit side panel is a tool that allows you to manage your brand kit. It includes a color palette, typography, and assets. We recommend to read the [Overview](./overview) page to understand how to use the Brand Kit side panel. ## Installation [#installation] ```bash npx shadcn@latest add http://registry.polotno.com:/r/brand-kit.json --path ./src/components/side-panel/brand-kit/ ``` After installatin you will see a new files in `/src/components/side-panel/brand-kit/` You can change setup location as you want. ## Usage [#usage] ```tsx // make sure to change the path to the correct location import { BrandKitSection } from './components/side-panel/brand-kit/brand-kit-panel'; import { SidePanel, DEFAULT_SECTIONS } from 'polotno/side-panel'; const sections = [...DEFAULT_SECTIONS, BrandKitSection]; // in sidepanel render: ``` ## Demo [#demo] --- # Components Overview URL: https://polotno.com/docs/components/overview ## Open components β€” own and customize [#open-components--own-and-customize] These components are installed as source files into your repository. You commit them, edit them, and adapt them to your product. They’re designed to be customized β€” markup, styles, and behavior are all yours. Note: we still publish npm components for convenience. These β€œopen components” are an additional option when you want full control and easy customization. ## Add components [#add-components] Use our CLI to add components; it copies files into your repo (commonly under `./src/components/`). ```bash # add a few UI components npx shadcn@latest add http://registry.polotno.com:/r/qr-code.json ``` ## Customize [#customize] * Edit the component files directly to match your UX, API and business logic * Because it’s your code, refactors and reviews are straightforward ## Update or remove [#update-or-remove] * To update, add the component again and compare diffs in Git (cherry‑pick what you need) * To remove, delete the files and related imports β€” no special uninstall --- # QR Code Side Panel URL: https://polotno.com/docs/components/qr-code ## Installatin [#installatin] ```bash npx shadcn@latest add http://registry.polotno.com:/r/qr-code.json --path ./src/components/side-panel/qr-code.tsx ``` After installatin you will see a new create file in `/src/components/side-panel/qr-code.tsx`. You can change setup location as you want. ```bash npx install qr-code @meronex/icons ``` Copy and paste the following code into your project. ## Usage [#usage] ```tsx import { QrSection } from './side-panel/qr-code'; // make sure to change the path to the correct location import { SidePanel, DEFAULT_SECTIONS } from 'polotno/side-panel'; const sections = [...DEFAULT_SECTIONS, QrSection]; ``` ## Demo [#demo] --- # Animation URL: https://polotno.com/docs/schema/animation ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ---------- | --------------------------------- | --------------------------------------------------- | | `delay` | `number` | Delay before the animation starts, in milliseconds. | | `duration` | `number` | Animation duration in milliseconds. | | `enabled` | `boolean` | Whether the animation is enabled. | | `type` | `"enter"` \| `"exit"` \| `"loop"` | Animation phase. | | `name` | `string` | Animation name identifier. | | `data` | `unknown` | Animation-specific data. | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "delay": { "default": 0, "description": "Delay before the animation starts, in milliseconds.", "type": "number" }, "duration": { "default": 500, "description": "Animation duration in milliseconds.", "type": "number" }, "enabled": { "default": true, "description": "Whether the animation is enabled.", "type": "boolean" }, "type": { "type": "string", "enum": [ "enter", "exit", "loop" ], "description": "Animation phase." }, "name": { "default": "none", "description": "Animation name identifier.", "type": "string" }, "data": { "default": {}, "description": "Animation-specific data." } }, "required": [ "type" ] } ``` ## Related Definitions [#related-definitions] *No related definitions found.* --- # Audio URL: https://polotno.com/docs/schema/audio ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ----------- | --------- | --------------------------------------- | | `id` | `string` | Unique identifier for the audio track. | | `src` | `string` | URL or data URI of the audio file. | | `duration` | `number` | Duration in milliseconds. | | `startTime` | `number` | Start offset (0-1). | | `endTime` | `number` | End offset (0-1). | | `volume` | `number` | Volume level (0-1). | | `delay` | `number` | Delay before playing, in milliseconds. | | `speed` | `number` | Playback speed multiplier (1 = normal). | | `custom` | `unknown` | *No description provided* | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the audio track." }, "src": { "default": "", "description": "URL or data URI of the audio file.", "type": "string" }, "duration": { "default": 0, "description": "Duration in milliseconds.", "type": "number" }, "startTime": { "default": 0, "description": "Start offset (0-1).", "type": "number" }, "endTime": { "default": 1, "description": "End offset (0-1).", "type": "number" }, "volume": { "default": 1, "description": "Volume level (0-1).", "type": "number" }, "delay": { "default": 0, "description": "Delay before playing, in milliseconds.", "type": "number" }, "speed": { "default": 1, "description": "Playback speed multiplier (1 = normal).", "type": "number" }, "custom": {} }, "required": [ "id" ] } ``` ## Related Definitions [#related-definitions] *No related definitions found.* --- # BorderSide URL: https://polotno.com/docs/schema/border-side Top border override for this cell. ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | -------- | ------------------------------------------------- | ------------------------------------- | | `color` | `string` | Border color for this side. | | `width` | `number` | Border width in pixels for this side. | | `style` | `"solid"` \| `"dashed"` \| `"dotted"` \| `"none"` | Border style for this side. | ## JSON Schema [#json-schema] ```json { "description": "Top border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } } ``` ## Related Definitions [#related-definitions] *No related definitions found.* --- # CellBorders URL: https://polotno.com/docs/schema/cell-borders Per-side border overrides for this cell. ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | -------- | ----------------------------- | ------------------------------------- | | `top` | [`BorderSide`](./border-side) | Top border override for this cell. | | `right` | [`BorderSide`](./border-side) | Right border override for this cell. | | `bottom` | [`BorderSide`](./border-side) | Bottom border override for this cell. | | `left` | [`BorderSide`](./border-side) | Left border override for this cell. | ## JSON Schema [#json-schema] ```json { "description": "Per-side border overrides for this cell.", "type": "object", "properties": { "top": { "description": "Top border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } }, "right": { "description": "Right border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } }, "bottom": { "description": "Bottom border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } }, "left": { "description": "Left border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } } } } ``` ## Related Definitions [#related-definitions] * [BorderSide](./border-side) --- # Element URL: https://polotno.com/docs/schema/element ## Type Information [#type-information] **Base Type:** `union` ## Variants [#variants] A value of this type is one of: * [TextElement](./text-element) * [ImageElement](./image-element) * [SVGElement](./svg-element) * [LineElement](./line-element) * [FigureElement](./figure-element) * [GifElement](./gif-element) * [VideoElement](./video-element) * [TableElement](./table-element) * [GroupElement](./group-element) ## JSON Schema [#json-schema] ```json { "oneOf": [ { "type": "text" }, { "type": "image" }, { "type": "svg" }, { "type": "line" }, { "type": "figure" }, { "type": "gif" }, { "type": "video" }, { "type": "table" }, { "type": "group" } ] } ``` ## Related Definitions [#related-definitions] * [Animation](./animation) * [TableCell](./table-cell) * [CellBorders](./cell-borders) * [BorderSide](./border-side) --- # FigureElement URL: https://polotno.com/docs/schema/figure-element ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ------------------- | ---------------------------------- | --------------------------------------------- | | `id` | `string` | Unique identifier for the element. | | `type` | `"figure"` | *No description provided* | | `name` | `string` | Display name for the element. | | `custom` | `unknown` | Custom data attached to the element. | | `x` | `number` | X position in pixels. | | `y` | `number` | Y position in pixels. | | `width` | `number` | Width in pixels. | | `height` | `number` | Height in pixels. | | `rotation` | `number` | Rotation angle in degrees. | | `opacity` | `number` | Opacity level (0-1). | | `animations` | Array\<[`Animation`](./animation)> | Animation configurations. | | `blurEnabled` | `boolean` | Whether blur effect is enabled. | | `blurRadius` | `number` | Blur radius in pixels. | | `brightnessEnabled` | `boolean` | Whether brightness effect is enabled. | | `brightness` | `number` | Brightness level (-1 to 1). | | `sepiaEnabled` | `boolean` | Whether sepia effect is enabled. | | `grayscaleEnabled` | `boolean` | Whether grayscale effect is enabled. | | `filters` | `object` | Applied image filters keyed by filter name. | | `shadowEnabled` | `boolean` | Whether shadow is enabled. | | `shadowBlur` | `number` | Shadow blur radius in pixels. | | `shadowOffsetX` | `number` | Shadow horizontal offset in pixels. | | `shadowOffsetY` | `number` | Shadow vertical offset in pixels. | | `shadowColor` | `string` | Shadow color. | | `shadowOpacity` | `number` | Shadow opacity (0-1). | | `visible` | `boolean` | Whether the element is visible. | | `draggable` | `boolean` | Whether the element can be dragged. | | `resizable` | `boolean` | Whether the element can be resized. | | `selectable` | `boolean` | Whether the element can be selected. | | `removable` | `boolean` | Whether the element can be removed. | | `contentEditable` | `boolean` | Whether the element content can be edited. | | `styleEditable` | `boolean` | Whether the element style can be edited. | | `alwaysOnTop` | `boolean` | Whether the element stays on top of others. | | `showInExport` | `boolean` | Whether the element appears in exports. | | `subType` | `string` | Figure subtype: rect, ellipse, triangle, etc. | | `fill` | `string` | Fill color. | | `dash` | Array\<`number`> | Dash pattern array. | | `strokeWidth` | `number` | *No description provided* | | `stroke` | `string` | *No description provided* | | `cornerRadius` | `number` | *No description provided* | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the element." }, "type": { "type": "string", "const": "figure" }, "name": { "default": "", "description": "Display name for the element.", "type": "string" }, "custom": { "description": "Custom data attached to the element." }, "x": { "default": 0, "description": "X position in pixels.", "type": "number" }, "y": { "default": 0, "description": "Y position in pixels.", "type": "number" }, "width": { "default": 100, "description": "Width in pixels.", "type": "number" }, "height": { "default": 100, "description": "Height in pixels.", "type": "number" }, "rotation": { "default": 0, "description": "Rotation angle in degrees.", "type": "number" }, "opacity": { "default": 1, "description": "Opacity level (0-1).", "type": "number" }, "animations": { "default": [], "description": "Animation configurations.", "type": "array", "items": { "type": "object", "properties": { "delay": { "default": 0, "description": "Delay before the animation starts, in milliseconds.", "type": "number" }, "duration": { "default": 500, "description": "Animation duration in milliseconds.", "type": "number" }, "enabled": { "default": true, "description": "Whether the animation is enabled.", "type": "boolean" }, "type": { "type": "string", "enum": [ "enter", "exit", "loop" ], "description": "Animation phase." }, "name": { "default": "none", "description": "Animation name identifier.", "type": "string" }, "data": { "default": {}, "description": "Animation-specific data." } }, "required": [ "type" ] } }, "blurEnabled": { "default": false, "description": "Whether blur effect is enabled.", "type": "boolean" }, "blurRadius": { "default": 10, "description": "Blur radius in pixels.", "type": "number" }, "brightnessEnabled": { "default": false, "description": "Whether brightness effect is enabled.", "type": "boolean" }, "brightness": { "default": 0, "description": "Brightness level (-1 to 1).", "type": "number" }, "sepiaEnabled": { "default": false, "description": "Whether sepia effect is enabled.", "type": "boolean" }, "grayscaleEnabled": { "default": false, "description": "Whether grayscale effect is enabled.", "type": "boolean" }, "filters": { "default": {}, "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": { "type": "object", "properties": { "intensity": { "default": 1, "description": "Filter intensity (-1 to 1).", "type": "number" } } }, "description": "Applied image filters keyed by filter name." }, "shadowEnabled": { "default": false, "description": "Whether shadow is enabled.", "type": "boolean" }, "shadowBlur": { "default": 5, "description": "Shadow blur radius in pixels.", "type": "number" }, "shadowOffsetX": { "default": 0, "description": "Shadow horizontal offset in pixels.", "type": "number" }, "shadowOffsetY": { "default": 0, "description": "Shadow vertical offset in pixels.", "type": "number" }, "shadowColor": { "default": "black", "description": "Shadow color.", "type": "string" }, "shadowOpacity": { "default": 1, "description": "Shadow opacity (0-1).", "type": "number" }, "visible": { "default": true, "description": "Whether the element is visible.", "type": "boolean" }, "draggable": { "default": true, "description": "Whether the element can be dragged.", "type": "boolean" }, "resizable": { "default": true, "description": "Whether the element can be resized.", "type": "boolean" }, "selectable": { "default": true, "description": "Whether the element can be selected.", "type": "boolean" }, "removable": { "default": true, "description": "Whether the element can be removed.", "type": "boolean" }, "contentEditable": { "default": true, "description": "Whether the element content can be edited.", "type": "boolean" }, "styleEditable": { "default": true, "description": "Whether the element style can be edited.", "type": "boolean" }, "alwaysOnTop": { "default": false, "description": "Whether the element stays on top of others.", "type": "boolean" }, "showInExport": { "default": true, "description": "Whether the element appears in exports.", "type": "boolean" }, "subType": { "default": "rect", "description": "Figure subtype: rect, ellipse, triangle, etc.", "type": "string" }, "fill": { "default": "rgb(0, 161, 255)", "description": "Fill color.", "type": "string" }, "dash": { "default": [], "description": "Dash pattern array.", "type": "array", "items": { "type": "number" } }, "strokeWidth": { "default": 0, "type": "number" }, "stroke": { "default": "rgba(98, 197, 255, 1)", "type": "string" }, "cornerRadius": { "default": 0, "type": "number" } }, "required": [ "id", "type" ] } ``` ## Related Definitions [#related-definitions] * [Animation](./animation) --- # FontStyleVariant URL: https://polotno.com/docs/schema/font-style-variant ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ------------ | -------- | ----------------------------------------- | | `src` | `string` | Font source, typically in CSS url() form. | | `fontStyle` | `string` | *No description provided* | | `fontWeight` | `string` | *No description provided* | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "src": { "type": "string", "description": "Font source, typically in CSS url() form." }, "fontStyle": { "type": "string" }, "fontWeight": { "type": "string" } }, "required": [ "src" ] } ``` ## Related Definitions [#related-definitions] *No related definitions found.* --- # Font URL: https://polotno.com/docs/schema/font ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ------------ | -------------------------------------------------- | ----------------------------------------------------- | | `fontFamily` | `string` | Font family name. | | `url` | `string` | URL to load the font from (single-variant fonts). | | `styles` | Array\<[`FontStyleVariant`](./font-style-variant)> | Per-weight/style variants for multi-variant families. | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "fontFamily": { "type": "string", "description": "Font family name." }, "url": { "default": "", "description": "URL to load the font from (single-variant fonts).", "type": "string" }, "styles": { "description": "Per-weight/style variants for multi-variant families.", "type": "array", "items": { "type": "object", "properties": { "src": { "type": "string", "description": "Font source, typically in CSS url() form." }, "fontStyle": { "type": "string" }, "fontWeight": { "type": "string" } }, "required": [ "src" ] } } }, "required": [ "fontFamily" ] } ``` ## Related Definitions [#related-definitions] * [FontStyleVariant](./font-style-variant) --- # GifElement URL: https://polotno.com/docs/schema/gif-element ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ------------------- | ---------------------------------- | ------------------------------------------- | | `id` | `string` | Unique identifier for the element. | | `type` | `"gif"` | *No description provided* | | `name` | `string` | Display name for the element. | | `custom` | `unknown` | Custom data attached to the element. | | `x` | `number` | X position in pixels. | | `y` | `number` | Y position in pixels. | | `width` | `number` | Width in pixels. | | `height` | `number` | Height in pixels. | | `rotation` | `number` | Rotation angle in degrees. | | `opacity` | `number` | Opacity level (0-1). | | `animations` | Array\<[`Animation`](./animation)> | Animation configurations. | | `blurEnabled` | `boolean` | Whether blur effect is enabled. | | `blurRadius` | `number` | Blur radius in pixels. | | `brightnessEnabled` | `boolean` | Whether brightness effect is enabled. | | `brightness` | `number` | Brightness level (-1 to 1). | | `sepiaEnabled` | `boolean` | Whether sepia effect is enabled. | | `grayscaleEnabled` | `boolean` | Whether grayscale effect is enabled. | | `filters` | `object` | Applied image filters keyed by filter name. | | `shadowEnabled` | `boolean` | Whether shadow is enabled. | | `shadowBlur` | `number` | Shadow blur radius in pixels. | | `shadowOffsetX` | `number` | Shadow horizontal offset in pixels. | | `shadowOffsetY` | `number` | Shadow vertical offset in pixels. | | `shadowColor` | `string` | Shadow color. | | `shadowOpacity` | `number` | Shadow opacity (0-1). | | `visible` | `boolean` | Whether the element is visible. | | `draggable` | `boolean` | Whether the element can be dragged. | | `resizable` | `boolean` | Whether the element can be resized. | | `selectable` | `boolean` | Whether the element can be selected. | | `removable` | `boolean` | Whether the element can be removed. | | `contentEditable` | `boolean` | Whether the element content can be edited. | | `styleEditable` | `boolean` | Whether the element style can be edited. | | `alwaysOnTop` | `boolean` | Whether the element stays on top of others. | | `showInExport` | `boolean` | Whether the element appears in exports. | | `src` | `string` | URL or data URI of the image file. | | `cropX` | `number` | Crop X offset as a fraction (0-1). | | `cropY` | `number` | Crop Y offset as a fraction (0-1). | | `cropWidth` | `number` | Crop width as a fraction (0-1). | | `cropHeight` | `number` | Crop height as a fraction (0-1). | | `cornerRadius` | `number` | Corner radius in pixels. | | `flipX` | `boolean` | Whether the image is flipped horizontally. | | `flipY` | `boolean` | Whether the image is flipped vertically. | | `clipSrc` | `string` | URL or data URI for a clipping mask. | | `borderColor` | `string` | Border color. | | `borderSize` | `number` | Border width in pixels. | | `keepRatio` | `boolean` | *No description provided* | | `stretchEnabled` | `boolean` | Whether stretch mode is enabled. | | `duration` | `number` | GIF duration in milliseconds. | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the element." }, "type": { "type": "string", "const": "gif" }, "name": { "default": "", "description": "Display name for the element.", "type": "string" }, "custom": { "description": "Custom data attached to the element." }, "x": { "default": 0, "description": "X position in pixels.", "type": "number" }, "y": { "default": 0, "description": "Y position in pixels.", "type": "number" }, "width": { "default": 100, "description": "Width in pixels.", "type": "number" }, "height": { "default": 100, "description": "Height in pixels.", "type": "number" }, "rotation": { "default": 0, "description": "Rotation angle in degrees.", "type": "number" }, "opacity": { "default": 1, "description": "Opacity level (0-1).", "type": "number" }, "animations": { "default": [], "description": "Animation configurations.", "type": "array", "items": { "type": "object", "properties": { "delay": { "default": 0, "description": "Delay before the animation starts, in milliseconds.", "type": "number" }, "duration": { "default": 500, "description": "Animation duration in milliseconds.", "type": "number" }, "enabled": { "default": true, "description": "Whether the animation is enabled.", "type": "boolean" }, "type": { "type": "string", "enum": [ "enter", "exit", "loop" ], "description": "Animation phase." }, "name": { "default": "none", "description": "Animation name identifier.", "type": "string" }, "data": { "default": {}, "description": "Animation-specific data." } }, "required": [ "type" ] } }, "blurEnabled": { "default": false, "description": "Whether blur effect is enabled.", "type": "boolean" }, "blurRadius": { "default": 10, "description": "Blur radius in pixels.", "type": "number" }, "brightnessEnabled": { "default": false, "description": "Whether brightness effect is enabled.", "type": "boolean" }, "brightness": { "default": 0, "description": "Brightness level (-1 to 1).", "type": "number" }, "sepiaEnabled": { "default": false, "description": "Whether sepia effect is enabled.", "type": "boolean" }, "grayscaleEnabled": { "default": false, "description": "Whether grayscale effect is enabled.", "type": "boolean" }, "filters": { "default": {}, "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": { "type": "object", "properties": { "intensity": { "default": 1, "description": "Filter intensity (-1 to 1).", "type": "number" } } }, "description": "Applied image filters keyed by filter name." }, "shadowEnabled": { "default": false, "description": "Whether shadow is enabled.", "type": "boolean" }, "shadowBlur": { "default": 5, "description": "Shadow blur radius in pixels.", "type": "number" }, "shadowOffsetX": { "default": 0, "description": "Shadow horizontal offset in pixels.", "type": "number" }, "shadowOffsetY": { "default": 0, "description": "Shadow vertical offset in pixels.", "type": "number" }, "shadowColor": { "default": "black", "description": "Shadow color.", "type": "string" }, "shadowOpacity": { "default": 1, "description": "Shadow opacity (0-1).", "type": "number" }, "visible": { "default": true, "description": "Whether the element is visible.", "type": "boolean" }, "draggable": { "default": true, "description": "Whether the element can be dragged.", "type": "boolean" }, "resizable": { "default": true, "description": "Whether the element can be resized.", "type": "boolean" }, "selectable": { "default": true, "description": "Whether the element can be selected.", "type": "boolean" }, "removable": { "default": true, "description": "Whether the element can be removed.", "type": "boolean" }, "contentEditable": { "default": true, "description": "Whether the element content can be edited.", "type": "boolean" }, "styleEditable": { "default": true, "description": "Whether the element style can be edited.", "type": "boolean" }, "alwaysOnTop": { "default": false, "description": "Whether the element stays on top of others.", "type": "boolean" }, "showInExport": { "default": true, "description": "Whether the element appears in exports.", "type": "boolean" }, "src": { "default": "", "description": "URL or data URI of the image file.", "type": "string" }, "cropX": { "default": 0, "description": "Crop X offset as a fraction (0-1).", "type": "number" }, "cropY": { "default": 0, "description": "Crop Y offset as a fraction (0-1).", "type": "number" }, "cropWidth": { "default": 1, "description": "Crop width as a fraction (0-1).", "type": "number" }, "cropHeight": { "default": 1, "description": "Crop height as a fraction (0-1).", "type": "number" }, "cornerRadius": { "default": 0, "description": "Corner radius in pixels.", "type": "number" }, "flipX": { "default": false, "description": "Whether the image is flipped horizontally.", "type": "boolean" }, "flipY": { "default": false, "description": "Whether the image is flipped vertically.", "type": "boolean" }, "clipSrc": { "default": "", "description": "URL or data URI for a clipping mask.", "type": "string" }, "borderColor": { "default": "black", "description": "Border color.", "type": "string" }, "borderSize": { "default": 0, "description": "Border width in pixels.", "type": "number" }, "keepRatio": { "default": true, "type": "boolean" }, "stretchEnabled": { "default": false, "description": "Whether stretch mode is enabled.", "type": "boolean" }, "duration": { "default": 0, "description": "GIF duration in milliseconds.", "type": "number" } }, "required": [ "id", "type" ] } ``` ## Related Definitions [#related-definitions] * [Animation](./animation) --- # GroupElement URL: https://polotno.com/docs/schema/group-element ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | -------------- | ---------------------------------- | ----------------------------- | | `id` | `string` | *No description provided* | | `type` | `"group"` | *No description provided* | | `name` | `string` | *No description provided* | | `custom` | `unknown` | *No description provided* | | `opacity` | `number` | *No description provided* | | `visible` | `boolean` | *No description provided* | | `selectable` | `boolean` | *No description provided* | | `removable` | `boolean` | *No description provided* | | `alwaysOnTop` | `boolean` | *No description provided* | | `showInExport` | `boolean` | *No description provided* | | `animations` | Array\<[`Animation`](./animation)> | *No description provided* | | `children` | Array\<[`Element`](./element)> | Child elements in this group. | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "id": { "type": "string" }, "type": { "type": "string", "const": "group" }, "name": { "default": "", "type": "string" }, "custom": {}, "opacity": { "default": 1, "type": "number" }, "visible": { "default": true, "type": "boolean" }, "selectable": { "default": true, "type": "boolean" }, "removable": { "default": true, "type": "boolean" }, "alwaysOnTop": { "default": false, "type": "boolean" }, "showInExport": { "default": true, "type": "boolean" }, "animations": { "default": [], "type": "array", "items": { "type": "object", "properties": { "delay": { "default": 0, "description": "Delay before the animation starts, in milliseconds.", "type": "number" }, "duration": { "default": 500, "description": "Animation duration in milliseconds.", "type": "number" }, "enabled": { "default": true, "description": "Whether the animation is enabled.", "type": "boolean" }, "type": { "type": "string", "enum": [ "enter", "exit", "loop" ], "description": "Animation phase." }, "name": { "default": "none", "description": "Animation name identifier.", "type": "string" }, "data": { "default": {}, "description": "Animation-specific data." } }, "required": [ "type" ] } }, "children": { "default": [], "description": "Child elements in this group.", "type": "array", "items": { "$ref": "#/$defs/__schema0" } } }, "required": [ "id", "type" ] } ``` ## Related Definitions [#related-definitions] * [Animation](./animation) * [Element](./element) --- # ImageElement URL: https://polotno.com/docs/schema/image-element ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ------------------- | ---------------------------------- | ----------------------------------------------- | | `id` | `string` | Unique identifier for the element. | | `type` | `"image"` | *No description provided* | | `name` | `string` | Display name for the element. | | `custom` | `unknown` | Custom data attached to the element. | | `x` | `number` | X position in pixels. | | `y` | `number` | Y position in pixels. | | `width` | `number` | Width in pixels. | | `height` | `number` | Height in pixels. | | `rotation` | `number` | Rotation angle in degrees. | | `opacity` | `number` | Opacity level (0-1). | | `animations` | Array\<[`Animation`](./animation)> | Animation configurations. | | `blurEnabled` | `boolean` | Whether blur effect is enabled. | | `blurRadius` | `number` | Blur radius in pixels. | | `brightnessEnabled` | `boolean` | Whether brightness effect is enabled. | | `brightness` | `number` | Brightness level (-1 to 1). | | `sepiaEnabled` | `boolean` | Whether sepia effect is enabled. | | `grayscaleEnabled` | `boolean` | Whether grayscale effect is enabled. | | `filters` | `object` | Applied image filters keyed by filter name. | | `shadowEnabled` | `boolean` | Whether shadow is enabled. | | `shadowBlur` | `number` | Shadow blur radius in pixels. | | `shadowOffsetX` | `number` | Shadow horizontal offset in pixels. | | `shadowOffsetY` | `number` | Shadow vertical offset in pixels. | | `shadowColor` | `string` | Shadow color. | | `shadowOpacity` | `number` | Shadow opacity (0-1). | | `visible` | `boolean` | Whether the element is visible. | | `draggable` | `boolean` | Whether the element can be dragged. | | `resizable` | `boolean` | Whether the element can be resized. | | `selectable` | `boolean` | Whether the element can be selected. | | `removable` | `boolean` | Whether the element can be removed. | | `contentEditable` | `boolean` | Whether the element content can be edited. | | `styleEditable` | `boolean` | Whether the element style can be edited. | | `alwaysOnTop` | `boolean` | Whether the element stays on top of others. | | `showInExport` | `boolean` | Whether the element appears in exports. | | `src` | `string` | URL or data URI of the image file. | | `cropX` | `number` | Crop X offset as a fraction (0-1). | | `cropY` | `number` | Crop Y offset as a fraction (0-1). | | `cropWidth` | `number` | Crop width as a fraction (0-1). | | `cropHeight` | `number` | Crop height as a fraction (0-1). | | `cornerRadius` | `number` | Corner radius in pixels. | | `flipX` | `boolean` | Whether the image is flipped horizontally. | | `flipY` | `boolean` | Whether the image is flipped vertically. | | `clipSrc` | `string` | URL or data URI for a clipping mask. | | `borderColor` | `string` | Border color. | | `borderSize` | `number` | Border width in pixels. | | `keepRatio` | `boolean` | Whether to maintain aspect ratio when resizing. | | `stretchEnabled` | `boolean` | Whether stretch mode is enabled. | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the element." }, "type": { "type": "string", "const": "image" }, "name": { "default": "", "description": "Display name for the element.", "type": "string" }, "custom": { "description": "Custom data attached to the element." }, "x": { "default": 0, "description": "X position in pixels.", "type": "number" }, "y": { "default": 0, "description": "Y position in pixels.", "type": "number" }, "width": { "default": 100, "description": "Width in pixels.", "type": "number" }, "height": { "default": 100, "description": "Height in pixels.", "type": "number" }, "rotation": { "default": 0, "description": "Rotation angle in degrees.", "type": "number" }, "opacity": { "default": 1, "description": "Opacity level (0-1).", "type": "number" }, "animations": { "default": [], "description": "Animation configurations.", "type": "array", "items": { "type": "object", "properties": { "delay": { "default": 0, "description": "Delay before the animation starts, in milliseconds.", "type": "number" }, "duration": { "default": 500, "description": "Animation duration in milliseconds.", "type": "number" }, "enabled": { "default": true, "description": "Whether the animation is enabled.", "type": "boolean" }, "type": { "type": "string", "enum": [ "enter", "exit", "loop" ], "description": "Animation phase." }, "name": { "default": "none", "description": "Animation name identifier.", "type": "string" }, "data": { "default": {}, "description": "Animation-specific data." } }, "required": [ "type" ] } }, "blurEnabled": { "default": false, "description": "Whether blur effect is enabled.", "type": "boolean" }, "blurRadius": { "default": 10, "description": "Blur radius in pixels.", "type": "number" }, "brightnessEnabled": { "default": false, "description": "Whether brightness effect is enabled.", "type": "boolean" }, "brightness": { "default": 0, "description": "Brightness level (-1 to 1).", "type": "number" }, "sepiaEnabled": { "default": false, "description": "Whether sepia effect is enabled.", "type": "boolean" }, "grayscaleEnabled": { "default": false, "description": "Whether grayscale effect is enabled.", "type": "boolean" }, "filters": { "default": {}, "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": { "type": "object", "properties": { "intensity": { "default": 1, "description": "Filter intensity (-1 to 1).", "type": "number" } } }, "description": "Applied image filters keyed by filter name." }, "shadowEnabled": { "default": false, "description": "Whether shadow is enabled.", "type": "boolean" }, "shadowBlur": { "default": 5, "description": "Shadow blur radius in pixels.", "type": "number" }, "shadowOffsetX": { "default": 0, "description": "Shadow horizontal offset in pixels.", "type": "number" }, "shadowOffsetY": { "default": 0, "description": "Shadow vertical offset in pixels.", "type": "number" }, "shadowColor": { "default": "black", "description": "Shadow color.", "type": "string" }, "shadowOpacity": { "default": 1, "description": "Shadow opacity (0-1).", "type": "number" }, "visible": { "default": true, "description": "Whether the element is visible.", "type": "boolean" }, "draggable": { "default": true, "description": "Whether the element can be dragged.", "type": "boolean" }, "resizable": { "default": true, "description": "Whether the element can be resized.", "type": "boolean" }, "selectable": { "default": true, "description": "Whether the element can be selected.", "type": "boolean" }, "removable": { "default": true, "description": "Whether the element can be removed.", "type": "boolean" }, "contentEditable": { "default": true, "description": "Whether the element content can be edited.", "type": "boolean" }, "styleEditable": { "default": true, "description": "Whether the element style can be edited.", "type": "boolean" }, "alwaysOnTop": { "default": false, "description": "Whether the element stays on top of others.", "type": "boolean" }, "showInExport": { "default": true, "description": "Whether the element appears in exports.", "type": "boolean" }, "src": { "default": "", "description": "URL or data URI of the image file.", "type": "string" }, "cropX": { "default": 0, "description": "Crop X offset as a fraction (0-1).", "type": "number" }, "cropY": { "default": 0, "description": "Crop Y offset as a fraction (0-1).", "type": "number" }, "cropWidth": { "default": 1, "description": "Crop width as a fraction (0-1).", "type": "number" }, "cropHeight": { "default": 1, "description": "Crop height as a fraction (0-1).", "type": "number" }, "cornerRadius": { "default": 0, "description": "Corner radius in pixels.", "type": "number" }, "flipX": { "default": false, "description": "Whether the image is flipped horizontally.", "type": "boolean" }, "flipY": { "default": false, "description": "Whether the image is flipped vertically.", "type": "boolean" }, "clipSrc": { "default": "", "description": "URL or data URI for a clipping mask.", "type": "string" }, "borderColor": { "default": "black", "description": "Border color.", "type": "string" }, "borderSize": { "default": 0, "description": "Border width in pixels.", "type": "number" }, "keepRatio": { "default": false, "description": "Whether to maintain aspect ratio when resizing.", "type": "boolean" }, "stretchEnabled": { "default": false, "description": "Whether stretch mode is enabled.", "type": "boolean" } }, "required": [ "id", "type" ] } ``` ## Related Definitions [#related-definitions] * [Animation](./animation) --- # Design Format URL: https://polotno.com/docs/schema The **Design Format** is the JSON shape every Polotno design uses β€” the same object `store.toJSON()` produces and `store.loadJSON()` accepts. It's a flat, readable document: top-level `width` / `height` / `dpi`, a `fonts` array, and a `pages` array whose `children` are typed elements (text, image, svg, line, figure, gif, video, table, group). You can work with this format **without running the editor**. The [`@polotno/schema`](https://www.npmjs.com/package/@polotno/schema) package gives you TypeScript types, default values, validation, and a language-neutral JSON Schema β€” everything you need to generate, validate, or transform designs on a server, in an edge function, or inside an LLM pipeline. ```bash npm install @polotno/schema ``` ## What it's for [#what-its-for] * **Validate** a design before saving it, or when accepting it over the wire. * **Normalize** partial or hand-authored JSON into a complete, canonical document (defaults filled in). * **Constrain LLM output** β€” feed the JSON Schema to a model as a structured-output schema, then validate what comes back. ## Validate [#validate] ```ts import { validateDesign } from '@polotno/schema'; // json from store.toJSON(), a converter, or an LLM const result = validateDesign(json, { mode: 'canonical' }); if (!result.valid) { console.error(result.errors); // [{ path, message }, …] } ``` Use `mode: 'partial'` to allow missing defaults, or `mode: 'canonical'` to reject unknown keys. `assertValidDesign(json)` throws instead of returning a result. The exported `designSchemaLenient` / `designSchemaCanonical` implement the [Standard Schema](https://standardschema.dev) interface (zod, Valibot, Effect Schema, …). ## Normalize (fill defaults) [#normalize-fill-defaults] ```ts import { normalizeDesign } from '@polotno/schema'; const complete = normalizeDesign(partial); // fills defaults, strips unknown keys store.loadJSON(complete); ``` Normalize is idempotent for current-format input. It does **not** migrate old `schemaVersion` documents, and it leaves render-derived values (such as text auto-height) for the editor to compute. ## Types [#types] ```ts import type { Design } from '@polotno/schema'; const design: Design = store.toJSON(); ``` Also exported: `Page`, `Element`, `TextElement`, and every other type in the format. ## JSON Schema & LLMs [#json-schema--llms] A language-neutral JSON Schema (draft 2020-12) ships with the package β€” validate from any language, or hand it to a model as a structured-output schema and validate what comes back before rendering: ```ts import schema from '@polotno/schema/design.schema.json' with { type: 'json' }; import { validateDesign } from '@polotno/schema'; // `schema` as your LLM client's structured-output / response schema const design = await model.generate({ prompt, jsonSchema: schema }); if (validateDesign(design, { mode: 'partial' }).valid) { store.loadJSON(design); } ``` ## Field reference [#field-reference] Every type in the format has a generated reference page with all properties, types, and defaults: * [Design](/docs/schema/store) β€” the root document * [Page](/docs/schema/page) and its [Element](/docs/schema/element) children: [Text](/docs/schema/text-element), [Image](/docs/schema/image-element), [SVG](/docs/schema/svg-element), [Line](/docs/schema/line-element), [Figure](/docs/schema/figure-element), [Gif](/docs/schema/gif-element), [Video](/docs/schema/video-element), [Table](/docs/schema/table-element), [Group](/docs/schema/group-element) * [Font](/docs/schema/font) and [Audio](/docs/schema/audio) Browse the full list in the sidebar. For the complete package API, see the [`@polotno/schema` README](https://www.npmjs.com/package/@polotno/schema) on npm. `@polotno/schema` is part of the Polotno SDK. Production use requires a valid Polotno [subscription](https://polotno.com/legal/license). The format is at `schemaVersion` 4 and still pre-1.0 β€” expect additive changes. --- # LineElement URL: https://polotno.com/docs/schema/line-element ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ------------------- | ---------------------------------- | ---------------------------------------------------- | | `id` | `string` | Unique identifier for the element. | | `type` | `"line"` | *No description provided* | | `name` | `string` | Display name for the element. | | `custom` | `unknown` | Custom data attached to the element. | | `x` | `number` | X position in pixels. | | `y` | `number` | Y position in pixels. | | `width` | `number` | *No description provided* | | `height` | `number` | *No description provided* | | `rotation` | `number` | Rotation angle in degrees. | | `opacity` | `number` | Opacity level (0-1). | | `animations` | Array\<[`Animation`](./animation)> | Animation configurations. | | `blurEnabled` | `boolean` | Whether blur effect is enabled. | | `blurRadius` | `number` | Blur radius in pixels. | | `brightnessEnabled` | `boolean` | Whether brightness effect is enabled. | | `brightness` | `number` | Brightness level (-1 to 1). | | `sepiaEnabled` | `boolean` | Whether sepia effect is enabled. | | `grayscaleEnabled` | `boolean` | Whether grayscale effect is enabled. | | `filters` | `object` | Applied image filters keyed by filter name. | | `shadowEnabled` | `boolean` | Whether shadow is enabled. | | `shadowBlur` | `number` | Shadow blur radius in pixels. | | `shadowOffsetX` | `number` | Shadow horizontal offset in pixels. | | `shadowOffsetY` | `number` | Shadow vertical offset in pixels. | | `shadowColor` | `string` | Shadow color. | | `shadowOpacity` | `number` | Shadow opacity (0-1). | | `visible` | `boolean` | Whether the element is visible. | | `draggable` | `boolean` | Whether the element can be dragged. | | `resizable` | `boolean` | Whether the element can be resized. | | `selectable` | `boolean` | Whether the element can be selected. | | `removable` | `boolean` | Whether the element can be removed. | | `contentEditable` | `boolean` | Whether the element content can be edited. | | `styleEditable` | `boolean` | Whether the element style can be edited. | | `alwaysOnTop` | `boolean` | Whether the element stays on top of others. | | `showInExport` | `boolean` | Whether the element appears in exports. | | `color` | `string` | Line color. | | `dash` | Array\<`number`> | Dash pattern array. | | `startHead` | `string` | Start arrow head: empty, arrow, circle, square, bar. | | `endHead` | `string` | End arrow head: empty, arrow, circle, square, bar. | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the element." }, "type": { "type": "string", "const": "line" }, "name": { "default": "", "description": "Display name for the element.", "type": "string" }, "custom": { "description": "Custom data attached to the element." }, "x": { "default": 0, "description": "X position in pixels.", "type": "number" }, "y": { "default": 0, "description": "Y position in pixels.", "type": "number" }, "width": { "default": 400, "type": "number" }, "height": { "default": 10, "type": "number" }, "rotation": { "default": 0, "description": "Rotation angle in degrees.", "type": "number" }, "opacity": { "default": 1, "description": "Opacity level (0-1).", "type": "number" }, "animations": { "default": [], "description": "Animation configurations.", "type": "array", "items": { "type": "object", "properties": { "delay": { "default": 0, "description": "Delay before the animation starts, in milliseconds.", "type": "number" }, "duration": { "default": 500, "description": "Animation duration in milliseconds.", "type": "number" }, "enabled": { "default": true, "description": "Whether the animation is enabled.", "type": "boolean" }, "type": { "type": "string", "enum": [ "enter", "exit", "loop" ], "description": "Animation phase." }, "name": { "default": "none", "description": "Animation name identifier.", "type": "string" }, "data": { "default": {}, "description": "Animation-specific data." } }, "required": [ "type" ] } }, "blurEnabled": { "default": false, "description": "Whether blur effect is enabled.", "type": "boolean" }, "blurRadius": { "default": 10, "description": "Blur radius in pixels.", "type": "number" }, "brightnessEnabled": { "default": false, "description": "Whether brightness effect is enabled.", "type": "boolean" }, "brightness": { "default": 0, "description": "Brightness level (-1 to 1).", "type": "number" }, "sepiaEnabled": { "default": false, "description": "Whether sepia effect is enabled.", "type": "boolean" }, "grayscaleEnabled": { "default": false, "description": "Whether grayscale effect is enabled.", "type": "boolean" }, "filters": { "default": {}, "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": { "type": "object", "properties": { "intensity": { "default": 1, "description": "Filter intensity (-1 to 1).", "type": "number" } } }, "description": "Applied image filters keyed by filter name." }, "shadowEnabled": { "default": false, "description": "Whether shadow is enabled.", "type": "boolean" }, "shadowBlur": { "default": 5, "description": "Shadow blur radius in pixels.", "type": "number" }, "shadowOffsetX": { "default": 0, "description": "Shadow horizontal offset in pixels.", "type": "number" }, "shadowOffsetY": { "default": 0, "description": "Shadow vertical offset in pixels.", "type": "number" }, "shadowColor": { "default": "black", "description": "Shadow color.", "type": "string" }, "shadowOpacity": { "default": 1, "description": "Shadow opacity (0-1).", "type": "number" }, "visible": { "default": true, "description": "Whether the element is visible.", "type": "boolean" }, "draggable": { "default": true, "description": "Whether the element can be dragged.", "type": "boolean" }, "resizable": { "default": true, "description": "Whether the element can be resized.", "type": "boolean" }, "selectable": { "default": true, "description": "Whether the element can be selected.", "type": "boolean" }, "removable": { "default": true, "description": "Whether the element can be removed.", "type": "boolean" }, "contentEditable": { "default": true, "description": "Whether the element content can be edited.", "type": "boolean" }, "styleEditable": { "default": true, "description": "Whether the element style can be edited.", "type": "boolean" }, "alwaysOnTop": { "default": false, "description": "Whether the element stays on top of others.", "type": "boolean" }, "showInExport": { "default": true, "description": "Whether the element appears in exports.", "type": "boolean" }, "color": { "default": "black", "description": "Line color.", "type": "string" }, "dash": { "default": [], "description": "Dash pattern array.", "type": "array", "items": { "type": "number" } }, "startHead": { "default": "", "description": "Start arrow head: empty, arrow, circle, square, bar.", "type": "string" }, "endHead": { "default": "", "description": "End arrow head: empty, arrow, circle, square, bar.", "type": "string" } }, "required": [ "id", "type" ] } ``` ## Related Definitions [#related-definitions] * [Animation](./animation) --- # Page URL: https://polotno.com/docs/schema/page ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ------------- | ------------------------------ | ------------------------------------------------------------------------------ | | `id` | `string` | Unique identifier for the page. | | `width` | `number` \| `"auto"` | Page width in pixels or 'auto'. | | `height` | `number` \| `"auto"` | Page height in pixels or 'auto'. | | `background` | `string` | Background color or image. | | `bleed` | `number` | Bleed area in pixels for print (all sides; base value for per-side overrides). | | `bleedTop` | `number` | Per-side bleed override (top), in pixels. Inherits `bleed` when omitted. | | `bleedRight` | `number` | Per-side bleed override (right), in pixels. Inherits `bleed` when omitted. | | `bleedBottom` | `number` | Per-side bleed override (bottom), in pixels. Inherits `bleed` when omitted. | | `bleedLeft` | `number` | Per-side bleed override (left), in pixels. Inherits `bleed` when omitted. | | `custom` | `unknown` | *No description provided* | | `duration` | `number` | Page duration in milliseconds. | | `children` | Array\<[`Element`](./element)> | Elements on this page. | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the page." }, "width": { "default": "auto", "description": "Page width in pixels or 'auto'.", "anyOf": [ { "type": "number" }, { "type": "string", "const": "auto" } ] }, "height": { "default": "auto", "description": "Page height in pixels or 'auto'.", "anyOf": [ { "type": "number" }, { "type": "string", "const": "auto" } ] }, "background": { "default": "white", "description": "Background color or image.", "type": "string" }, "bleed": { "default": 0, "description": "Bleed area in pixels for print (all sides; base value for per-side overrides).", "type": "number" }, "bleedTop": { "description": "Per-side bleed override (top), in pixels. Inherits `bleed` when omitted.", "type": "number" }, "bleedRight": { "description": "Per-side bleed override (right), in pixels. Inherits `bleed` when omitted.", "type": "number" }, "bleedBottom": { "description": "Per-side bleed override (bottom), in pixels. Inherits `bleed` when omitted.", "type": "number" }, "bleedLeft": { "description": "Per-side bleed override (left), in pixels. Inherits `bleed` when omitted.", "type": "number" }, "custom": {}, "duration": { "default": 5000, "description": "Page duration in milliseconds.", "type": "number" }, "children": { "default": [], "description": "Elements on this page.", "type": "array", "items": { "$ref": "#/$defs/__schema0" } } }, "required": [ "id" ] } ``` ## Related Definitions [#related-definitions] * [Element](./element) --- # Design URL: https://polotno.com/docs/schema/store ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | --------------- | ---------------------------------------------- | --------------------------------------------- | | `schemaVersion` | `number` | Schema version for compatibility / migration. | | `width` | `number` | Canvas width in pixels. | | `height` | `number` | Canvas height in pixels. | | `unit` | `"px"` \| `"pt"` \| `"mm"` \| `"cm"` \| `"in"` | Unit type for print measurements. | | `dpi` | `number` | Dots per inch for print calculations. | | `custom` | `unknown` | Custom data attached to the design. | | `fonts` | Array\<[`Font`](./font)> | Fonts used in the design. | | `pages` | Array\<[`Page`](./page)> | Pages in the design. | | `audios` | Array\<[`Audio`](./audio)> | Audio tracks in the design. | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "schemaVersion": { "default": 4, "description": "Schema version for compatibility / migration.", "type": "number" }, "width": { "default": 1080, "description": "Canvas width in pixels.", "type": "number" }, "height": { "default": 1080, "description": "Canvas height in pixels.", "type": "number" }, "unit": { "default": "px", "description": "Unit type for print measurements.", "type": "string", "enum": [ "px", "pt", "mm", "cm", "in" ] }, "dpi": { "default": 72, "description": "Dots per inch for print calculations.", "type": "number" }, "custom": { "description": "Custom data attached to the design." }, "fonts": { "default": [], "description": "Fonts used in the design.", "type": "array", "items": { "type": "object", "properties": { "fontFamily": { "type": "string", "description": "Font family name." }, "url": { "default": "", "description": "URL to load the font from (single-variant fonts).", "type": "string" }, "styles": { "description": "Per-weight/style variants for multi-variant families.", "type": "array", "items": { "type": "object", "properties": { "src": { "type": "string", "description": "Font source, typically in CSS url() form." }, "fontStyle": { "type": "string" }, "fontWeight": { "type": "string" } }, "required": [ "src" ] } } }, "required": [ "fontFamily" ] } }, "pages": { "default": [], "description": "Pages in the design.", "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the page." }, "width": { "default": "auto", "description": "Page width in pixels or 'auto'.", "anyOf": [ { "type": "number" }, { "type": "string", "const": "auto" } ] }, "height": { "default": "auto", "description": "Page height in pixels or 'auto'.", "anyOf": [ { "type": "number" }, { "type": "string", "const": "auto" } ] }, "background": { "default": "white", "description": "Background color or image.", "type": "string" }, "bleed": { "default": 0, "description": "Bleed area in pixels for print (all sides; base value for per-side overrides).", "type": "number" }, "bleedTop": { "description": "Per-side bleed override (top), in pixels. Inherits `bleed` when omitted.", "type": "number" }, "bleedRight": { "description": "Per-side bleed override (right), in pixels. Inherits `bleed` when omitted.", "type": "number" }, "bleedBottom": { "description": "Per-side bleed override (bottom), in pixels. Inherits `bleed` when omitted.", "type": "number" }, "bleedLeft": { "description": "Per-side bleed override (left), in pixels. Inherits `bleed` when omitted.", "type": "number" }, "custom": {}, "duration": { "default": 5000, "description": "Page duration in milliseconds.", "type": "number" }, "children": { "default": [], "description": "Elements on this page.", "type": "array", "items": { "$ref": "#/$defs/__schema0" } } }, "required": [ "id" ] } }, "audios": { "default": [], "description": "Audio tracks in the design.", "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the audio track." }, "src": { "default": "", "description": "URL or data URI of the audio file.", "type": "string" }, "duration": { "default": 0, "description": "Duration in milliseconds.", "type": "number" }, "startTime": { "default": 0, "description": "Start offset (0-1).", "type": "number" }, "endTime": { "default": 1, "description": "End offset (0-1).", "type": "number" }, "volume": { "default": 1, "description": "Volume level (0-1).", "type": "number" }, "delay": { "default": 0, "description": "Delay before playing, in milliseconds.", "type": "number" }, "speed": { "default": 1, "description": "Playback speed multiplier (1 = normal).", "type": "number" }, "custom": {} }, "required": [ "id" ] } } } } ``` ## Related Definitions [#related-definitions] * [Font](./font) * [Page](./page) * [Audio](./audio) * [FontStyleVariant](./font-style-variant) * [Element](./element) * [Animation](./animation) * [TableCell](./table-cell) * [CellBorders](./cell-borders) * [BorderSide](./border-side) --- # SVGElement URL: https://polotno.com/docs/schema/svg-element ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ------------------- | ---------------------------------- | ---------------------------------------------------- | | `id` | `string` | Unique identifier for the element. | | `type` | `"svg"` | *No description provided* | | `name` | `string` | Display name for the element. | | `custom` | `unknown` | Custom data attached to the element. | | `x` | `number` | X position in pixels. | | `y` | `number` | Y position in pixels. | | `width` | `number` | *No description provided* | | `height` | `number` | *No description provided* | | `rotation` | `number` | Rotation angle in degrees. | | `opacity` | `number` | Opacity level (0-1). | | `animations` | Array\<[`Animation`](./animation)> | Animation configurations. | | `blurEnabled` | `boolean` | Whether blur effect is enabled. | | `blurRadius` | `number` | Blur radius in pixels. | | `brightnessEnabled` | `boolean` | Whether brightness effect is enabled. | | `brightness` | `number` | Brightness level (-1 to 1). | | `sepiaEnabled` | `boolean` | Whether sepia effect is enabled. | | `grayscaleEnabled` | `boolean` | Whether grayscale effect is enabled. | | `filters` | `object` | Applied image filters keyed by filter name. | | `shadowEnabled` | `boolean` | Whether shadow is enabled. | | `shadowBlur` | `number` | Shadow blur radius in pixels. | | `shadowOffsetX` | `number` | Shadow horizontal offset in pixels. | | `shadowOffsetY` | `number` | Shadow vertical offset in pixels. | | `shadowColor` | `string` | Shadow color. | | `shadowOpacity` | `number` | Shadow opacity (0-1). | | `visible` | `boolean` | Whether the element is visible. | | `draggable` | `boolean` | Whether the element can be dragged. | | `resizable` | `boolean` | Whether the element can be resized. | | `selectable` | `boolean` | Whether the element can be selected. | | `removable` | `boolean` | Whether the element can be removed. | | `contentEditable` | `boolean` | Whether the element content can be edited. | | `styleEditable` | `boolean` | Whether the element style can be edited. | | `alwaysOnTop` | `boolean` | Whether the element stays on top of others. | | `showInExport` | `boolean` | Whether the element appears in exports. | | `src` | `string` | URL or data URI of the SVG. | | `maskSrc` | `string` | URL or data URI for an SVG mask. | | `cropX` | `number` | *No description provided* | | `cropY` | `number` | *No description provided* | | `cropWidth` | `number` | *No description provided* | | `cropHeight` | `number` | *No description provided* | | `keepRatio` | `boolean` | *No description provided* | | `stretchEnabled` | `boolean` | *No description provided* | | `flipX` | `boolean` | *No description provided* | | `flipY` | `boolean` | *No description provided* | | `borderColor` | `string` | *No description provided* | | `borderSize` | `number` | *No description provided* | | `cornerRadius` | `number` | *No description provided* | | `colorsReplace` | `object` | Color replacement mappings (original β†’ replacement). | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the element." }, "type": { "type": "string", "const": "svg" }, "name": { "default": "", "description": "Display name for the element.", "type": "string" }, "custom": { "description": "Custom data attached to the element." }, "x": { "default": 0, "description": "X position in pixels.", "type": "number" }, "y": { "default": 0, "description": "Y position in pixels.", "type": "number" }, "width": { "default": 100, "type": "number" }, "height": { "default": 100, "type": "number" }, "rotation": { "default": 0, "description": "Rotation angle in degrees.", "type": "number" }, "opacity": { "default": 1, "description": "Opacity level (0-1).", "type": "number" }, "animations": { "default": [], "description": "Animation configurations.", "type": "array", "items": { "type": "object", "properties": { "delay": { "default": 0, "description": "Delay before the animation starts, in milliseconds.", "type": "number" }, "duration": { "default": 500, "description": "Animation duration in milliseconds.", "type": "number" }, "enabled": { "default": true, "description": "Whether the animation is enabled.", "type": "boolean" }, "type": { "type": "string", "enum": [ "enter", "exit", "loop" ], "description": "Animation phase." }, "name": { "default": "none", "description": "Animation name identifier.", "type": "string" }, "data": { "default": {}, "description": "Animation-specific data." } }, "required": [ "type" ] } }, "blurEnabled": { "default": false, "description": "Whether blur effect is enabled.", "type": "boolean" }, "blurRadius": { "default": 10, "description": "Blur radius in pixels.", "type": "number" }, "brightnessEnabled": { "default": false, "description": "Whether brightness effect is enabled.", "type": "boolean" }, "brightness": { "default": 0, "description": "Brightness level (-1 to 1).", "type": "number" }, "sepiaEnabled": { "default": false, "description": "Whether sepia effect is enabled.", "type": "boolean" }, "grayscaleEnabled": { "default": false, "description": "Whether grayscale effect is enabled.", "type": "boolean" }, "filters": { "default": {}, "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": { "type": "object", "properties": { "intensity": { "default": 1, "description": "Filter intensity (-1 to 1).", "type": "number" } } }, "description": "Applied image filters keyed by filter name." }, "shadowEnabled": { "default": false, "description": "Whether shadow is enabled.", "type": "boolean" }, "shadowBlur": { "default": 5, "description": "Shadow blur radius in pixels.", "type": "number" }, "shadowOffsetX": { "default": 0, "description": "Shadow horizontal offset in pixels.", "type": "number" }, "shadowOffsetY": { "default": 0, "description": "Shadow vertical offset in pixels.", "type": "number" }, "shadowColor": { "default": "black", "description": "Shadow color.", "type": "string" }, "shadowOpacity": { "default": 1, "description": "Shadow opacity (0-1).", "type": "number" }, "visible": { "default": true, "description": "Whether the element is visible.", "type": "boolean" }, "draggable": { "default": true, "description": "Whether the element can be dragged.", "type": "boolean" }, "resizable": { "default": true, "description": "Whether the element can be resized.", "type": "boolean" }, "selectable": { "default": true, "description": "Whether the element can be selected.", "type": "boolean" }, "removable": { "default": true, "description": "Whether the element can be removed.", "type": "boolean" }, "contentEditable": { "default": true, "description": "Whether the element content can be edited.", "type": "boolean" }, "styleEditable": { "default": true, "description": "Whether the element style can be edited.", "type": "boolean" }, "alwaysOnTop": { "default": false, "description": "Whether the element stays on top of others.", "type": "boolean" }, "showInExport": { "default": true, "description": "Whether the element appears in exports.", "type": "boolean" }, "src": { "default": "", "description": "URL or data URI of the SVG.", "type": "string" }, "maskSrc": { "default": "", "description": "URL or data URI for an SVG mask.", "type": "string" }, "cropX": { "default": 0, "type": "number" }, "cropY": { "default": 0, "type": "number" }, "cropWidth": { "default": 1, "type": "number" }, "cropHeight": { "default": 1, "type": "number" }, "keepRatio": { "default": true, "type": "boolean" }, "stretchEnabled": { "default": true, "type": "boolean" }, "flipX": { "default": false, "type": "boolean" }, "flipY": { "default": false, "type": "boolean" }, "borderColor": { "default": "black", "type": "string" }, "borderSize": { "default": 0, "type": "number" }, "cornerRadius": { "default": 0, "type": "number" }, "colorsReplace": { "default": {}, "description": "Color replacement mappings (original β†’ replacement).", "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": { "type": "string" } } }, "required": [ "id", "type" ] } ``` ## Related Definitions [#related-definitions] * [Animation](./animation) --- # TableCell URL: https://polotno.com/docs/schema/table-cell ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ---------------- | ------------------------------- | ---------------------------------------------------------------------------------------------- | | `id` | `string` | Unique identifier for the cell. | | `type` | `"tablecell"` | Cell type identifier. | | `text` | `string` | Visible text shown in the cell. | | `opacity` | `number` | Opacity level (0-1). | | `rowSpan` | `number` | Number of rows this cell spans. | | `colSpan` | `number` | Number of columns this cell spans. | | `mergedInto` | `string` | Id of the cell this one is merged into, if any. | | `name` | `string` | Display name for the cell. | | `custom` | `unknown` | Custom data attached to the cell. | | `borders` | [`CellBorders`](./cell-borders) | Per-side border overrides for this cell. | | `fontSize` | `number` | Font size in pixels (inherits from the table when omitted). | | `fontFamily` | `string` | Font family name (inherits from the table when omitted). | | `fontWeight` | `string` | Font weight: 'normal', 'bold', or numeric string (inherits from the table when omitted). | | `fontStyle` | `string` | Font style: 'normal' or 'italic' (inherits from the table when omitted). | | `textDecoration` | `string` | Text decoration, e.g. underline, line-through (inherits from the table when omitted). | | `textTransform` | `string` | Text transform: uppercase, lowercase, capitalize, none (inherits from the table when omitted). | | `fill` | `string` | Text color (inherits from the table when omitted). | | `align` | `string` | Horizontal alignment: left, center, right, justify (inherits from the table when omitted). | | `verticalAlign` | `string` | Vertical alignment: top, middle, bottom (inherits from the table when omitted). | | `lineHeight` | `number` \| `string` | Line height multiplier or 'auto' (inherits from the table when omitted). | | `letterSpacing` | `number` | Letter spacing as a fraction of font size (inherits from the table when omitted). | | `strokeWidth` | `number` | Stroke width in pixels (inherits from the table when omitted). | | `stroke` | `string` | Stroke color (inherits from the table when omitted). | | `cellBackground` | `string` | Cell background color (inherits from the table when omitted). | | `cellPadding` | `number` | Cell padding in pixels (inherits from the table when omitted). | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the cell." }, "type": { "default": "tablecell", "description": "Cell type identifier.", "type": "string", "const": "tablecell" }, "text": { "default": "", "description": "Visible text shown in the cell.", "type": "string" }, "opacity": { "description": "Opacity level (0-1).", "type": "number" }, "rowSpan": { "description": "Number of rows this cell spans.", "type": "number" }, "colSpan": { "description": "Number of columns this cell spans.", "type": "number" }, "mergedInto": { "description": "Id of the cell this one is merged into, if any.", "type": "string" }, "name": { "description": "Display name for the cell.", "type": "string" }, "custom": { "description": "Custom data attached to the cell." }, "borders": { "description": "Per-side border overrides for this cell.", "type": "object", "properties": { "top": { "description": "Top border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } }, "right": { "description": "Right border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } }, "bottom": { "description": "Bottom border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } }, "left": { "description": "Left border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } } } }, "fontSize": { "description": "Font size in pixels (inherits from the table when omitted).", "type": "number" }, "fontFamily": { "description": "Font family name (inherits from the table when omitted).", "type": "string" }, "fontWeight": { "description": "Font weight: 'normal', 'bold', or numeric string (inherits from the table when omitted).", "type": "string" }, "fontStyle": { "description": "Font style: 'normal' or 'italic' (inherits from the table when omitted).", "type": "string" }, "textDecoration": { "description": "Text decoration, e.g. underline, line-through (inherits from the table when omitted).", "type": "string" }, "textTransform": { "description": "Text transform: uppercase, lowercase, capitalize, none (inherits from the table when omitted).", "type": "string" }, "fill": { "description": "Text color (inherits from the table when omitted).", "type": "string" }, "align": { "description": "Horizontal alignment: left, center, right, justify (inherits from the table when omitted).", "type": "string" }, "verticalAlign": { "description": "Vertical alignment: top, middle, bottom (inherits from the table when omitted).", "type": "string" }, "lineHeight": { "description": "Line height multiplier or 'auto' (inherits from the table when omitted).", "anyOf": [ { "type": "number" }, { "type": "string" } ] }, "letterSpacing": { "description": "Letter spacing as a fraction of font size (inherits from the table when omitted).", "type": "number" }, "strokeWidth": { "description": "Stroke width in pixels (inherits from the table when omitted).", "type": "number" }, "stroke": { "description": "Stroke color (inherits from the table when omitted).", "type": "string" }, "cellBackground": { "description": "Cell background color (inherits from the table when omitted).", "type": "string" }, "cellPadding": { "description": "Cell padding in pixels (inherits from the table when omitted).", "type": "number" } }, "required": [ "id" ] } ``` ## Related Definitions [#related-definitions] * [CellBorders](./cell-borders) * [BorderSide](./border-side) --- # TableElement URL: https://polotno.com/docs/schema/table-element ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ----------------- | ------------------------------------------------- | --------------------------------------------------------------- | | `id` | `string` | Unique identifier for the element. | | `type` | `"table"` | Element type identifier. | | `name` | `string` | Display name for the element. | | `custom` | `unknown` | Custom data attached to the element. | | `x` | `number` | X position in pixels. | | `y` | `number` | Y position in pixels. | | `width` | `number` | Width in pixels. | | `height` | `number` | Height in pixels. | | `rotation` | `number` | Rotation angle in degrees. | | `opacity` | `number` | Opacity level (0-1). | | `animations` | Array\<[`Animation`](./animation)> | Animation configurations. | | `visible` | `boolean` | Whether the element is visible. | | `draggable` | `boolean` | Whether the element can be dragged. | | `resizable` | `boolean` | Whether the element can be resized. | | `selectable` | `boolean` | Whether the element can be selected. | | `removable` | `boolean` | Whether the element can be removed. | | `contentEditable` | `boolean` | Whether the element content can be edited. | | `styleEditable` | `boolean` | Whether the element style can be edited. | | `alwaysOnTop` | `boolean` | Whether the element stays on top of others. | | `showInExport` | `boolean` | Whether the element appears in exports. | | `rows` | `number` | Number of rows. | | `cols` | `number` | Number of columns. | | `colWidths` | Array\<`number`> | Column width fractions (sum to 1). | | `rowHeights` | Array\<`number`> | Row height fractions (sum to 1). | | `cells` | Array\<[`TableCell`](./table-cell)> | Flat cell array in row-major order. | | `borderColor` | `string` | Default border color for the table grid. | | `borderWidth` | `number` | Default border width in pixels. | | `borderStyle` | `"solid"` \| `"dashed"` \| `"dotted"` \| `"none"` | Default border style. | | `fontSize` | `number` | Default font size in pixels for cells. | | `fontFamily` | `string` | Default font family name for cells. | | `fontWeight` | `string` | Default font weight: 'normal', 'bold', or numeric string. | | `fontStyle` | `string` | Default font style: 'normal' or 'italic'. | | `textDecoration` | `string` | Default text decoration (e.g. underline, line-through). | | `textTransform` | `string` | Default text transform: uppercase, lowercase, capitalize, none. | | `fill` | `string` | Default text color for cells. | | `align` | `string` | Default horizontal alignment: left, center, right, justify. | | `verticalAlign` | `string` | Default vertical alignment: top, middle, bottom. | | `lineHeight` | `number` \| `string` | Default line height multiplier or 'auto'. | | `letterSpacing` | `number` | Default letter spacing as a fraction of font size. | | `strokeWidth` | `number` | Default stroke width in pixels. | | `stroke` | `string` | Default stroke color. | | `cellBackground` | `string` | Default cell background color. | | `cellPadding` | `number` | Default cell padding in pixels. | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the element." }, "type": { "type": "string", "const": "table", "description": "Element type identifier." }, "name": { "default": "", "description": "Display name for the element.", "type": "string" }, "custom": { "description": "Custom data attached to the element." }, "x": { "default": 0, "description": "X position in pixels.", "type": "number" }, "y": { "default": 0, "description": "Y position in pixels.", "type": "number" }, "width": { "default": 300, "description": "Width in pixels.", "type": "number" }, "height": { "default": 200, "description": "Height in pixels.", "type": "number" }, "rotation": { "default": 0, "description": "Rotation angle in degrees.", "type": "number" }, "opacity": { "default": 1, "description": "Opacity level (0-1).", "type": "number" }, "animations": { "default": [], "description": "Animation configurations.", "type": "array", "items": { "type": "object", "properties": { "delay": { "default": 0, "description": "Delay before the animation starts, in milliseconds.", "type": "number" }, "duration": { "default": 500, "description": "Animation duration in milliseconds.", "type": "number" }, "enabled": { "default": true, "description": "Whether the animation is enabled.", "type": "boolean" }, "type": { "type": "string", "enum": [ "enter", "exit", "loop" ], "description": "Animation phase." }, "name": { "default": "none", "description": "Animation name identifier.", "type": "string" }, "data": { "default": {}, "description": "Animation-specific data." } }, "required": [ "type" ] } }, "visible": { "default": true, "description": "Whether the element is visible.", "type": "boolean" }, "draggable": { "default": true, "description": "Whether the element can be dragged.", "type": "boolean" }, "resizable": { "default": true, "description": "Whether the element can be resized.", "type": "boolean" }, "selectable": { "default": true, "description": "Whether the element can be selected.", "type": "boolean" }, "removable": { "default": true, "description": "Whether the element can be removed.", "type": "boolean" }, "contentEditable": { "default": true, "description": "Whether the element content can be edited.", "type": "boolean" }, "styleEditable": { "default": true, "description": "Whether the element style can be edited.", "type": "boolean" }, "alwaysOnTop": { "default": false, "description": "Whether the element stays on top of others.", "type": "boolean" }, "showInExport": { "default": true, "description": "Whether the element appears in exports.", "type": "boolean" }, "rows": { "default": 3, "description": "Number of rows.", "type": "number" }, "cols": { "default": 3, "description": "Number of columns.", "type": "number" }, "colWidths": { "default": [], "description": "Column width fractions (sum to 1).", "type": "array", "items": { "type": "number" } }, "rowHeights": { "default": [], "description": "Row height fractions (sum to 1).", "type": "array", "items": { "type": "number" } }, "cells": { "default": [], "description": "Flat cell array in row-major order.", "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the cell." }, "type": { "default": "tablecell", "description": "Cell type identifier.", "type": "string", "const": "tablecell" }, "text": { "default": "", "description": "Visible text shown in the cell.", "type": "string" }, "opacity": { "description": "Opacity level (0-1).", "type": "number" }, "rowSpan": { "description": "Number of rows this cell spans.", "type": "number" }, "colSpan": { "description": "Number of columns this cell spans.", "type": "number" }, "mergedInto": { "description": "Id of the cell this one is merged into, if any.", "type": "string" }, "name": { "description": "Display name for the cell.", "type": "string" }, "custom": { "description": "Custom data attached to the cell." }, "borders": { "description": "Per-side border overrides for this cell.", "type": "object", "properties": { "top": { "description": "Top border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } }, "right": { "description": "Right border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } }, "bottom": { "description": "Bottom border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } }, "left": { "description": "Left border override for this cell.", "type": "object", "properties": { "color": { "description": "Border color for this side.", "type": "string" }, "width": { "description": "Border width in pixels for this side.", "type": "number" }, "style": { "description": "Border style for this side.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] } } } } }, "fontSize": { "description": "Font size in pixels (inherits from the table when omitted).", "type": "number" }, "fontFamily": { "description": "Font family name (inherits from the table when omitted).", "type": "string" }, "fontWeight": { "description": "Font weight: 'normal', 'bold', or numeric string (inherits from the table when omitted).", "type": "string" }, "fontStyle": { "description": "Font style: 'normal' or 'italic' (inherits from the table when omitted).", "type": "string" }, "textDecoration": { "description": "Text decoration, e.g. underline, line-through (inherits from the table when omitted).", "type": "string" }, "textTransform": { "description": "Text transform: uppercase, lowercase, capitalize, none (inherits from the table when omitted).", "type": "string" }, "fill": { "description": "Text color (inherits from the table when omitted).", "type": "string" }, "align": { "description": "Horizontal alignment: left, center, right, justify (inherits from the table when omitted).", "type": "string" }, "verticalAlign": { "description": "Vertical alignment: top, middle, bottom (inherits from the table when omitted).", "type": "string" }, "lineHeight": { "description": "Line height multiplier or 'auto' (inherits from the table when omitted).", "anyOf": [ { "type": "number" }, { "type": "string" } ] }, "letterSpacing": { "description": "Letter spacing as a fraction of font size (inherits from the table when omitted).", "type": "number" }, "strokeWidth": { "description": "Stroke width in pixels (inherits from the table when omitted).", "type": "number" }, "stroke": { "description": "Stroke color (inherits from the table when omitted).", "type": "string" }, "cellBackground": { "description": "Cell background color (inherits from the table when omitted).", "type": "string" }, "cellPadding": { "description": "Cell padding in pixels (inherits from the table when omitted).", "type": "number" } }, "required": [ "id" ] } }, "borderColor": { "default": "#000000", "description": "Default border color for the table grid.", "type": "string" }, "borderWidth": { "default": 1, "description": "Default border width in pixels.", "type": "number" }, "borderStyle": { "default": "solid", "description": "Default border style.", "type": "string", "enum": [ "solid", "dashed", "dotted", "none" ] }, "fontSize": { "default": 30, "description": "Default font size in pixels for cells.", "type": "number" }, "fontFamily": { "default": "Roboto", "description": "Default font family name for cells.", "type": "string" }, "fontWeight": { "default": "normal", "description": "Default font weight: 'normal', 'bold', or numeric string.", "type": "string" }, "fontStyle": { "default": "normal", "description": "Default font style: 'normal' or 'italic'.", "type": "string" }, "textDecoration": { "default": "", "description": "Default text decoration (e.g. underline, line-through).", "type": "string" }, "textTransform": { "default": "none", "description": "Default text transform: uppercase, lowercase, capitalize, none.", "type": "string" }, "fill": { "default": "black", "description": "Default text color for cells.", "type": "string" }, "align": { "default": "center", "description": "Default horizontal alignment: left, center, right, justify.", "type": "string" }, "verticalAlign": { "default": "middle", "description": "Default vertical alignment: top, middle, bottom.", "type": "string" }, "lineHeight": { "default": 1.2, "description": "Default line height multiplier or 'auto'.", "anyOf": [ { "type": "number" }, { "type": "string" } ] }, "letterSpacing": { "default": 0, "description": "Default letter spacing as a fraction of font size.", "type": "number" }, "strokeWidth": { "default": 0, "description": "Default stroke width in pixels.", "type": "number" }, "stroke": { "default": "black", "description": "Default stroke color.", "type": "string" }, "cellBackground": { "default": "transparent", "description": "Default cell background color.", "type": "string" }, "cellPadding": { "default": 4, "description": "Default cell padding in pixels.", "type": "number" } }, "required": [ "id", "type" ] } ``` ## Related Definitions [#related-definitions] * [Animation](./animation) * [TableCell](./table-cell) * [CellBorders](./cell-borders) * [BorderSide](./border-side) --- # TextElement URL: https://polotno.com/docs/schema/text-element ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | | | | ------------------------ | ---------------------------------- | ----------------------------------------------------------- | ------ | ----- | --------- | | `id` | `string` | Unique identifier for the element. | | | | | `type` | `"text"` | *No description provided* | | | | | `name` | `string` | Display name for the element. | | | | | `custom` | `unknown` | Custom data attached to the element. | | | | | `x` | `number` | X position in pixels. | | | | | `y` | `number` | Y position in pixels. | | | | | `width` | `number` | Width in pixels. | | | | | `height` | `number` | Height in pixels (0 until measured/rendered). | | | | | `rotation` | `number` | Rotation angle in degrees. | | | | | `opacity` | `number` | Opacity level (0-1). | | | | | `animations` | Array\<[`Animation`](./animation)> | Animation configurations. | | | | | `blurEnabled` | `boolean` | Whether blur effect is enabled. | | | | | `blurRadius` | `number` | Blur radius in pixels. | | | | | `brightnessEnabled` | `boolean` | Whether brightness effect is enabled. | | | | | `brightness` | `number` | Brightness level (-1 to 1). | | | | | `sepiaEnabled` | `boolean` | Whether sepia effect is enabled. | | | | | `grayscaleEnabled` | `boolean` | Whether grayscale effect is enabled. | | | | | `filters` | `object` | Applied image filters keyed by filter name. | | | | | `shadowEnabled` | `boolean` | Whether shadow is enabled. | | | | | `shadowBlur` | `number` | Shadow blur radius in pixels. | | | | | `shadowOffsetX` | `number` | Shadow horizontal offset in pixels. | | | | | `shadowOffsetY` | `number` | Shadow vertical offset in pixels. | | | | | `shadowColor` | `string` | Shadow color. | | | | | `shadowOpacity` | `number` | Shadow opacity (0-1). | | | | | `visible` | `boolean` | Whether the element is visible. | | | | | `draggable` | `boolean` | Whether the element can be dragged. | | | | | `resizable` | `boolean` | Whether the element can be resized. | | | | | `selectable` | `boolean` | Whether the element can be selected. | | | | | `removable` | `boolean` | Whether the element can be removed. | | | | | `contentEditable` | `boolean` | Whether the element content can be edited. | | | | | `styleEditable` | `boolean` | Whether the element style can be edited. | | | | | `alwaysOnTop` | `boolean` | Whether the element stays on top of others. | | | | | `showInExport` | `boolean` | Whether the element appears in exports. | | | | | `text` | `string` | Visible text shown on canvas. | | | | | `placeholder` | `string` | Placeholder text shown when empty. | | | | | `fontSize` | `number` | Font size in pixels. | | | | | `fontFamily` | `string` | Font family name. | | | | | `fontStyle` | `string` | Font style: 'normal' or 'italic'. | | | | | `fontWeight` | `string` | Font weight: 'normal', 'bold', or numeric string. | | | | | `textDecoration` | `string` | Text decoration (e.g. underline, line-through). | | | | | `textTransform` | `string` | Text transform: uppercase, lowercase, capitalize, none. | | | | | `fill` | `string` | Text color. | | | | | `align` | `string` | Horizontal alignment (commonly left | center | right | justify). | | `verticalAlign` | `string` | Vertical alignment: top, middle, bottom. | | | | | `strokeWidth` | `number` | Stroke width in pixels. | | | | | `stroke` | `string` | Stroke color. | | | | | `strokeLineJoin` | `string` | Stroke corner join: round, miter, bevel. | | | | | `lineHeight` | `number` \| `string` | Line height multiplier or 'auto'. | | | | | `letterSpacing` | `number` | Letter spacing as a fraction of font size. | | | | | `backgroundEnabled` | `boolean` | Whether the text background is enabled. | | | | | `backgroundColor` | `string` | Text background color. | | | | | `backgroundOpacity` | `number` | Text background opacity (0-1). | | | | | `backgroundCornerRadius` | `number` | Text background corner radius. | | | | | `backgroundPadding` | `number` | Text background padding. | | | | | `curveEnabled` | `boolean` | Whether the text curve effect is enabled. | | | | | `curvePower` | `number` | Text curve power (-1 to 1, 0 = straight). | | | | | `legacyBackground` | `boolean` | Internal compatibility flag stamped by the v2β†’v3 migration. | | | | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the element." }, "type": { "type": "string", "const": "text" }, "name": { "default": "", "description": "Display name for the element.", "type": "string" }, "custom": { "description": "Custom data attached to the element." }, "x": { "default": 0, "description": "X position in pixels.", "type": "number" }, "y": { "default": 0, "description": "Y position in pixels.", "type": "number" }, "width": { "default": 100, "description": "Width in pixels.", "type": "number" }, "height": { "default": 0, "description": "Height in pixels (0 until measured/rendered).", "type": "number" }, "rotation": { "default": 0, "description": "Rotation angle in degrees.", "type": "number" }, "opacity": { "default": 1, "description": "Opacity level (0-1).", "type": "number" }, "animations": { "default": [], "description": "Animation configurations.", "type": "array", "items": { "type": "object", "properties": { "delay": { "default": 0, "description": "Delay before the animation starts, in milliseconds.", "type": "number" }, "duration": { "default": 500, "description": "Animation duration in milliseconds.", "type": "number" }, "enabled": { "default": true, "description": "Whether the animation is enabled.", "type": "boolean" }, "type": { "type": "string", "enum": [ "enter", "exit", "loop" ], "description": "Animation phase." }, "name": { "default": "none", "description": "Animation name identifier.", "type": "string" }, "data": { "default": {}, "description": "Animation-specific data." } }, "required": [ "type" ] } }, "blurEnabled": { "default": false, "description": "Whether blur effect is enabled.", "type": "boolean" }, "blurRadius": { "default": 10, "description": "Blur radius in pixels.", "type": "number" }, "brightnessEnabled": { "default": false, "description": "Whether brightness effect is enabled.", "type": "boolean" }, "brightness": { "default": 0, "description": "Brightness level (-1 to 1).", "type": "number" }, "sepiaEnabled": { "default": false, "description": "Whether sepia effect is enabled.", "type": "boolean" }, "grayscaleEnabled": { "default": false, "description": "Whether grayscale effect is enabled.", "type": "boolean" }, "filters": { "default": {}, "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": { "type": "object", "properties": { "intensity": { "default": 1, "description": "Filter intensity (-1 to 1).", "type": "number" } } }, "description": "Applied image filters keyed by filter name." }, "shadowEnabled": { "default": false, "description": "Whether shadow is enabled.", "type": "boolean" }, "shadowBlur": { "default": 5, "description": "Shadow blur radius in pixels.", "type": "number" }, "shadowOffsetX": { "default": 0, "description": "Shadow horizontal offset in pixels.", "type": "number" }, "shadowOffsetY": { "default": 0, "description": "Shadow vertical offset in pixels.", "type": "number" }, "shadowColor": { "default": "black", "description": "Shadow color.", "type": "string" }, "shadowOpacity": { "default": 1, "description": "Shadow opacity (0-1).", "type": "number" }, "visible": { "default": true, "description": "Whether the element is visible.", "type": "boolean" }, "draggable": { "default": true, "description": "Whether the element can be dragged.", "type": "boolean" }, "resizable": { "default": true, "description": "Whether the element can be resized.", "type": "boolean" }, "selectable": { "default": true, "description": "Whether the element can be selected.", "type": "boolean" }, "removable": { "default": true, "description": "Whether the element can be removed.", "type": "boolean" }, "contentEditable": { "default": true, "description": "Whether the element content can be edited.", "type": "boolean" }, "styleEditable": { "default": true, "description": "Whether the element style can be edited.", "type": "boolean" }, "alwaysOnTop": { "default": false, "description": "Whether the element stays on top of others.", "type": "boolean" }, "showInExport": { "default": true, "description": "Whether the element appears in exports.", "type": "boolean" }, "text": { "default": "", "description": "Visible text shown on canvas.", "type": "string" }, "placeholder": { "default": "", "description": "Placeholder text shown when empty.", "type": "string" }, "fontSize": { "default": 14, "description": "Font size in pixels.", "type": "number" }, "fontFamily": { "default": "Roboto", "description": "Font family name.", "type": "string" }, "fontStyle": { "default": "normal", "description": "Font style: 'normal' or 'italic'.", "type": "string" }, "fontWeight": { "default": "normal", "description": "Font weight: 'normal', 'bold', or numeric string.", "type": "string" }, "textDecoration": { "default": "", "description": "Text decoration (e.g. underline, line-through).", "type": "string" }, "textTransform": { "default": "none", "description": "Text transform: uppercase, lowercase, capitalize, none.", "type": "string" }, "fill": { "default": "black", "description": "Text color.", "type": "string" }, "align": { "default": "center", "description": "Horizontal alignment (commonly left | center | right | justify).", "type": "string" }, "verticalAlign": { "default": "top", "description": "Vertical alignment: top, middle, bottom.", "type": "string" }, "strokeWidth": { "default": 0, "description": "Stroke width in pixels.", "type": "number" }, "stroke": { "default": "black", "description": "Stroke color.", "type": "string" }, "strokeLineJoin": { "default": "round", "description": "Stroke corner join: round, miter, bevel.", "type": "string" }, "lineHeight": { "default": 1.2, "description": "Line height multiplier or 'auto'.", "anyOf": [ { "type": "number" }, { "type": "string" } ] }, "letterSpacing": { "default": 0, "description": "Letter spacing as a fraction of font size.", "type": "number" }, "backgroundEnabled": { "default": false, "description": "Whether the text background is enabled.", "type": "boolean" }, "backgroundColor": { "default": "#7ED321", "description": "Text background color.", "type": "string" }, "backgroundOpacity": { "default": 1, "description": "Text background opacity (0-1).", "type": "number" }, "backgroundCornerRadius": { "default": 0.5, "description": "Text background corner radius.", "type": "number" }, "backgroundPadding": { "default": 0.5, "description": "Text background padding.", "type": "number" }, "curveEnabled": { "default": false, "description": "Whether the text curve effect is enabled.", "type": "boolean" }, "curvePower": { "default": 0.5, "description": "Text curve power (-1 to 1, 0 = straight).", "type": "number" }, "legacyBackground": { "description": "Internal compatibility flag stamped by the v2β†’v3 migration.", "type": "boolean" } }, "required": [ "id", "type" ] } ``` ## Related Definitions [#related-definitions] * [Animation](./animation) --- # VideoElement URL: https://polotno.com/docs/schema/video-element ## Type Information [#type-information] **Base Type:** `object` ## Properties [#properties] | Property | Type | Description | | ------------------- | ---------------------------------- | ----------------------------------------------- | | `id` | `string` | Unique identifier for the element. | | `type` | `"video"` | *No description provided* | | `name` | `string` | Display name for the element. | | `custom` | `unknown` | Custom data attached to the element. | | `x` | `number` | X position in pixels. | | `y` | `number` | Y position in pixels. | | `width` | `number` | Width in pixels. | | `height` | `number` | Height in pixels. | | `rotation` | `number` | Rotation angle in degrees. | | `opacity` | `number` | Opacity level (0-1). | | `animations` | Array\<[`Animation`](./animation)> | Animation configurations. | | `blurEnabled` | `boolean` | Whether blur effect is enabled. | | `blurRadius` | `number` | Blur radius in pixels. | | `brightnessEnabled` | `boolean` | Whether brightness effect is enabled. | | `brightness` | `number` | Brightness level (-1 to 1). | | `sepiaEnabled` | `boolean` | Whether sepia effect is enabled. | | `grayscaleEnabled` | `boolean` | Whether grayscale effect is enabled. | | `filters` | `object` | Applied image filters keyed by filter name. | | `shadowEnabled` | `boolean` | Whether shadow is enabled. | | `shadowBlur` | `number` | Shadow blur radius in pixels. | | `shadowOffsetX` | `number` | Shadow horizontal offset in pixels. | | `shadowOffsetY` | `number` | Shadow vertical offset in pixels. | | `shadowColor` | `string` | Shadow color. | | `shadowOpacity` | `number` | Shadow opacity (0-1). | | `visible` | `boolean` | Whether the element is visible. | | `draggable` | `boolean` | Whether the element can be dragged. | | `resizable` | `boolean` | Whether the element can be resized. | | `selectable` | `boolean` | Whether the element can be selected. | | `removable` | `boolean` | Whether the element can be removed. | | `contentEditable` | `boolean` | Whether the element content can be edited. | | `styleEditable` | `boolean` | Whether the element style can be edited. | | `alwaysOnTop` | `boolean` | Whether the element stays on top of others. | | `showInExport` | `boolean` | Whether the element appears in exports. | | `src` | `string` | URL or data URI of the image file. | | `cropX` | `number` | Crop X offset as a fraction (0-1). | | `cropY` | `number` | Crop Y offset as a fraction (0-1). | | `cropWidth` | `number` | Crop width as a fraction (0-1). | | `cropHeight` | `number` | Crop height as a fraction (0-1). | | `cornerRadius` | `number` | Corner radius in pixels. | | `flipX` | `boolean` | Whether the image is flipped horizontally. | | `flipY` | `boolean` | Whether the image is flipped vertically. | | `clipSrc` | `string` | URL or data URI for a clipping mask. | | `borderColor` | `string` | Border color. | | `borderSize` | `number` | Border width in pixels. | | `keepRatio` | `boolean` | Whether to maintain aspect ratio when resizing. | | `stretchEnabled` | `boolean` | Whether stretch mode is enabled. | | `duration` | `number` | Video duration in milliseconds. | | `startTime` | `number` | Start offset (0-1). | | `endTime` | `number` | End offset (0-1). | | `volume` | `number` | Volume (0-1). | | `speed` | `number` | Playback speed multiplier (1 = normal). | ## JSON Schema [#json-schema] ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the element." }, "type": { "type": "string", "const": "video" }, "name": { "default": "", "description": "Display name for the element.", "type": "string" }, "custom": { "description": "Custom data attached to the element." }, "x": { "default": 0, "description": "X position in pixels.", "type": "number" }, "y": { "default": 0, "description": "Y position in pixels.", "type": "number" }, "width": { "default": 100, "description": "Width in pixels.", "type": "number" }, "height": { "default": 100, "description": "Height in pixels.", "type": "number" }, "rotation": { "default": 0, "description": "Rotation angle in degrees.", "type": "number" }, "opacity": { "default": 1, "description": "Opacity level (0-1).", "type": "number" }, "animations": { "default": [], "description": "Animation configurations.", "type": "array", "items": { "type": "object", "properties": { "delay": { "default": 0, "description": "Delay before the animation starts, in milliseconds.", "type": "number" }, "duration": { "default": 500, "description": "Animation duration in milliseconds.", "type": "number" }, "enabled": { "default": true, "description": "Whether the animation is enabled.", "type": "boolean" }, "type": { "type": "string", "enum": [ "enter", "exit", "loop" ], "description": "Animation phase." }, "name": { "default": "none", "description": "Animation name identifier.", "type": "string" }, "data": { "default": {}, "description": "Animation-specific data." } }, "required": [ "type" ] } }, "blurEnabled": { "default": false, "description": "Whether blur effect is enabled.", "type": "boolean" }, "blurRadius": { "default": 10, "description": "Blur radius in pixels.", "type": "number" }, "brightnessEnabled": { "default": false, "description": "Whether brightness effect is enabled.", "type": "boolean" }, "brightness": { "default": 0, "description": "Brightness level (-1 to 1).", "type": "number" }, "sepiaEnabled": { "default": false, "description": "Whether sepia effect is enabled.", "type": "boolean" }, "grayscaleEnabled": { "default": false, "description": "Whether grayscale effect is enabled.", "type": "boolean" }, "filters": { "default": {}, "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": { "type": "object", "properties": { "intensity": { "default": 1, "description": "Filter intensity (-1 to 1).", "type": "number" } } }, "description": "Applied image filters keyed by filter name." }, "shadowEnabled": { "default": false, "description": "Whether shadow is enabled.", "type": "boolean" }, "shadowBlur": { "default": 5, "description": "Shadow blur radius in pixels.", "type": "number" }, "shadowOffsetX": { "default": 0, "description": "Shadow horizontal offset in pixels.", "type": "number" }, "shadowOffsetY": { "default": 0, "description": "Shadow vertical offset in pixels.", "type": "number" }, "shadowColor": { "default": "black", "description": "Shadow color.", "type": "string" }, "shadowOpacity": { "default": 1, "description": "Shadow opacity (0-1).", "type": "number" }, "visible": { "default": true, "description": "Whether the element is visible.", "type": "boolean" }, "draggable": { "default": true, "description": "Whether the element can be dragged.", "type": "boolean" }, "resizable": { "default": true, "description": "Whether the element can be resized.", "type": "boolean" }, "selectable": { "default": true, "description": "Whether the element can be selected.", "type": "boolean" }, "removable": { "default": true, "description": "Whether the element can be removed.", "type": "boolean" }, "contentEditable": { "default": true, "description": "Whether the element content can be edited.", "type": "boolean" }, "styleEditable": { "default": true, "description": "Whether the element style can be edited.", "type": "boolean" }, "alwaysOnTop": { "default": false, "description": "Whether the element stays on top of others.", "type": "boolean" }, "showInExport": { "default": true, "description": "Whether the element appears in exports.", "type": "boolean" }, "src": { "default": "", "description": "URL or data URI of the image file.", "type": "string" }, "cropX": { "default": 0, "description": "Crop X offset as a fraction (0-1).", "type": "number" }, "cropY": { "default": 0, "description": "Crop Y offset as a fraction (0-1).", "type": "number" }, "cropWidth": { "default": 1, "description": "Crop width as a fraction (0-1).", "type": "number" }, "cropHeight": { "default": 1, "description": "Crop height as a fraction (0-1).", "type": "number" }, "cornerRadius": { "default": 0, "description": "Corner radius in pixels.", "type": "number" }, "flipX": { "default": false, "description": "Whether the image is flipped horizontally.", "type": "boolean" }, "flipY": { "default": false, "description": "Whether the image is flipped vertically.", "type": "boolean" }, "clipSrc": { "default": "", "description": "URL or data URI for a clipping mask.", "type": "string" }, "borderColor": { "default": "black", "description": "Border color.", "type": "string" }, "borderSize": { "default": 0, "description": "Border width in pixels.", "type": "number" }, "keepRatio": { "default": false, "description": "Whether to maintain aspect ratio when resizing.", "type": "boolean" }, "stretchEnabled": { "default": false, "description": "Whether stretch mode is enabled.", "type": "boolean" }, "duration": { "default": 0, "description": "Video duration in milliseconds.", "type": "number" }, "startTime": { "default": 0, "description": "Start offset (0-1).", "type": "number" }, "endTime": { "default": 1, "description": "End offset (0-1).", "type": "number" }, "volume": { "default": 1, "description": "Volume (0-1).", "type": "number" }, "speed": { "default": 1, "description": "Playback speed multiplier (1 = normal).", "type": "number" } }, "required": [ "id", "type" ] } ``` ## Related Definitions [#related-definitions] * [Animation](./animation)