Design Editing

How to read and update elements in a design.

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.

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

npm install @canva/design@beta
bash

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.

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.

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.

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.

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

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

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.

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
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
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
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
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
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