On September 25th, 2024, we released v2 of the Apps SDK. To learn what’s new and how to upgrade, see Migration FAQ and Migration guide.

Design editing

How to read and update elements in a design.

This API is in preview mode and may experience breaking changes. Apps that use this API will not pass the review process and can't be made available on the Apps Marketplace.

Apps can traverse the elements in a design and then read or update their structure and content. This allows apps to analyze and manipulate designs in a variety of ways, such as by rearranging all of the content on a page.

Elements are not the same as content in a design. However, they are related because elements contain content. An app can position elements on a page, and change the dimensions of an element through design editing, but an app needs content querying to operate on content. Even if design editing can reach content in some instances, content querying operates consistently on the content contained within elements. See Content for more information, and the Content Querying API guidelines.

Prerequisites

The APIs documented on this page require the beta version of @canva/design package:

npm install @canva/design@beta
BASH

Element types

Apps can traverse the following types of elements:

While traversing, there's no such thing as an "image" or "video" element. Instead, there's only rect elements, which may contain video or image content. (In the future, we hope to resolve this inconsistency.)

Other types of elements, such as tables, are not supported.

Design contexts

App must specify a context that indicates which part of the design should be traversed.

For the time being, apps can only traverse elements on the current page. In the future, additional contexts will be supported, such as specific pages, ranges of pages, and more.

It's not possible for apps to traverse elements across an entire design.

Reading designs

To read the structure of a design, call the openDesign method:

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft) => {
console.log(draft);
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Read design
</Button>
</Rows>
</div>
);
}
TSX

This method accepts a context and a function that receives a draft object.

This draft is a snapshot that contains information about the specified part of the design, such as its pages and elements on those pages. The available information depends on the context.

Reading pages

When the context is the current page, the draft contains a page object:

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft) => {
console.log(draft.page);
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Read design
</Button>
</Rows>
</div>
);
}
TSX

There are different types of pages, each of which exposes different properties.

For the time being, apps can only read the structure and content of fixed pages. All other types of pages have a type of "unsupported" and don't expose any additional information.

Reading elements

A page may contain zero or more elements:

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft) => {
console.log(draft.page.elements);
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Read design
</Button>
</Rows>
</div>
);
}
TSX

These elements are exposed as a list and this list provides methods for interacting with the elements in a variety of ways, such as by counting or filtering them:

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft) => {
draft.page.elements.forEach((element, index) => {
console.log(index, element);
});
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Read design
</Button>
</Rows>
</div>
);
}
TSX

The order of the elements in the list determines the order in which the elements are rendered. For example, elements later in the list are rendered in front of elements earlier in the list.

The elements themselves share some common properties, such as dimensions, while also having properties that are unique to their type:

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft) => {
draft.page.elements.forEach((element, index) => {
// Properties specific to types
if (element.type === "embed") {
console.log(element.url);
}
// General properties
console.log(element.width);
});
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Read design
</Button>
</Rows>
</div>
);
}
TSX

Updating elements

Apps can manipulate elements by calling methods exposed on the lists:

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
import type { DesignEditing } from "@canva/design";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft, { elementBuilder }) => {
if (draft.page.type !== "fixed") {
return;
}
// Inserting a new rectangle element using list methods
const newRect = elementBuilder.createRectElement({
top: 100,
left: 100,
width: 150,
height: 100,
fill: {
color: {
type: "solid",
color: "#ff0000",
},
},
});
// Insert the new rectangle at the end of the elements list
draft.page.elements.insertAfter(undefined, newRect);
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Insert rectangle
</Button>
</Rows>
</div>
);
}
TSX

...or by overwriting the properties of individual elements:

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft) => {
if (draft.page.type !== "fixed") {
return;
}
// Changing the fill color of all rectangle elements
draft.page.elements.forEach((element) => {
if (element.type === "rect") {
// Overwrite the fill color property
element.fill.color = {
type: "solid",
color: "#00ff00",
};
}
});
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Change rectangle colors
</Button>
</Rows>
</div>
);
}
TSX

Saving changes

Any changes made to the draft object are not automatically reflected in the design. For the changes to be reflected, the app needs to explicitly save the changes that it makes.

To save changes to the draft, call the save method that's exposed by the draft object:

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft) => {
if (draft.page.type !== "fixed") {
return;
}
// Moving all elements 50 pixels to the right and 50 pixels down
draft.page.elements.forEach((element) => {
element.left += 50;
element.top += 50;
console.log(`Moved element to (${element.left}, ${element.top}).`);
});
return await draft.save();
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Move elements
</Button>
</Rows>
</div>
);
}
TSX

Once this method is called, no further changes can be made without calling the openDesign method again.

Code samples

Counting elements

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft) => {
if (draft.page.type !== "fixed") {
return;
}
const elementCount = draft.page.elements.count();
console.log(`Total elements on the page: ${elementCount}`);
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Count elements
</Button>
</Rows>
</div>
);
}
TSX

Iterating elements

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft) => {
if (draft.page.type !== "fixed") {
return;
}
draft.page.elements.forEach((element, index) => {
console.log(
`Element ${index + 1}: Type=${element.type}, Position=(${element.left}, ${element.top})`
);
});
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Iterate elements
</Button>
</Rows>
</div>
);
}
TSX

Filtering elements

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
import type { DesignEditing } from "@canva/design";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft) => {
if (draft.page.type !== "fixed") {
return;
}
const textElements =
draft.page.elements.filter<DesignEditing.TextElement>(
(el) => el.type === "text"
);
console.log(`Found ${textElements.length} text element(s):`);
textElements.forEach((el, index) => {
console.log(
`Text Element ${index + 1}: "${el.text.text}" at (${el.left}, ${el.top})`
);
});
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Filter text elements
</Button>
</Rows>
</div>
);
}
TSX

Inserting elements

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
import type { DesignEditing } from "@canva/design";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft, { elementBuilder }) => {
if (draft.page.type !== "fixed") {
return;
}
// Define rectangle properties
const rectOpts: DesignEditing.CreateRectElementOpts = {
top: 150,
left: 150,
width: 200,
height: 100,
fill: {
color: {
type: "solid",
color: "#00ff00",
},
},
};
// Create the rectangle element
const rectElement = elementBuilder.createRectElement(rectOpts);
// Insert the rectangle at the end of the elements list
draft.page.elements.insertAfter(undefined, rectElement);
// Save the changes
return await draft.save();
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Insert rectangle
</Button>
</Rows>
</div>
);
}
TSX

Moving elements

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft) => {
if (draft.page.type !== "fixed") {
return;
}
draft.page.elements.forEach((element) => {
element.left += 50;
element.top += 50;
console.log(`Moved element to (${element.left}, ${element.top})`);
});
// Save the changes
console.log("All elements have been moved.");
return await draft.save();
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Move elements
</Button>
</Rows>
</div>
);
}
TSX

Deleting elements

import { openDesign } from "@canva/design";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";
import type { DesignEditing } from "@canva/design";
export function App() {
function handleClick() {
openDesign({ type: "current_page" }, async (draft) => {
if (draft.page.type !== "fixed") {
return;
}
const textElements =
draft.page.elements.filter<DesignEditing.TextElement>(
(el) => el.type === "text"
);
textElements.forEach((el) => {
draft.page.elements.delete(el);
console.log(`Deleted text element at (${el.left}, ${el.top})`);
});
// Save the changes
console.log("All text elements have been deleted.");
return await draft.save();
});
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Delete text elements
</Button>
</Rows>
</div>
);
}
TSX