Skip to content

Design Elements

Positioning Elements

How to arrange elements in a user's design.

By default, adding an element to the user's design positions the element in the center of the page and scales it to a size that's determined by Canva.

Likewise, elements inside groups and app elements have default positions and scaling.

Apps can, however, override these values to set the position of elements in either context.

Feature support considerations

Apps can position elements in responsive and fixed Canva design types depending on the method:

  • addElementAtPoint only works in fixed design types, such as Presentations.
  • addElementAtCursor only works in responsive design types, such as Canva docs.
  • getCurrentPageContext only works in fixed design types.

See Feature support for more information.

How to position elements on a page

The syntax for positioning an element depends on the method that renders the element.

When calling the addElementAtPoint method (or addElementAtCursor, depending on the current context), pass the position in with the first argument:

ts
await addElementAtPoint({
  type: "embed",
  url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",

  // Position
  width: 640,
  height: 360,
  top: 0,
  left: 0,
  rotation: 0,
});

When calling the addOrUpdateElement method, pass the position in as the second argument:

ts
appElement.addOrUpdateElement(
  {
    // app element data goes here
  },
  {
    // Position
    width: 640,
    height: 360,
    top: 0,
    left: 0,
    rotation: 0,
  }
);

In both cases, the following properties must be used together:

  • width
  • height
  • top
  • left

The rotation property is always optional, but only has an effect if the positional properties are set.

Using the dimensions of the current page

  1. In the Developer Portal, enable the canva:design:content:read scope. In the future, the Apps SDK will throw an error if the required scopes aren't enabled. To learn more, see Configuring scopes.

  2. Call the getCurrentPageContext method:

    ts
    import { getCurrentPageContext } from "@canva/design";
    const context = await getCurrentPageContext();
    console.log(context.dimensions); // => { width: 1280, height: 720 }

    The method returns a dimensions property that you can use to:

    • Position elements within the bounds of the current page.
    • Position elements in relative terms — for example, in the bottom-right corner.

Warning: Some types of designs, such as whiteboards, don't have dimensions. In these cases, the dimensions property will be undefined.

How to position elements in groups and app elements

You can set the position of elements inside groups and app elements, and since app elements are essentially groups with extra features, the approach to positioning elements is the same.

Box model

You can think of an app element and a group as a box.

By default, the box doesn't have a width or height. Instead, the size of the box is based on the size and spacing between the elements it contains.

Dimensions

