Querying content

Reading and updating content in a user's design.

Apps can query the content in a user's design, similar to how the HTML DOM can be queried. They can then read and update the queried content, which unlocks a range of powerful use-cases.

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

npm install @canva/design@beta
bash

There are different types of content that may appear in a design, including:

For the time being, richtext is the only content type that's compatible with querying.

In the future, other content types will be supported.

When an app queries a design, it must specify a context. This determines what part of the design to query. For the time being, the only supported context is the current page. In the future, other contexts will be supported.

Apps can register a callback for any combination of the supported content types and contexts:

import { readContent } from "@canva/design";
// Read richtext content on current page
await readContent(
{
contentType: "richtext",
context: "current_page",
},
async (draft) => {
console.log(draft);
}
);
tsx

This callback receives a snapshot of the queried content, known as the draft.

A single design may contain multiple pieces of content and each piece of queried content exists within this draft. You'll typically want to loop through the individual content items:

import { readContent } from "@canva/design";
// Read richtext content on current page
await readContent(
{
contentType: "richtext",
context: "current_page",
},
async (draft) => {
// Loop through content items
for (const content of draft.contents) {
console.log(content);
}
}
);
tsx

Each content item exposes methods and properties for reading and updating the content. The available methods and properties depend on the type of content.

In the case of richtext content, each item is a richtext range:

import { readContent } from "@canva/design";
// Read richtext content on current page
await readContent(
{
contentType: "richtext",
context: "current_page",
},
async (draft) => {
// Loop through content items
for (const content of draft.contents) {
// Each content item is a richtext range
const regions = content.readTextRegions();
console.log(regions);
}
}
);
tsx

To learn more, see Richtext ranges.

Each content item exposes methods and properties that can be used to update the queried content:

import { readContent } from "@canva/design";
await readContent(
{
contentType: "richtext",
context: "current_page",
},
async (draft) => {
// Loop through each content item
for (const content of draft.contents) {
// Get the richtext content as plaintext
const plaintext = content.readPlaintext();
// Format the richtext
content.formatParagraph(
{ index: 0, length: plaintext.length },
{ fontWeight: "bold" }
);
}
}
);
tsx

But before a change is reflected in the user's design, the draft must be synced:

import { readContent } from "@canva/design";
await readContent(
{
contentType: "richtext",
context: "current_page",
},
async (draft) => {
// Loop through each content item
for (const content of draft.contents) {
// Get the richtext content as plaintext
const plaintext = content.readPlaintext();
// Format the richtext
content.formatParagraph(
{ index: 0, length: plaintext.length },
{ fontWeight: "bold" }
);
}
// Sync the content so that it's reflected in the design
await draft.sync();
}
);
tsx

Syncing a draft:

  • Commits the changes to the user's design.
  • Updates the draft so that it contains the modified content.

Any changes that are not synced will be discarded.

If queried content is changed after the readContent method is called but before the draft is synced, it can lead to conflicts between the state of the draft and the true state of the design.

Canva will attempt to resolve conflicts, but we can't make any guarantees about how conflicts will be resolved. To minimize the risk of unexpected behavior, we recommend syncing the draft as often as possible. This will reduce the size and complexity of conflicts, leading to more predictable behavior.

There's no limit to how many times a draft can be synced.

If a queried content item is deleted after the readContent method is called, the deleted content will continue to exist within the draft, even after the draft is synced.

To identify deleted content, each content item has a deleted property that is either true or false. You can use this property to avoid operating on content that no longer exists in the user's design:

import { readContent } from "@canva/design";
// Read richtext content on current page
await readContent(
{
contentType: "richtext",
context: "current_page",
},
async (draft) => {
// Loop through the individual content items
for (const content of draft.contents) {
// Ignore deleted content
if (content.deleted) {
continue;
}
// Get the richtext content as plaintext
const plaintext = content.readPlaintext();
// Format the richtext
content.formatParagraph(
{ index: 0, length: plaintext.length },
{ fontWeight: "bold" }
);
}
// Sync the content so that it's reflected in the design
await draft.sync();
}
);
tsx
  • You can't use querying to read or update the position of elements on a page. You can only use querying to read and update the content of a design, not its structure.
  • In a content draft, the order of the content items is not guaranteed. This means an app shouldn't rely on the order of the items being consistent or predictable.
  • You can't call the readContent method from within the callback of another readContent method — that is, the calls can't be nested. If you try to do this, an error will be thrown.
import { Button, Rows } from "@canva/app-ui-kit";
import { readContent } from "@canva/design";
import styles from "styles/components.css";
export function App() {
async function handleClick() {
// Read richtext content on current page
await readContent(
{
contentType: "richtext",
context: "current_page",
},
async (draft) => {
// Loop through each content item
for (const content of draft.contents) {
// Get the richtext content as plaintext
const plaintext = content.readPlaintext();
// Format the richtext
content.formatParagraph(
{ index: 0, length: plaintext.length },
{ fontWeight: "bold" }
);
}
// Sync the content so that it's reflected in the design
await draft.sync();
}
);
}
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Button variant="primary" onClick={handleClick}>
Update richtext content
</Button>
</Rows>
</div>
);
}
tsx