Reading elements

How to read the content of selected elements.

When a user selects certain type of elements, an app can detect the selection of those elements and read content from them. This unlocks a number of powerful features, such as tools for replacing elements.

For the time being, apps cannot arbitrarily read data from a design. They can only read certain types of content from the user's current selection.

Check out the Selection guidelines

Our design guidelines help you create a high-quality app that easily passes app review.

Apps can read the following types of content from selected elements:

  • Images, including raster images and vector images
  • Text, as plain text or rich text
  • Videos

Be aware that different types of elements may contain the same types of content. For example, both image elements and page backgrounds contain image content that can be read by an app.

In the future, we intend to support more types of content.

In the Developer Portal, enable the canva:design:content:read permission. In the future, the Apps SDK will throw an error if the required permissions are not enabled. To learn more, see Configuring permissions.

When a user selects one or more elements, Canva emits a selection event. This event contains information about the selected elements. You can listen for selection events with a React hook (recommended) or manually.

To listen for selection events with the React hook:

  1. Import the useSelection hook from the starter kit's utils directory:

    import { useSelection } from "utils/use_selection_hook";
    ts
  2. Call the hook, passing in a type of content as the only argument:

    const currentSelection = useSelection("plaintext");
    ts

    The argument must be one of the following values:

    • "image"
    • "plaintext"

    The hook returns the selection event.

You can then access the selection event via the currentSelection variable:

console.log(currentSelection);
ts

The selection event contains a count property that contains the number of selected elements. When an element isn't selected, this value is 0. You can use the count property to check if an element is currently selected:

const isElementSelected = currentSelection.count > 0;
ts

This is useful for updating the user interface in response to the user's selection — for example, by disabling a button if an element isn't selected:

<Button variant="primary" disabled={!isElementSelected}>
Click me
</Button>
tsx

The selection event has a read method that returns an object with a contents property. This property contains an array of objects. Each object represents an individual piece of content, such as the image used for a page background.

If the selection event contains image content and the change handler is scoped to "image", each object contains a ref property. This property contains a unique identifier that points to an asset in Canva's backend:

const draft = await currentSelection.read();
for (const content of draft.contents) {
console.log(content.ref);
}
ts

The value of the ref property is an opaque string. This means it's not intended to be read or manipulated. You can, however, convert the ref into a URL and then download the image data from that URL.

To convert the ref into a URL:

  1. Import the getTemporaryUrl method from the @canva/asset package:

    import { getTemporaryUrl } from "@canva/asset";
    ts
  2. Call the method, passing in the ref and the type of asset:

    const { url } = await getTemporaryUrl({
    type: "IMAGE",
    ref: content.ref,
    });
    console.log(url);
    ts

If the selection event contains plain text content, each object contains the plain text of the element:

const draft = await currentSelection.read();
for (const content of draft.contents) {
console.log(content.text);
}
ts

All formatting is stripped out, but the text may contain line breaks.

If the selection event contains rich text content, each object has a readTextRegions method:

const draft = await currentSelection.read();
for (const content of draft.contents) {
// Get the text with formatting information
const regions = await content.readTextRegions();
for (const region of regions) {
// The plain text content of a region
console.log(region.text);
// The formatting information of a region
console.log(region.formatting);
}
}
ts

When called, this method returns an array of objects. Each object represents a distinctly formatted string of text, also known as a text region. These regions contain the text itself and any formatting information.

For example, imagine the following text:

Hello world.

In this case, the word "world" is bold, while the rest of the text — the word "Hello" and the period at the end of the sentence — are not bold. This means the rich text would be represented as three separate regions:

[
{
text: "Hello ",
},
{
text: "world",
formatting: {
fontWeight: "bold";
}
},
{
text: ".",
},
];
ts

You can find the complete list of rich text formatting properties in the API reference.

If the selection event contains video content and the change handler is scoped to "video", each object contains a ref property. This property contains a unique identifier that points to an asset in Canva's backend:

const draft = await currentSelection.read();
for (const content of draft.contents) {
console.log(content.ref);
}
ts