The elements inside a box must have a width and a height. (The one exception is text elements, which can't have a predefined height.)

When the width is defined as a number, the height can be set to "auto" (and vice versa). At runtime, Canva replaces "auto" with a value that maintains the aspect ratio of the element.

Warning: You can't set both the width and height to "auto". At least one value must be a number.

Coordinates

Within a box, elements can be positioned with top and left coordinates. These coordinates are relative to the box's origin — that is, the top-left corner of the box.

The origin is determined by the box's topmost and leftmost elements. For example, if the topmost element has a top position of 50 and the leftmost element has a left position of 100, then:

  • The top and left coordinates of the box start from 50 and 100
  • The top and left coordinates of all other elements are relative to 50 and 100

To illustrate, here's a diagram:

This can be confusing — especially if you're using negative values for positions — so we recommend treating the top and left coordinates as 0 and 0, even though this isn't strictly required.

Spacing

Elements can be positioned to overlap or to have spacing between them. The size of the box grows or shrinks to accommodate for the combined size of the elements.

When elements overlap, the ordering of the elements is determined by the order in which the elements are rendered. The elements later in an array are rendered in front of elements earlier in the array.

Padding

Boxes do not have padding. There's always one element positioned against each edge of the box.

Units of measurement

Within a box, dimensions and coordinates are defined with relative units, not absolute units. This means 200 is always twice as large as 100, but these numbers don't correspond to pixel values.

The end result is that, if a design is 1920 pixels × 1280 pixels, setting the dimensions of a box's elements to a combined size of 1920 pixels × 1280 pixels will not necessarily result in the box extending to the edges of the design. Instead, the box will be scaled to a size that's calculated by Canva.

How Canva determines the absolute size of elements depends on a variety of factors that may change over time and is beyond the scope of this documentation.

The key takeaway is to never treat dimensions or coordinates within a box as absolute values.

API reference

  • addElementAtPoint
  • getCurrentPageContext
  • initAppElement

Code samples

Positioning app elements

tsx
import { Button, Rows } from "@canva/app-ui-kit";
import { getCurrentPageContext, initAppElement } from "@canva/design";
import * as styles from "styles/components.css";
import { useFeatureSupport } from "@canva/app-hooks";

const appElement = initAppElement({
  render: () => {
    return [
      {
        type: "embed",
        url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        width: 640,
        height: 360,
        top: 0,
        left: 0,
      },
    ];
  },
});

export function App() {

  const isSupported = useFeatureSupport();

  async function handleClick() {
    if (!isSupported(getCurrentPageContext)) {
     const context = await getCurrentPageContext();

     if (!context.dimensions) {
       console.warn("The current design does not have dimensions");
       return;
     }

     const width = 640;
     const height = 360;
     const top = context.dimensions.height - height;
     const left = context.dimensions.width - width;

     await appElement.addOrUpdateElement(
       {
         // app element data goes here
       },
       {
         width,
         height,
         top,
         left,
       }
     );
   }
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="2u">
        <Button
          variant="primary"
          onClick={handleClick}
          disabled={!isSupported(getCurrentPageContext)}
          >
          Create positioned app element
        </Button>
      </Rows>
    </div>
  );
}

Positioning native elements

tsx
import { Button, Rows } from "@canva/app-ui-kit";
import { addElementAtPoint, addElementAtCursor, getCurrentPageContext } from "@canva/design";
import { useFeatureSupport } from "@canva/app-hooks";
import * as styles from "styles/components.css";

export function App() {

  const isSupported = useFeatureSupport();

  async function handleClick() {
    if (!isSupported(getCurrentPageContext)) {
     const context = await getCurrentPageContext();

     if (!context.dimension) {
       console.warn("The current design does not have dimensions");
       return;
     }

     const width = 640;
     const height = 360;
     const top = context.dimensions.height - height;
     const left = context.dimensions.width - width;

     if (isSupported(addElementAtPoint)) {
       await addElementAtPoint({
         type: "embed",
         url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
         width,
         height,
         top,
         left,
       });
     }
   }
  }

   async function handleAddElementAtCursor() {
    if (!isSupported(addElementAtPoint)) { // Check the design type and use addElementAtCursor if addElementAtPoint isn't supported
      await addElementAtCursor({
        type: "text",
        children: ["Adding content at Cursor."]
      })
    }
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="2u">
        <Button
          variant="primary"
          onClick={handleClick}
          disabled={!isSupported(addElementAtPoint)}
          >
          Create positioned native element
        </Button>
        <Button
          variant="secondary"
          onClick={handleAddElementAtCursor}
          disabled={!isSupported(addElementAtCursor)}
          >
          Create native element at cursor
        </Button>
      </Rows>
    </div>
  );
}

Positioning elements in groups and app elements

tsx
import { addElementAtPoint } from "@canva/design";
import { useFeatureSupport } from "@canva/app-hooks";
import { Button, Rows } from "@canva/app-ui-kit";
import * as styles from "styles/components.css";

export function App() {
  const isSupported = useFeatureSupport();
  async function handleClick() {
    if (isSupported(addElementAtPoint)) {
      await addElementAtPoint({
        type: "group",
        children: [
          {
            type: "embed",
            url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
            width: 100,
            height: 100,
            top: 0,
            left: 0,
          },
          {
            type: "embed",
            url: "https://www.youtube.com/watch?v=o-YBDTqX_ZU",
            width: 100,
            height: 100,
            top: 0,
            left: 100,
          },
        ],
      });
    }
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="2u">
        <Button
          variant="primary"
          onClick={handleClick}
          disabled={!isSupported(addElementAtPoint)}
          >
          Add group element
        </Button>
      </Rows>
    </div>
  );
}

Supporting Drag and Drop

How apps can support drag and drop.

Something that Canva users love is the ability to drag and drop content straight into their designs. Apps can use the Apps SDK to support this behavior, ensuring that the user experience is delightfully consistent.

Content types

Apps can enable drag and drop for various types of content, including:

  • Audio tracks
  • Embeds
  • Images
  • Text
  • Videos

Additional content types may be supported in the future.

Feature support considerations

Drag and drop methods are supported in responsive and fixed Canva design types depending on the method:

  • startDragToCursor only works in fixed design types, such as Presentations.
  • startDragToPoint only works in responsive design types, such as Docs.

To learn more, see Feature support.

Rendering the UI

Apps need to render something in their UI that can be dragged.

This can be as simple as an HTMLDivElement with a draggable attribute:

tsx
<div draggable>This text can be dragged.</div>

In the App UI Kit though, we've also provided a number of card components that are designed to work seamlessly with drag and drop. The components include:

  • AudioCard
  • EmbedCard
  • ImageCard
  • TypographyCard
  • VideoCard

To see how to use these components, see Code samples.

Handling drag events

Apps can handle drag events by registering an onDragStart callback and passing the drag event into either of the following methods:

  • startDragToPoint
  • startDragToCursor

Both methods add content to the user's design, but:

  • startDragToPoint is only compatible with design types that support absolute positions, which is all design types except for documents.
  • startDragToCursor is only compatible with design types that contain streams of text, which is only the document design type.

The supported content types also depend on the method being called:

Content TypestartDragToPointstartDragToCursor
Audio tracks
Embeds
Images
Text
Videos

Where possible, apps should determine the context in which the app is running and either call the compatible method or make it obvious when functionality isn't available. To learn more, see Feature support.

Handling clicks

For accessibility reasons, drag and drop should not be the only way that users can add content to a design. Apps should also support click events. This behavior is built into the App UI Kit components and demonstrated below.

Code samples

<Tabs> <Tab name="Audio"> ```tsx import React from "react"; import { AudioCard, AudioContextProvider, Rows } from "@canva/app-ui-kit"; import { upload } from "@canva/asset"; import { addAudioTrack, ui } from "@canva/design"; import { useFeatureSupport } from "@canva/app-hooks"; import * as styles from "styles/components.css";

export function App() {
  const isSupported = useFeatureSupport();

  async function handleClick() {
    if (isSupported(addAudioTrack)) {
      const asset = await upload({
        type: "audio",
        title: "Example audio",
        mimeType: "audio/mp3",
        url: "https://www.canva.dev/example-assets/audio-import/audio.mp3",
        aiDisclosure: "none",
      });

      addAudioTrack({
        ref: asset.ref,
      });
    }
  }

  function handleDragStart(event: React.DragEvent<HTMLElement>) {
    if (isSupported(ui.startDragToPoint)) {
      ui.startDragToPoint(event, {
        type: "audio",
        title: "Example audio",
        resolveAudioRef: () => {
          return upload({
            type: "audio",
            title: "Example audio",
            mimeType: "audio/mp3",
            url: "https://www.canva.dev/example-assets/audio-import/audio.mp3",
            aiDisclosure: "none",
          });
        },
      });
    }

    if (isSupported(ui.startDragToCursor)) {
      ui.startDragToCursor(event, {
        type: "audio",
        title: "Example audio",
        resolveAudioRef: () => {
          return upload({
            type: "audio",
            title: "Example audio",
            mimeType: "audio/mp3",
            url: "https://www.canva.dev/example-assets/audio-import/audio.mp3",
            aiDisclosure: "none",
          });
        },
      });
    }
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="1u">
        {/* A single `AudioContextProvider` component must be an ancestor to all `AudioCard` components*/}
        <AudioContextProvider>
          <AudioCard
            title="Example audio"
            audioPreviewUrl="https://www.canva.dev/example-assets/audio-import/audio.mp3"
            durationInSeconds={86}
            onClick={handleClick}
            onDragStart={handleDragStart}
          />
        </AudioContextProvider>
      </Rows>
    </div>
  );
}
```

</Tab>

<Tab name="Embeds"> ```tsx import React from "react"; import { EmbedCard, Rows } from "@canva/app-ui-kit"; import { addElementAtCursor, addElementAtPoint, ui } from "@canva/design"; import { useFeatureSupport } from "@canva/app-hooks"; import * as styles from "styles/components.css";

export function App() {
  const isSupported = useFeatureSupport();

  function handleClick() {
    if (isSupported(addElementAtPoint)) {
      addElementAtPoint({
        type: "embed",
        url: "https://www.youtube.com/embed/L3MtFGWRXAA",
      });
    }

    if (isSupported(addElementAtCursor)) {
      addElementAtCursor({
        type: "embed",
        url: "https://www.youtube.com/embed/L3MtFGWRXAA",
      });
    }
  }

  function handleDragStart(event: React.DragEvent<HTMLElement>) {
    if (isSupported(ui.startDragToPoint)) {
      ui.startDragToPoint(event, {
        type: "embed",
        embedUrl: "https://www.youtube.com/embed/L3MtFGWRXAA",
        previewSize: { width: 300, height: 200 },
        previewUrl: "https://www.canva.dev/example-assets/images/puppyhood.jpg",
      });
    }

    if (isSupported(ui.startDragToCursor)) {
      ui.startDragToCursor(event, {
        type: "embed",
        embedUrl: "https://www.youtube.com/embed/L3MtFGWRXAA",
        previewSize: { width: 300, height: 200 },
        previewUrl: "https://www.canva.dev/example-assets/images/puppyhood.jpg",
      });
    }
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="1u">
        <EmbedCard
          title="Heartwarming Chatter: Adorable Conversation with a Puppy"
          description="Puppyhood"
          ariaLabel="Add embed to design"
          thumbnailUrl="https://www.canva.dev/example-assets/images/puppyhood.jpg"
          onClick={handleClick}
          onDragStart={handleDragStart}
        />
      </Rows>
    </div>
  );
}
```

</Tab>

<Tab name="Images"> ```tsx import React from "react"; import { ImageCard, Rows } from "@canva/app-ui-kit"; import { upload } from "@canva/asset"; import { addElementAtCursor, addElementAtPoint, ui } from "@canva/design"; import { useFeatureSupport } from "@canva/app-hooks"; import * as styles from "styles/components.css";

export function App() {
  const isSupported = useFeatureSupport();

  async function handleClick() {
    if (isSupported(addElementAtPoint)) {
      const asset = await upload({
        mimeType: "image/jpeg",
        thumbnailUrl:
          "https://www.canva.dev/example-assets/image-import/grass-image-thumbnail.jpg",
        type: "image",
        url: "https://www.canva.dev/example-assets/image-import/grass-image.jpg",
        width: 320,
        height: 212,
        altText: {
          text: "Example grass image",
          decorative: false,
        },
        aiDisclosure: "none",
      });

      addElementAtPoint({
        type: "image",
        ref: asset.ref,
        altText: {
          text: "Example grass image",
          decorative: false,
        },
      });
    }

    if (isSupported(addElementAtCursor)) {
      const asset = await upload({
        mimeType: "image/jpeg",
        thumbnailUrl:
          "https://www.canva.dev/example-assets/image-import/grass-image-thumbnail.jpg",
        type: "image",
        url: "https://www.canva.dev/example-assets/image-import/grass-image.jpg",
        width: 320,
        height: 212,
        altText: {
          text: "Example grass image",
          decorative: false,
        },
        aiDisclosure: "none",
      });

      addElementAtCursor({
        type: "image",
        ref: asset.ref,
        altText: {
          text: "Example grass image",
          decorative: false,
        },
      });
    }
  }

  function handleDragStart(event: React.DragEvent<HTMLElement>) {
    if (isSupported(ui.startDragToPoint)) {
      ui.startDragToPoint(event, {
        type: "image",
        resolveImageRef: () => {
          return upload({
            mimeType: "image/jpeg",
            thumbnailUrl:
              "https://www.canva.dev/example-assets/image-import/grass-image-thumbnail.jpg",
            type: "image",
            url: "https://www.canva.dev/example-assets/image-import/grass-image.jpg",
            width: 320,
            height: 212,
            altText: {
              text: "Example grass image",
              decorative: false,
            },
            aiDisclosure: "none",
          });
        },
        previewUrl:
          "https://www.canva.dev/example-assets/image-import/grass-image.jpg",
        previewSize: {
          width: 320,
          height: 212,
        },
        fullSize: {
          width: 320,
          height: 212,
        },
        altText: {
          text: "Example grass image",
          decorative: false,
        },
      });
    }

    if (isSupported(ui.startDragToCursor)) {
      ui.startDragToCursor(event, {
        type: "image",
        resolveImageRef: () => {
          return upload({
            mimeType: "image/jpeg",
            thumbnailUrl:
              "https://www.canva.dev/example-assets/image-import/grass-image-thumbnail.jpg",
            type: "image",
            url: "https://www.canva.dev/example-assets/image-import/grass-image.jpg",
            width: 320,
            height: 212,
            altText: {
              text: "Example grass image",
              decorative: false,
            },
            aiDisclosure: "none",
          });
        },
        previewUrl:
          "https://www.canva.dev/example-assets/image-import/grass-image.jpg",
        previewSize: {
          width: 320,
          height: 212,
        },
        fullSize: {
          width: 320,
          height: 212,
        },
        altText: {
          text: "Example grass image",
          decorative: false,
        },
      });
    }
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="1u">
        <ImageCard
          ariaLabel="Add image to design"
          alt="Grass image"
          thumbnailUrl="https://www.canva.dev/example-assets/image-import/grass-image-thumbnail.jpg"
          onDragStart={handleDragStart}
          onClick={handleClick}
        />
      </Rows>
    </div>
  );
}
```

</Tab>

<Tab name="Text"> ```tsx import React from "react"; import { Rows, Text, TypographyCard } from "@canva/app-ui-kit"; import { addElementAtCursor, addElementAtPoint, ui } from "@canva/design"; import { useFeatureSupport } from "@canva/app-hooks"; import * as styles from "styles/components.css";

export function App() {
  const isSupported = useFeatureSupport();

  function handleClick() {
    if (isSupported(addElementAtPoint)) {
      addElementAtPoint({
        type: "text",
        children: ["This is some text"],
      });
    }

    if (isSupported(addElementAtCursor)) {
      addElementAtCursor({
        type: "text",
        children: ["This is some text"],
      });
    }
  }

  function handleDragStart(event: React.DragEvent<HTMLElement>) {
    if (isSupported(ui.startDragToPoint)) {
      ui.startDragToPoint(event, {
        type: "text",
        children: ["This is some text"],
      });
    }

    if (isSupported(ui.startDragToCursor)) {
      ui.startDragToCursor(event, {
        type: "text",
        children: ["This is some text"],
      });
    }
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="1u">
        <TypographyCard
          ariaLabel="Hello world"
          onClick={handleClick}
          onDragStart={handleDragStart}
        >
          <Text>This is some text</Text>
        </TypographyCard>
      </Rows>
    </div>
  );
}
```

</Tab>

<Tab name="Videos"> ```tsx import React from "react"; import { Rows, VideoCard } from "@canva/app-ui-kit"; import { upload } from "@canva/asset"; import { addElementAtCursor, addElementAtPoint, ui } from "@canva/design"; import { useFeatureSupport } from "@canva/app-hooks"; import * as styles from "styles/components.css";

export function App() {
  const isSupported = useFeatureSupport();

  async function handleClick() {
    if (isSupported(addElementAtPoint)) {
      const asset = await upload({
        mimeType: "video/mp4",
        thumbnailImageUrl:
          "https://www.canva.dev/example-assets/video-import/beach-thumbnail-image.jpg",
        thumbnailVideoUrl:
          "https://www.canva.dev/example-assets/video-import/beach-thumbnail-video.mp4",
        type: "video",
        url: "https://www.canva.dev/example-assets/video-import/beach-video.mp4",
        width: 320,
        height: 180,
        aiDisclosure: "none",
      });

      addElementAtPoint({
        type: "video",
        ref: asset.ref,
        altText: {
          text: "Example video",
          decorative: false,
        },
      });
    }

    if (isSupported(addElementAtCursor)) {
      const asset = await upload({
        mimeType: "video/mp4",
        thumbnailImageUrl:
          "https://www.canva.dev/example-assets/video-import/beach-thumbnail-image.jpg",
        thumbnailVideoUrl:
          "https://www.canva.dev/example-assets/video-import/beach-thumbnail-video.mp4",
        type: "video",
        url: "https://www.canva.dev/example-assets/video-import/beach-video.mp4",
        width: 320,
        height: 180,
        aiDisclosure: "none",
      });

      addElementAtCursor({
        type: "video",
        ref: asset.ref,
        altText: {
          text: "Example video",
          decorative: false,
        },
      });
    }
  }

  function handleDragStart(event: React.DragEvent<HTMLElement>) {
    if (isSupported(ui.startDragToPoint)) {
      ui.startDragToPoint(event, {
        type: "video",
        resolveVideoRef: () => {
          return upload({
            mimeType: "video/mp4",
            thumbnailImageUrl:
              "https://www.canva.dev/example-assets/video-import/beach-thumbnail-image.jpg",
            thumbnailVideoUrl:
              "https://www.canva.dev/example-assets/video-import/beach-thumbnail-video.mp4",
            type: "video",
            url: "https://www.canva.dev/example-assets/video-import/beach-video.mp4",
            width: 320,
            height: 180,
            aiDisclosure: "none",
          });
        },
        previewSize: {
          width: 320,
          height: 180,
        },
        previewUrl:
          "https://www.canva.dev/example-assets/video-import/beach-thumbnail-image.jpg",
        altText: {
          text: "Example video",
          decorative: false,
        },
      });
    }

    if (isSupported(ui.startDragToCursor)) {
      ui.startDragToCursor(event, {
        type: "video",
        resolveVideoRef: () => {
          return upload({
            mimeType: "video/mp4",
            thumbnailImageUrl:
              "https://www.canva.dev/example-assets/video-import/beach-thumbnail-image.jpg",
            thumbnailVideoUrl:
              "https://www.canva.dev/example-assets/video-import/beach-thumbnail-video.mp4",
            type: "video",
            url: "https://www.canva.dev/example-assets/video-import/beach-video.mp4",
            width: 320,
            height: 180,
            aiDisclosure: "none",
          });
        },
        previewSize: {
          width: 320,
          height: 180,
        },
        previewUrl:
          "https://www.canva.dev/example-assets/video-import/beach-thumbnail-image.jpg",
        altText: {
          text: "Example video",
          decorative: false,
        },
      });
    }
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="1u">
        <VideoCard
          ariaLabel="Add video to design"
          thumbnailUrl="https://www.canva.dev/example-assets/video-import/beach-thumbnail-image.jpg"
          videoPreviewUrl="https://www.canva.dev/example-assets/video-import/beach-thumbnail-video.mp4"
          durationInSeconds={7}
          mimeType="video/mp4"
          onDragStart={handleDragStart}
          onClick={handleClick}
        />
      </Rows>
    </div>
  );
}
```

</Tab> </Tabs>

Selection

Read, transform, and write the user's current selection.

The Selection API allows apps to read and replace the content of the user's current selection. This enables powerful features like image effects and text manipulation.

Supported content types

Apps can read and replace the following types of content:

  • Images (not including SVGs)
  • Plaintext
  • Richtext
  • Videos

Be aware that different design ingredients 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. For now, if you are looking for guides on how to select and manage app elements, see Creating app elements.

Scopes

The Selection API requires different scopes depending on how your app uses the API:

  • If your app only reads content, enable canva:design:content:read.
  • If your app replaces plaintext or richtext content, enable:
    • canva:design:content:read
    • canva:design:content:write
  • If your app replaces image or video content, enable:
    • canva:design:content:read
    • canva:design:content:write
    • canva:asset:private:read
    • canva:asset:private:write

The Apps SDK will throw an error if the required scopes aren't enabled.

To learn more, see Configuring scopes.

Listening for selection events

When a user selects one or more pieces of content, Canva emits a selection event. This event contains information about the selected content.

To listen for selection events:

  1. Import the useSelection hook from the @canva/app-hooks package:

    ts
    import { useSelection } from "@canva/app-hooks";
  2. Call the hook, passing in the type of content to detect the selection of:

    ts
    const selectedContent = useSelection("plaintext");

    The following values are supported:

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

    The hook runs:

    • when a user selects one or more pieces of content
    • immediately, if a piece of content is already selected

The result of calling the hook is the selection event:

ts
console.log(selectedContent);

Checking if content is selected

The selection event contains a count property that contains the number of selected content items. When content isn't selected, this value is 0. You can use this behavior to detect if content is selected:

ts
const isContentSelected = selectedContent.count > 0;

This is useful for updating the user interface in response to the user's selection, such as by only enabling a button when content is selected:

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

Reading selected content

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. The available properties in this object depends on the type of content.

Images

If the selection event contains image content, each object contains an asset reference that points to an asset in Canva's backend:

ts
const draft = await selectedContent.read();

for (const content of draft.contents) {
  console.log(content.ref);
}

You can use the getTemporaryUrl method to get the URL of the underlying image file:

ts
import { getTemporaryUrl } from "@canva/asset";

const { url } = await getTemporaryUrl({
  type: "image",
  ref: content.ref,
});

console.log(url);

Keep in mind that:

  • The URL always points to the full-size version of the image, even if the image is cropped or exists in a frame. This is the intended behavior as the user can always change the crop or frame.
  • 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:

ts
const draft = await selectedContent.read();

for (const content of draft.contents) {
  console.log(content.text);
}

All formatting is stripped out, but the text may contain line breaks in the form \n.

Richtext

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

tsx
const draft = await selectedContent.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);
  }
}

