Examples
App elements
Assets and media
Fundamentals
Intents
Design interaction
Drag and drop
Design elements
Localization
Content replacement
Design editing
Edit and modify design properties and content.
Running this example
To run this example locally:
-
If you haven't already, create a new app in the Developer Portal(opens in a new tab or window). For more information, refer to our Quickstart guide.
-
In your app's configuration on the Developer Portal(opens in a new tab or window), ensure the "Development URL" is set to
http://localhost:8080
. -
Clone the starter kit:
git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.gitcd canva-apps-sdk-starter-kitSHELL -
Install dependencies:
npm installSHELL -
Run the example:
npm run start design_editingSHELL -
Click the Preview URL link shown in the terminal to open the example in the Canva editor.
Example app source code
// For usage information, see the README.md file./* eslint-disable no-restricted-imports */import { Button, Rows, Text, Alert } from "@canva/app-ui-kit";import { upload } from "@canva/asset";import type { DesignEditing } from "@canva/design";import { openDesign } from "@canva/design";import dog from "assets/images/dog.jpg";import sparkle from "assets/images/sparkle.png";import { useState } from "react";import * as styles from "styles/components.css";enum Operation {NONE,UPDATE,FLIP,DELETE,INSERT,GROUP,INSERT_AND_GROUP,}export const App = () => {const [operation, setOperation] = useState<Operation>(Operation.NONE);const [error, setError] = useState<string | undefined>(undefined);// Helper function to verify the page type. The Design Editing API only supports// "absolute" pages (like presentations, posters). Other page types like "flow"// pages (docs, whiteboards) are not supported by the Design Editing API.function checkAbsolute(page: DesignEditing.Page,): asserts page is DesignEditing.AbsolutePage {if (page.type !== "absolute") {setError("Page type is not supported");throw new Error("Page type is not supported");} else {setError(undefined);}}// Demonstrates updating element properties using the Design Editing API.// Shows how to replace fills with uploaded images across multiple elements.const replaceAllFillsWithDogs = () => {// openDesign creates a Design Editing session for the current pageopenDesign({ type: "current_page" }, async (session) => {// Verify we're on a supported page typecheckAbsolute(session.page);// Find elements that support fill modifications (rect and shape elements)const elementsToReplace = session.page.elements.filter((el) => el.type === "rect" || el.type === "shape",);if (elementsToReplace.length === 0) {return;}setOperation(Operation.UPDATE);// Upload image using the Asset API and create an ImageFill objectconst dogImage = await uploadLocalImage();const dogMedia: DesignEditing.ImageFill = {type: "image",flipX: false,flipY: false,imageRef: dogImage.ref,};// Update each element's fill with the uploaded imageelementsToReplace.forEach((element) => {switch (element.type) {case "rect":element.fill.mediaContainer.set(dogMedia);break;case "shape":// Shape elements have paths - check if each path's fill can be editedelement.paths.forEach((p) =>p.fill.isMediaEditable && p.fill.mediaContainer.set(dogMedia),);break;default:throw new Error("Unexpected Element");}});// Commit all changes to the design using the session.sync() methodreturn session.sync();}).catch((e) => (e instanceof Error ? setError(e.message) : setError(e))).finally(() => setOperation(Operation.NONE));};// Demonstrates element transformation by flipping the entire design horizontally.// Shows how to manipulate element positions and media flip properties.const flipDesign = () => {openDesign({ type: "current_page" }, async (session) => {checkAbsolute(session.page);// Calculate bounding box for pages without fixed dimensions (like whiteboards)const dimensions = findElementsBoundingBox(session.page.elements);// Determine the horizontal center point for flipping calculationsconst pageCenter = session.page.dimensions? session.page.dimensions.width / 2: dimensions.centerX;// Process all supported elements on the pagesession.page.elements.filter((element) => element.type !== "unsupported").forEach((element) => {// Mirror element position around the page centerelement.left = 2 * pageCenter - element.left - element.width;// Also flip any media content horizontally to maintain visual consistencyif (element.type === "rect" &&element.fill.mediaContainer.ref != null) {element.fill.mediaContainer.ref.flipX =!element.fill.mediaContainer.ref.flipX;}if (element.type === "shape") {element.paths.forEach((path) => {if (path.fill.mediaContainer.ref != null) {path.fill.mediaContainer.ref.flipX =!path.fill.mediaContainer.ref.flipX;}});}});setOperation(Operation.FLIP);// Commit the transformation changes to the designreturn session.sync();}).catch((e) => (e instanceof Error ? setError(e.message) : setError(e))).finally(() => setOperation(Operation.NONE));};// Demonstrates element deletion using the Design Editing API.// Shows how to remove specific element types from the page.const deleteAllTextElements = () => {openDesign({ type: "current_page" }, async (session) => {// Verify we're on a supported page typecheckAbsolute(session.page);const page = session.page;// Remove all text elements from the page using the elements.delete() methodpage.elements.forEach((element) => {if (element.type === "text") {page.elements.delete(element);}});setOperation(Operation.DELETE);// Commit the deletion changes to the designreturn session.sync();}).catch((e) => (e instanceof Error ? setError(e.message) : setError(e))).finally(() => setOperation(Operation.NONE));};// Demonstrates element creation and insertion using the Design Editing API.// Shows how to create text and image elements and add them to the page.const addTextWithSparkles = () => {setOperation(Operation.INSERT);openDesign({ type: "current_page" }, async (session) => {// Verify we're on a supported page typecheckAbsolute(session.page);// Use elementStateBuilder helper to create new elements with proper formattingconst { elementStateBuilder } = session.helpers;const { ref } = await uploadSparkle();const text = elementStateBuilder.createRichtextRange();text.appendText("Hello World");text.formatParagraph({ index: 0, length: 11 }, { fontSize: 60 });// Insert text element into the page using the elements.insertBefore() method// Elements must be added to the page's elements list to appear in the designconst insertedText = session.page.elements.insertBefore(undefined,elementStateBuilder.createTextElement({text: { regions: text.readTextRegions() },left: (session.page.dimensions?.width || 0) / 2 - 160,top: (session.page.dimensions?.height || 0) / 2 - 160,width: 320,}),);if (!insertedText) {throw new Error(" Could not insert text");}// Create decorative sparkle elements positioned relative to the textsession.page.elements.insertBefore(insertedText,elementStateBuilder.createRectElement({left: insertedText.left - 25,top: insertedText.top - 10,width: 50,height: 50,fill: {mediaContainer: {imageRef: ref,type: "image",},},}),);// Insert another sparkle element using insertAfter for z-order controlsession.page.elements.insertAfter(insertedText,elementStateBuilder.createRectElement({left: insertedText.left + 280,top: insertedText.top + 20,width: 50,height: 50,fill: {mediaContainer: {imageRef: ref,type: "image",},},}),);// Commit all element insertions to the designreturn session.sync();}).catch((e) => (e instanceof Error ? setError(e.message) : setError(e))).finally(() => setOperation(Operation.NONE));};// Demonstrates element grouping using the Design Editing API helpers.// Shows how to group existing elements on the page together.async function groupAllSupportedElements() {return openDesign({ type: "current_page" }, async (session) => {// Verify we're on a supported page typecheckAbsolute(session.page);const { group } = session.helpers;// Filter to get elements that support grouping operationsconst elsToGroup = session.page.elements.filter((el) =>el.type === "embed" ||el.type === "rect" ||el.type === "shape" ||el.type === "text",);if (elsToGroup.length < 2) {setError("Need at least 2 supported elements (embed, rect, shape or text) to group them",);return;}setOperation(Operation.GROUP);// Use the group helper to combine elements into a single grouped elementawait group({ elements: elsToGroup });// Commit the grouping changes to the designawait session.sync();}).catch((e) => (e instanceof Error ? setError(e.message) : setError(e))).finally(() => setOperation(Operation.NONE));}// Demonstrates creating new elements and immediately grouping them.// Shows how to combine element creation, insertion, and grouping operations.async function insertAndGroup() {return openDesign({ type: "current_page" }, async (session) => {// Verify we're on a supported page typecheckAbsolute(session.page);const { group, elementStateBuilder } = session.helpers;const width = session.page.dimensions?.width || 0;const height = session.page.dimensions?.height || 0;// Create a star-shaped element with custom SVG path dataconst shape = session.page.elements.insertBefore(undefined,elementStateBuilder.createShapeElement({top: (height ? height / 2 : 0) - 300,left: (width ? width / 2 : 0) - 300,width: 600,height: 600,viewBox: {top: 0,left: 0,width: 64,height: 64,},paths: [{d: "M32 0L36.5053 3.55458L41.8885 1.56619L45.0749 6.33901L50.8091 6.11146L52.3647 11.6353L57.8885 13.1909L57.661 18.9251L62.4338 22.1115L60.4454 27.4947L64 32L60.4454 36.5053L62.4338 41.8885L57.661 45.0749L57.8885 50.8091L52.3647 52.3647L50.8091 57.8885L45.0749 57.661L41.8885 62.4338L36.5053 60.4454L32 64L27.4947 60.4454L22.1115 62.4338L18.9251 57.661L13.1909 57.8885L11.6353 52.3647L6.11146 50.8091L6.33901 45.0749L1.56619 41.8885L3.55458 36.5053L0 32L3.55458 27.4947L1.56619 22.1115L6.33901 18.9251L6.11146 13.1909L11.6353 11.6353L13.1909 6.11146L18.9251 6.33901L22.1115 1.56619L27.4947 3.55458L32 0Z",fill: {colorContainer: {color: "#ffde59",type: "solid",},},},],}),);if (shape == null) {setError("Could not create shape element");return;}// Create formatted text content using the richtext range builderconst textRange = elementStateBuilder.createRichtextRange();textRange.appendText("Well done!");textRange.formatParagraph({ index: 0, length: 10 },{ fontSize: 45, color: "#000000", textAlign: "center" },);// Insert text element positioned over the star shapeconst text = session.page.elements.insertAfter(shape,elementStateBuilder.createTextElement({top: shape.top + shape.height / 2 - 30,left: shape.left,width: shape?.width,text: { regions: textRange.readTextRegions() },}),);if (text == null) {setError("Could not create text element");return;}setOperation(Operation.INSERT_AND_GROUP);// Group the newly created elements together into a single unitawait group({ elements: [shape, text] });// Commit all creation and grouping operations to the designawait session.sync();}).catch((e) => (e instanceof Error ? setError(e.message) : setError(e))).finally(() => setOperation(Operation.NONE));}return (<div className={styles.scrollContainer}><Rows spacing="2u"><Text>This example demonstrates how apps can edit the design</Text>{error && <Alert tone="critical">{error}</Alert>}<Buttonvariant="secondary"onClick={flipDesign}disabled={operation !== Operation.NONE}loading={operation === Operation.FLIP}>Flip design</Button><Buttonvariant="secondary"onClick={deleteAllTextElements}disabled={operation !== Operation.NONE}loading={operation === Operation.DELETE}>Delete all text elements</Button><Buttonvariant="secondary"onClick={replaceAllFillsWithDogs}disabled={operation !== Operation.NONE}loading={operation === Operation.UPDATE}>Replace all fills with dogs</Button><Buttonvariant="secondary"onClick={addTextWithSparkles}disabled={operation !== Operation.NONE}loading={operation === Operation.INSERT}>Add some sparkly text</Button><Buttonvariant="secondary"onClick={groupAllSupportedElements}disabled={operation !== Operation.NONE}loading={operation === Operation.GROUP}>Group elements on page</Button><Buttonvariant="secondary"onClick={insertAndGroup}disabled={operation !== Operation.NONE}loading={operation === Operation.INSERT_AND_GROUP}>Insert a group of elements</Button></Rows></div>);};// Helper function to upload the dog image asset using the Asset APIfunction uploadLocalImage() {return upload({mimeType: "image/jpeg",thumbnailUrl: dog,type: "image",aiDisclosure: "none",url: dog,width: 100,height: 100,});}// Helper function to upload the sparkle image asset using the Asset APIfunction uploadSparkle() {return upload({mimeType: "image/png",thumbnailUrl: sparkle,type: "image",aiDisclosure: "none",url: sparkle,width: 538,height: 550,});}// Helper function to calculate the bounding box that contains all elements on a page// Used for pages without fixed dimensions (like whiteboards) to determine layout boundsfunction findElementsBoundingBox(elements: DesignEditing.ElementList) {const lefts: number[] = [];const tops: number[] = [];const rights: number[] = [];const bottoms: number[] = [];elements.forEach((el) => {lefts.push(el.left);rights.push(el.left + el.width);tops.push(el.top);bottoms.push(el.top + el.height);});const left = Math.min(...lefts);const right = Math.max(...rights);const top = Math.max(...tops);const bottom = Math.min(...bottoms);return {left,top,width: right - left,height: bottom - top,centerX: (right + left) / 2,};}
TYPESCRIPT
// For usage information, see the README.md file.import { AppUiProvider } from "@canva/app-ui-kit";import { createRoot } from "react-dom/client";import { App } from "./app";import "@canva/app-ui-kit/styles.css";const root = createRoot(document.getElementById("root") as Element);function render() {root.render(<AppUiProvider><App /></AppUiProvider>,);}render();// Hot Module Replacement for development (automatically reloads the app when changes are made)if (module.hot) {module.hot.accept("./app", render);}
TYPESCRIPT
# Design editingDemonstrates advanced design manipulation including element updates, transformations, deletions, insertions, and grouping operations. Shows complex design editing workflows using the Design Editing API.For API reference docs and instructions on running this example, see: https://www.canva.dev/docs/apps/examples/design-editing/.Related examples: See design_interaction/positioning_elements for element positioning, or design_interaction/page_addition for adding new pages.NOTE: This example differs from what is expected for public apps to pass a Canva review:- Static assets are used for demonstration purposes only. Production apps should host assets on a CDN/hosting service and use the `upload` function from the `@canva/asset` package- ESLint rule `no-restricted-imports` is disabled for example purposes only. Production apps should not disable linting rules without proper justification- Error handling is simplified for demonstration. Production apps must implement comprehensive error handling with clear user feedback and graceful failure modes- Internationalization is not implemented. Production apps must support multiple languages using the `@canva/app-i18n-kit` package to pass Canva review requirements
MARKDOWN
API reference
Need help?
- Join our Community Forum(opens in a new tab or window)
- Report issues with this example on GitHub(opens in a new tab or window)