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);// This is a helper function to check that the page we retrieved is// an "Absolute" page. Other pages are not supported by the 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);}}// This method shows how to replace all fills on the page// with an imageconst replaceAllFillsWithDogs = () => {openDesign({ type: "current_page" }, async (session) => {// Check that we're on a supported pagecheckAbsolute(session.page);// Find all elements on the page that can contain a fillconst elementsToReplace = session.page.elements.filter((el) => el.type === "rect" || el.type === "shape",);if (elementsToReplace.length === 0) {return;}setOperation(Operation.UPDATE);// Upload the image we want to useconst dogImage = await uploadLocalImage();const dogMedia: DesignEditing.ImageFill = {type: "image",flipX: false,flipY: false,imageRef: dogImage.ref,};// For each element, update the fills to contain our new imageelementsToReplace.forEach((element) => {switch (element.type) {case "rect":element.fill.mediaContainer.set(dogMedia);break;case "shape":// Some shape paths cannot be edited, so we need to check for thiselement.paths.forEach((p) =>p.fill.isMediaEditable && p.fill.mediaContainer.set(dogMedia),);break;default:throw new Error("Unexpected Element");}});// Save the changesreturn session.sync();}).catch((e) => (e instanceof Error ? setError(e.message) : setError(e))).finally(() => setOperation(Operation.NONE));};// This method flips the design around the Y-axisconst flipDesign = () => {openDesign({ type: "current_page" }, async (session) => {checkAbsolute(session.page);// Whiteboards do not have static dimensions, so we can use this to figure out// the dimensions that bound the elementsconst dimensions = findElementsBoundingBox(session.page.elements);// For designs with static dimensions, the center is just half the width.const pageCenter = session.page.dimensions? session.page.dimensions.width / 2: dimensions.centerX;session.page.elements.filter((element) => element.type !== "unsupported").forEach((element) => {// compute new x-position by flipping elements around the centerelement.left = 2 * pageCenter - element.left - element.width;// also horizontally flip any mediaif (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);// Save the changes to the designreturn session.sync();}).catch((e) => (e instanceof Error ? setError(e.message) : setError(e))).finally(() => setOperation(Operation.NONE));};// This shows an example of how to delete elementsconst deleteAllTextElements = () => {openDesign({ type: "current_page" }, async (session) => {// Check that we're on a supported pagecheckAbsolute(session.page);const page = session.page;// Delete all text elementspage.elements.forEach((element) => {if (element.type === "text") {page.elements.delete(element);}});setOperation(Operation.DELETE);// Save the changes to the designreturn session.sync();}).catch((e) => (e instanceof Error ? setError(e.message) : setError(e))).finally(() => setOperation(Operation.NONE));};// This examples shows how you can add elements to the designconst addTextWithSparkles = () => {setOperation(Operation.INSERT);openDesign({ type: "current_page" }, async (session) => {// Check that we're on a supported pagecheckAbsolute(session.page);// The elementStateBuilder provides convenience methods for creating new elementsconst { elementStateBuilder } = session.helpers;const { ref } = await uploadSparkle();const text = elementStateBuilder.createRichtextRange();text.appendText("Hello World");text.formatParagraph({ index: 0, length: 11 }, { fontSize: 60 });// Add text element to design// Note that the only way to add elements to a design is to insert// the element into a page's elements listconst 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");}// Insert a sparkle behind 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 a sparkle in front of the textsession.page.elements.insertAfter(insertedText,elementStateBuilder.createRectElement({left: insertedText.left + 280,top: insertedText.top + 20,width: 50,height: 50,fill: {mediaContainer: {imageRef: ref,type: "image",},},}),);// Save the changes to the designreturn session.sync();}).catch((e) => (e instanceof Error ? setError(e.message) : setError(e))).finally(() => setOperation(Operation.NONE));};// This example shows how to use a helper to group elementsasync function groupAllSupportedElements() {return openDesign({ type: "current_page" }, async (session) => {// Check that we're on a supported pagecheckAbsolute(session.page);const { group } = session.helpers;// get all elements on the page that can be groupedconst 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);// Group the elementsawait group({ elements: elsToGroup });// Save the changes to the designawait session.sync();}).catch((e) => (e instanceof Error ? setError(e.message) : setError(e))).finally(() => setOperation(Operation.NONE));}// This shows an example of how to add a group to a pageasync function insertAndGroup() {return openDesign({ type: "current_page" }, async (session) => {// Check that we're on a supported pagecheckAbsolute(session.page);const { group, elementStateBuilder } = session.helpers;const width = session.page.dimensions?.width || 0;const height = session.page.dimensions?.height || 0;// Create the elements we want in the groupconst 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;}const textRange = elementStateBuilder.createRichtextRange();textRange.appendText("Well Done!");textRange.formatParagraph({ index: 0, length: 11 },{ fontSize: 45, color: "#000000", textAlign: "center" },);const 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 elements we createdawait group({ elements: [shape, text] });// Save the changes 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>);};function uploadLocalImage() {return upload({mimeType: "image/jpeg",thumbnailUrl: dog,type: "image",aiDisclosure: "none",url: dog,width: 100,height: 100,});}function uploadSparkle() {return upload({mimeType: "image/png",thumbnailUrl: sparkle,type: "image",aiDisclosure: "none",url: sparkle,width: 538,height: 550,});}function 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
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();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)