NOTE: Richtext selection isn't currently supported on Canva Docs.

To learn more, see Creating text.

Videos

If the selection event contains video content, each object contains an asset reference that points to an asset in Canva's backend:

ts
const draft = await selectedContent.read();

for (const content of draft.contents) {
  console.log(content.ref);
}

You can use the getTemporaryUrl method to get the URL of the underlying video file:

ts
import { getTemporaryUrl } from "@canva/asset";

const { url } = await getTemporaryUrl({
  type: "video",
  ref: content.ref,
});

console.log(url);

Keep in mind that:

  • The URL always points to the full-size version of the video, even if the video is cropped or exists in a frame. This is the intended behavior as the user can always change the crop or frame.
  • 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.

Replacing selected content

In addition to reading selected content, apps can replace selected content.

The basic workflow for replacing content is always the same, regardless of the content type:

  1. Read the current selection:

    ts
    const draft = await selectedContent.read();
  2. Loop through the contents and transform them:

    ts
    for (const content of draft.contents) {
      // Content-specific transformation logic goes here
    }

    Each content item is an object. The properties in this object depend on the type of content.

  3. Save the changes:

    ts
    await draft.save();

    Note: Changes made to contents won't be reflected in the user's design until the changes are saved.

Images

For images, the transformation process within the loop involves:

  1. Downloading the image using getTemporaryUrl
  2. Transforming the image data
  3. Uploading the transformed image as an asset
  4. Replacing the ref of the asset
  5. Setting a parentRef property to the ref of the original asset (see Deriving assets)