The value of the ref property is an opaque string. This means it's not intended to be read or manipulated. You can, however, convert the ref into a URL and then download the video data from that URL.

To convert the ref into a URL:

  1. Import the getTemporaryUrl method from the @canva/asset package:

    import { getTemporaryUrl } from "@canva/asset";
    ts
  2. Call the method, passing in the ref and the type of asset:

    const { url } = await getTemporaryUrl({
    type: "VIDEO",
    ref: content.ref,
    });
    console.log(url);
    ts
import React from "react";
import { Button } from "@canva/app-ui-kit";
import { useSelection } from "utils/use_selection_hook";
import { getTemporaryUrl } from "@canva/asset";
import styles from "styles/components.css";
export function App() {
const currentSelection = useSelection("image");
const isElementSelected = currentSelection.count > 0;
async function handleClick() {
if (!isElementSelected) {
return;
}
const draft = await currentSelection.read();
for (const content of draft.contents) {
const { url } = await getTemporaryUrl({
type: "IMAGE",
ref: content.ref,
});
console.log(url);
}
}
return (
<div className={styles.scrollContainer}>
<Button
variant="primary"
disabled={!isElementSelected}
onClick={handleClick}
>
Read selected image content
</Button>
</div>
);
}
tsx
import React from "react";
import { Button } from "@canva/app-ui-kit";
import { useSelection } from "utils/use_selection_hook";
import styles from "styles/components.css";
export function App() {
const currentSelection = useSelection("plaintext");
const isElementSelected = currentSelection.count > 0;
async function handleClick() {
if (!isElementSelected) {
return;
}
const draft = await currentSelection.read();
for (const content of draft.contents) {
console.log(content.text);
}
}
return (
<div className={styles.scrollContainer}>
<Button
variant="primary"
disabled={!isElementSelected}
onClick={handleClick}
>
Read selected plain text content
</Button>
</div>
);
}
tsx
import React from "react";
import { Button } from "@canva/app-ui-kit";
import { selection, SelectionEvent } from "@canva/preview/design";
import styles from "styles/components.css";
export function App() {
const [currentSelection, setCurrentSelection] = React.useState<
SelectionEvent<"richtext"> | undefined
>();
const isElementSelected = (currentSelection?.count ?? 0) > 0;
React.useEffect(() => {
return selection.registerOnChange({
scope: "richtext",
onChange: setCurrentSelection,
});
}, []);
async function handleClick() {
if (!isElementSelected || !currentSelection) {
return;
}
const draft = await currentSelection.read();
for (const content of draft.contents) {
// Get the text with formatting information
const regions = await content.readTextRegions();
for (const region of regions) {
// The plain text content of a region
console.log(region.text);
// The formatting information of a region
console.log(region.formatting);
}
}
}
return (
<div className={styles.scrollContainer}>
<Button
variant="primary"
disabled={!isElementSelected}
onClick={handleClick}
stretch
>
Read selected rich text content
</Button>
</div>
);
}
tsx
import React from "react";
import { Button } from "@canva/app-ui-kit";
import { selection, SelectionEvent } from "@canva/preview/design";
import { getTemporaryUrl } from "@canva/asset";
import styles from "styles/components.css";
export function App() {
const [currentSelection, setCurrentSelection] = React.useState<
SelectionEvent<"video"> | undefined
>();
const isElementSelected = (currentSelection?.count ?? 0) > 0;
React.useEffect(() => {
return selection.registerOnChange({
scope: "video",
onChange: setCurrentSelection,
});
}, []);
async function handleClick() {
if (!isElementSelected || !currentSelection) {
return;
}
const draft = await currentSelection.read();
for (const content of draft.contents) {
const { url } = await getTemporaryUrl({
type: "VIDEO",
ref: content.ref,
});
console.log(url);
}
}
return (
<div className={styles.scrollContainer}>
<Button
variant="primary"
disabled={!isElementSelected}
onClick={handleClick}
>
Read selected video content
</Button>
</div>
);
}
tsx