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.

Reading elements

How to read the content of selected elements.

When a user selects certain types 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.

Supported content types

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

  • Raster images
  • Text, as plaintext or richtext
  • 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.

How to read elements

Step 1: Enable the required permissions

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.

Step 2: Listen for selection events

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.

For the time being, the React hook is not compatible with richtext. You must register the callback manually with methods and types from the @canva/design package.

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"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_selection_hook.ts
    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"
    • "video"

    The hook returns the selection event.

    Under the hood, the selection.registerOnChange method registers a callback that runs when a user selects an element or immediately if an element is already selected.

To listen for selection events without the React hook:

  1. Import the following type:

    // Images, plaintext, and videos
    import type { SelectionEvent } from "@canva/design";
    // Richtext
    import type { SelectionEvent } from "@canva/design";
    TS

    This is a generic data type that accepts the type of content as its only type parameter.

    The parameter must be one of the following values:

    • "image"
    • "plaintext"
    • "richtext"
    • "video"

    For example:

    SelectionEvent<"image">;
    SelectionEvent<"plaintext">;
    SelectionEvent<"richtext">;
    SelectionEvent<"video">;
    TS
  2. Import the selection client:

    // Images, plaintext, and video
    import { selection } from "@canva/design";
    // Richtext
    import { selection } from "@canva/design";
    TS

    This client contains the methods for interacting with selections.

  3. Create a useState hook for keeping track of the current selection:

    const [currentSelection, setCurrentSelection] =
    React.useState<SelectionEvent<"image">>();
    TS
  4. Use the selection.registerOnChange method to register a callback that stores the selection event in state:

    React.useEffect(() => {
    return selection.registerOnChange({
    scope: "image",
    onChange: (event) => {
    setCurrentSelection(event);
    },
    });
    }, []);
    TS

    You must:

    • Call this method within a useEffect hook to ensure that the callback is only registered once.
    • Return the result of the method to dispose of the callback when the component unmounts.

    The callback itself accepts two properties:

    • scope - The type of selection events to listen for, which can be either "plaintext" or "image".
    • onChange - A callback that receives the selection event.

    If an element is already selected, the selection.registerOnChange callback runs immediately.

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

console.log(currentSelection);
TS

Step 3: Check if an element is selected

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

Step 4: Read the contents of the element

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.

Images

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.

The URL always points to the full-size version of the image, even if the image is cropped or exists in a frame(opens in a new tab or window). This is the intended behavior as the user can always change the crop or frame.

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

    The returned URL is temporary and expires after a short period of time. Your app should immediately download the image to ensure that it has ongoing access to it.

Plaintext

If the selection event contains plaintext content, each object contains the plaintext 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.

Richtext

If the selection event contains richtext content, each object is a richtext range that exposes a variety of methods for interacting with that particular portion of richtext:

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

To learn more, see Richtext ranges.

Videos

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.

The URL always points to the full-size version of the video, even if the video is cropped or exists in a frame(opens in a new tab or window). This is the intended behavior as the user can always change the crop or frame.

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

    The returned URL is temporary and expires after a short period of time. Your app should immediately download the video to ensure that it has ongoing access to it.

Gotchas

  • Apps can only read the selection of raster images, not vector images.

API reference

Code sample

Image

import React from "react";
import { Button } from "@canva/app-ui-kit";
import { useSelection } from "utils/use_selection_hook"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_selection_hook.ts
import { getTemporaryUrl } from "@canva/asset";
import * as 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

Plaintext

import React from "react";
import { Button } from "@canva/app-ui-kit";
import { useSelection } from "utils/use_selection_hook"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_selection_hook.ts
import * as 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 plaintext content
</Button>
</div>
);
}
TSX

Richtext

import React from "react";
import { Button } from "@canva/app-ui-kit";
import { selection, SelectionEvent } from "@canva/design";
import * as 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 = content.readTextRegions();
for (const region of regions) {
// The plaintext 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 richtext content
</Button>
</div>
);
}
TSX

Video

import React from "react";
import { Button } from "@canva/app-ui-kit";
import { useSelection } from "utils/use_selection_hook"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_selection_hook.ts
import { getTemporaryUrl } from "@canva/asset";
import * as styles from "styles/components.css";
export function App() {
const currentSelection = useSelection("video");
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: "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