For example:

ts
import { getTemporaryUrl, upload } from "@canva/asset";

const draft = await selectedContent.read();

for (const content of draft.contents) {
  // Get the image URL
  const { url } = await getTemporaryUrl({
    type: "image",
    ref: content.ref,
  });

  // Transform the image (implementation varies)
  const transformedImage = await transformImage(url);

  // Upload the transformed image
  const asset = await upload({
    type: "image",
    url: transformedImage.url,
    mimeType: transformedImage.mimeType,
    thumbnailUrl: transformedImage.thumbnailUrl,
    parentRef: content.ref,
    aiDisclosure: "none",
  });

  // Replace the image ref
  content.ref = asset.ref;
}

await draft.save();

You can transform images either on the frontend or backend:

<Tabs> <Tab name="Frontend"> To transform images via the app's frontend:

1. Download each image
2. Draw each image into an `HTMLCanvasElement`
3. Apply transformations
4. Get the data URL

The following code sample contains a reusable function that handles this logic:

```ts
import { getTemporaryUrl, ImageMimeType, ImageRef } from "@canva/asset";

/**
 * Downloads and transforms a raster image.
 * @param ref - A unique identifier that points to an image asset in Canva's backend.
 * @param transformer - A function that transforms the image.
 * @returns The data URL and MIME type of the transformed image.
 */
async function transformRasterImage(
  ref: ImageRef,
  transformer: (ctx: CanvasRenderingContext2D, imageData: ImageData) => void
): Promise<{ dataUrl: string; mimeType: ImageMimeType }> {
  // Get a temporary URL for the asset
  const { url } = await getTemporaryUrl({
    type: "image",
    ref,
  });

  // Download the image
  const response = await fetch(url, { mode: "cors" });
  const imageBlob = await response.blob();

  // Extract MIME type from the downloaded image
  const mimeType = imageBlob.type;

  // Warning: This doesn't attempt to handle SVG images
  if (!isSupportedMimeType(mimeType)) {
    throw new Error(`Unsupported mime type: ${mimeType}`);
  }

  // Create an object URL for the image
  const objectURL = URL.createObjectURL(imageBlob);

  // Define an image element and load image from the object URL
  const image = new Image();
  image.crossOrigin = "Anonymous";

  await new Promise((resolve, reject) => {
    image.onload = resolve;
    image.onerror = () => reject(new Error("Image could not be loaded"));
    image.src = objectURL;
  });

  // Create a canvas and draw the image onto it
  const canvas = document.createElement("canvas");
  canvas.width = image.width;
  canvas.height = image.height;

  const ctx = canvas.getContext("2d");

  if (!ctx) {
    throw new Error("CanvasRenderingContext2D is not available");
  }

  ctx.drawImage(image, 0, 0);

  // Get the image data from the canvas to manipulate pixels
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  transformer(ctx, imageData);

  // Put the transformed image data back onto the canvas
  ctx.putImageData(imageData, 0, 0);

  // Clean up: Revoke the object URL to free up memory
  URL.revokeObjectURL(objectURL);

  // Convert the canvas content to a data URL with the original MIME type
  const dataUrl = canvas.toDataURL(mimeType);

  return { dataUrl, mimeType };
}

function isSupportedMimeType(
  input: string
): input is "image/jpeg" | "image/heic" | "image/png" | "image/webp" {
  // This does not include "image/svg+xml"
  const mimeTypes = ["image/jpeg", "image/heic", "image/png", "image/webp"];
  return mimeTypes.includes(input);
}
```

To use the `transformRasterImage`, function, pass an image reference in as the first argument and a function for transforming the image as the second argument. The following usage inverts the colors of an image:

```ts
const { dataUrl, mimeType } = await transformRasterImage(
  content.ref,
  (_, { data }) => {
    // Invert the colors of each pixel
    for (let i = 0; i < data.length; i += 4) {
      data[i] = 255 - data[i];
      data[i + 1] = 255 - data[i + 1];
      data[i + 2] = 255 - data[i + 2];
    }
  }
);

console.log("The data URL of the transformed image is:", dataUrl);
```

**Warning:** When processing images in the frontend, the final image is uploaded to Canva's backend from the user's device. This approach may incur data charges for users, especially on mobile.

</Tab>

<Tab name="Backend"> To transform images via your app's backend, use the Fetch API to send the URL of the image to the app's backend:

```ts
const response = await fetch("http://localhost:3000/invert-image", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url,
  }),
});
```

**Warning:** Your app's backend must verify incoming HTTP requests. To learn more, see HTTP request verification.

On the backend, transform the image and return a URL that Canva can use to download the new image:

```ts
import axios from "axios";
import cors from "cors";
import express from "express";
import Jimp from "jimp";
import path from "path";

// TODO: Add the URL of the server here — it must be available to Canva's backend
const PUBLIC_SERVER_URL = "<INSERT_PUBLIC_SERVER_URL_HERE>";

const app = express();

app.use(cors());
app.use(express.json());
app.use("/uploads", express.static(path.join(__dirname, "uploads")));

app.post("/invert-image", async (req, res) => {
  // Download the image
  const response = await axios({
    url: req.body.url,
    method: "get",
    responseType: "arraybuffer",
  });

  // Invert the image's colors
  const image = await Jimp.read(Buffer.from(response.data));
  image.invert();

  // Save the transformed image to "uploads" directory
  const id = Date.now().toString();
  const imageName = `${id}.jpg`;
  const imagePath = path.join(__dirname, "uploads", imageName);
  await image.writeAsync(imagePath);

  // Create a thumbnail of the transformed image
  const thumbnailName = `${id}_thumbnail.jpg`;
  const thumbnailPath = path.join(__dirname, "uploads", thumbnailName);
  const thumbnailWidth = 300;
  const thumbnailHeight = Jimp.AUTO;
  image.resize(thumbnailWidth, thumbnailHeight);
  await image.writeAsync(thumbnailPath);

  // Get the image's MIME type
  const mimeType = image.getMIME();

  // Return the URLs of the transformed image and thumbnail
  res.json({
    id,
    url: `${PUBLIC_SERVER_URL}/uploads/${imageName}`,
    thumbnailUrl: `${PUBLIC_SERVER_URL}/uploads/${thumbnailName}`,
    mimeType,
  });
});

app.listen(process.env.PORT || 3000, () => {
  console.log("The server is running...");
});
```

On the frontend, parse the response to access the returned data:

```ts
// Parse the response as JSON
const json = await response.json();
console.log(json.url); // => "https://..."
```

**Note:** The URL of the new image must be available via the public internet. This is because Canva's backend must be able to download the image. To learn more, see Assets.

</Tab> </Tabs>

Plaintext

For plaintext, modify the text property of each content item:

ts
const draft = await selectedContent.read();

for (const content of draft.contents) {
  // Transform the text however you need
  content.text = `${content.text} was modified!`;
}

await draft.save();

Richtext

For richtext, each content object is a richtext range that exposes methods for reading and modifying formatted text. Here's an example that creates a rainbow effect:

ts
const draft = await selectedContent.read();

// Keep track of the current color across text regions
let currentColorIndex = 0;

for (const content of draft.contents) {
  // Get the text regions
  const regions = content.readTextRegions();

  // Loop through each text region
  for (const region of regions) {
    // Loop through each character in the regions's text
    for (let i = 0; i < region.text.length; i++) {
      // Get the color for the current character
      const colorIndex = (currentColorIndex + i) % RAINBOW_COLORS.length;
      const color = RAINBOW_COLORS[colorIndex];

      // Format the current character
      content.formatText({ start: region.offset + i, length: 1 }, { color });
    }

    // Update the current color
    currentColorIndex += region.text.length;
  }
}

await draft.save();

To learn more about richtext manipulation, see Richtext ranges.

Videos

For videos, the process is similar to images:

  1. Downloading the video using getTemporaryUrl
  2. Transforming the video data
  3. Uploading the transformed video as an asset
  4. Replacing the ref of the asset
  5. Setting a parentRef property to the ref of the original asset (see Deriving assets)

For example:

ts
import { getTemporaryUrl, upload } from "@canva/asset";

const draft = await selectedContent.read();

for (const content of draft.contents) {
  // Get the video URL
  const { url } = await getTemporaryUrl({
    type: "video",
    ref: content.ref,
  });

  // Transform the video (typically on backend)
  const transformedVideo = await transformVideo(url);

  // Upload the transformed video
  const asset = await upload({
    type: "video",
    url: transformedVideo.url,
    mimeType: transformedVideo.mimeType,
    thumbnailImageUrl: transformedVideo.thumbnailImageUrl,
    thumbnailVideoUrl: transformedVideo.thumbnailVideoUrl,
    parentRef: content.ref,
    aiDisclosure: "none",
  });

  // Replace the video ref
  content.ref = asset.ref;
}

await draft.save();

Video transformation typically happens on the backend due to processing requirements:

ts
// Frontend: Send video URL to backend
const response = await fetch("http://localhost:3000/transform-video", {
  method: "post",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url,
  }),
});

// Parse the response
const json = await response.json();

Warning: Your app's backend must verify incoming HTTP requests. To learn more, see HTTP request verification.

Note: The transformed video URL must be publicly accessible. To learn more, see Assets.

Known limitations

  • Apps can only read and replace raster images — not vector images.
  • You can't replace one type of content with a different type of content.
  • If multiple pieces of content are selected, the ordering of the content is not stable and should not be relied upon.

API reference

  • getTemporaryUrl
  • selection.registerOnChange
  • upload

Code samples

Images

tsx
import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { getTemporaryUrl, upload, ImageMimeType, ImageRef } from "@canva/asset";
import { useSelection } from "@canva/app-hooks";
import * as styles from "styles/components.css";

export function App() {
  const selectedContent = useSelection("image");
  const isContentSelected = selectedContent.count > 0;

  async function handleClick() {
    if (!isContentSelected) {
      return;
    }

    const draft = await selectedContent.read();

    for (const content of draft.contents) {
      // Download and transform the image
      const newImage = await transformRasterImage(
        content.ref,
        (_, { data }) => {
          for (let i = 0; i < data.length; i += 4) {
            data[i] = 255 - data[i];
            data[i + 1] = 255 - data[i + 1];
            data[i + 2] = 255 - data[i + 2];
          }
        }
      );

      // Upload the transformed image
      const asset = await upload({
        type: "image",
        url: newImage.dataUrl,
        mimeType: newImage.mimeType,
        thumbnailUrl: newImage.dataUrl,
        parentRef: content.ref,
        aiDisclosure: "none",
      });

      // Replace the image
      content.ref = asset.ref;
    }

    await draft.save();
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="2u">
        <Button
          variant="primary"
          disabled={!isContentSelected}
          onClick={handleClick}
        >
          Replace selected image content
        </Button>
      </Rows>
    </div>
  );
}

/**
 * Downloads and transforms a raster image.
 * @param ref - A unique identifier that points to an image asset in Canva's backend.
 * @param transformer - A function that transforms the image.
 * @returns The data URL and MIME type of the transformed image.
 */
async function transformRasterImage(
  ref: ImageRef,
  transformer: (ctx: CanvasRenderingContext2D, imageData: ImageData) => void
): Promise<{ dataUrl: string; mimeType: ImageMimeType }> {
  // Get a temporary URL for the asset
  const { url } = await getTemporaryUrl({
    type: "image",
    ref,
  });

  // Download the image
  const response = await fetch(url, { mode: "cors" });
  const imageBlob = await response.blob();

  // Extract MIME type from the downloaded image
  const mimeType = imageBlob.type;

  // Warning: This doesn't attempt to handle SVG images
  if (!isSupportedMimeType(mimeType)) {
    throw new Error(`Unsupported mime type: ${mimeType}`);
  }

  // Create an object URL for the image
  const objectURL = URL.createObjectURL(imageBlob);

  // Define an image element and load image from the object URL
  const image = new Image();
  image.crossOrigin = "Anonymous";

  await new Promise((resolve, reject) => {
    image.onload = resolve;
    image.onerror = () => reject(new Error("Image could not be loaded"));
    image.src = objectURL;
  });

  // Create a canvas and draw the image onto it
  const canvas = document.createElement("canvas");
  canvas.width = image.width;
  canvas.height = image.height;

  const ctx = canvas.getContext("2d");

  if (!ctx) {
    throw new Error("CanvasRenderingContext2D is not available");
  }

  ctx.drawImage(image, 0, 0);

  // Get the image data from the canvas to manipulate pixels
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  transformer(ctx, imageData);

  // Put the transformed image data back onto the canvas
  ctx.putImageData(imageData, 0, 0);

  // Clean up: Revoke the object URL to free up memory
  URL.revokeObjectURL(objectURL);

  // Convert the canvas content to a data URL with the original MIME type
  const dataUrl = canvas.toDataURL(mimeType);

  return { dataUrl, mimeType };
}

function isSupportedMimeType(
  input: string
): input is "image/jpeg" | "image/heic" | "image/png" | "image/webp" {
  // This does not include "image/svg+xml"
  const mimeTypes = ["image/jpeg", "image/heic", "image/png", "image/webp"];
  return mimeTypes.includes(input);
}

Plaintext

tsx
import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { useSelection } from "@canva/app-hooks";
import * as styles from "styles/components.css";

export function App() {
  const selectedContent = useSelection("plaintext");
  const isContentSelected = selectedContent.count > 0;

  async function handleClick() {
    if (!isContentSelected) {
      return;
    }

    const draft = await selectedContent.read();

    for (const content of draft.contents) {
      content.text = `${content.text} was modified!`;
    }

    await draft.save();
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="2u">
        <Button
          variant="primary"
          disabled={!isContentSelected}
          onClick={handleClick}
        >
          Replace selected plaintext content
        </Button>
      </Rows>
    </div>
  );
}

Richtext

tsx
import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { useSelection } from "@canva/app-hooks";
import * as styles from "styles/components.css";

const RAINBOW_COLORS = [
  "#FF0000",
  "#FF7F00",
  "#FFFF00",
  "#00FF00",
  "#0000FF",
  "#4B0082",
  "#8B00FF",
];

export function App() {
  const selectedContent = useSelection("richtext");
  const isContentSelected = selectedContent.count > 0;

  async function handleClick() {
    if (!isContentSelected) {
      return;
    }

    // Get a snapshot of the currently selected text
    const draft = await selectedContent.read();

    // Keep track of the current color across text regions
    let currentColorIndex = 0;

    // Loop through all selected richtext content
    for (const content of draft.contents) {
      // Get the text regions
      const regions = content.readTextRegions();

      // Loop through each text region
      for (const region of regions) {
        // Loop through each character in the regions's text
        for (let i = 0; i < region.text.length; i++) {
          // Get the color for the current character
          const colorIndex = (currentColorIndex + i) % RAINBOW_COLORS.length;
          const color = RAINBOW_COLORS[colorIndex];

          // Format the current character
          content.formatText(
            { start: region.offset + i, length: 1 },
            { color }
          );
        }

        // Update the current color
        currentColorIndex += region.text.length;
      }
    }

    // Commit the changes
    await draft.save();
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="2u">
        <Button
          variant="primary"
          disabled={!isContentSelected}
          onClick={handleClick}
        >
          Replace selected richtext content
        </Button>
      </Rows>
    </div>
  );
}

Videos

tsx
import React from "react";
import { Button, Rows } from "@canva/app-ui-kit";
import { useSelection } from "@canva/app-hooks";
import { upload } from "@canva/asset";
import * as styles from "styles/components.css";

export function App() {
  const selectedContent = useSelection("video");
  const isContentSelected = selectedContent.count > 0;

  async function handleClick() {
    if (!isContentSelected) {
      return;
    }

    const draft = await selectedContent.read();

    for (const content of draft.contents) {
      // Upload the replacement video
      const asset = await upload({
        type: "video",
        mimeType: "video/mp4",
        url: "https://www.canva.dev/example-assets/video-import/beach-video.mp4",
        thumbnailImageUrl:
          "https://www.canva.dev/example-assets/video-import/beach-thumbnail-image.jpg",
        thumbnailVideoUrl:
          "https://www.canva.dev/example-assets/video-import/beach-thumbnail-video.mp4",
        parentRef: content.ref,
        aiDisclosure: "none",
      });

      // Replace the video
      content.ref = asset.ref;
    }

    await draft.save();
  }

  return (
    <div className={styles.scrollContainer}>
      <Rows spacing="2u">
        <Button
          variant="primary"
          disabled={!isContentSelected}
          onClick={handleClick}
        >
          Replace selected video content
        </Button>
      </Rows>
    </div>
  );
}

Canva Developer Documentation SOP Site