Replacing elements
In addition to reading content in a user's design, apps can also replace certain types of content. This unlocks a range of powerful features, such as image effects and text manipulation.
How to replace images
Step 1: Enable the required permissions
In the Developer Portal, enable the following permissions:
canva:design:content:read
canva:design:content:write
canva:asset:private:read
canva:asset:private:write
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: Get the selected image content
Use the useSelection
hook or register a callback with the selection.registerOnChange
method:
import React from "react";import { Button, Rows } 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.tsimport 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();console.log(draft.contents); // => [{ ref: "..." }]}return (<div className={styles.scrollContainer}><Rows spacing="1u"><Buttonvariant="primary"disabled={!isElementSelected}onClick={handleClick}>Replace selected image content</Button></Rows></div>);}
The selection event has a read
method that returns an array of the selected images. Each image is represented as an object with a ref
property. The ref
contains a unique identifier that points to an asset in Canva's backend:
const draft = await currentSelection.read();console.log(draft.contents); // => [{ ref: "..." }]
To learn more, see Reading elements.
Step 3: Download the selected images
To access the ref
property of each image, loop through the selected images:
const draft = await currentSelection.read();for (const content of draft.contents) {console.log(content.ref); // => "..."}
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:
-
Import the
getTemporaryUrl
method from the@canva/asset
package:import { getTemporaryUrl } from "@canva/asset";ts -
Call the method, passing in the
ref
and the type of asset:const { url } = await getTemporaryUrl({type: "image",ref: content.ref,});console.log("Temporary URL:", url);ts
Step 4: Transform the images
Once the app has access to the URL of one or more images:
- Download the images.
- Transform the images.
- Upload the transformed images to Canva's backend.
In some cases, this process can take place entirely via the frontend. In other cases, such as when integrating with AI models, it makes more sense to handle the transformation via the app's backend.
To transform images via the app's frontend:
- Download each image.
- Draw each image into an
HTMLCanvasElement
. - Apply some sort of transformation to each image.
- Get the data URL of the transformed images.
The following code sample contains a reusable function that handles this logic for you:
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 assetconst { url } = await getTemporaryUrl({type: "image",ref,});// Download the imageconst response = await fetch(url, { mode: "cors" });const imageBlob = await response.blob();// Extract MIME type from the downloaded imageconst mimeType = imageBlob.type;// Warning: This doesn't attempt to handle SVG imagesif (!isSupportedMimeType(mimeType)) {throw new Error(`Unsupported mime type: ${mimeType}`);}// Create an object URL for the imageconst objectURL = URL.createObjectURL(imageBlob);// Define an image element and load image from the object URLconst 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 itconst 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 pixelsconst imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);transformer(ctx, imageData);// Put the transformed image data back onto the canvasctx.putImageData(imageData, 0, 0);// Clean up: Revoke the object URL to free up memoryURL.revokeObjectURL(objectURL);// Convert the canvas content to a data URL with the original MIME typeconst 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:
const { dataUrl, mimeType } = await transformRasterImage(content.ref,(_, { data }) => {// Invert the colors of each pixelfor (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);
Step 5: Upload the replaced images
Once the app's frontend has a URL or data URL for the new image, upload the image to Canva's backend.
To upload the image:
-
Import the
upload
method from the@canva/asset
package:import { getTemporaryUrl, upload } from "@canva/asset";ts -
Call the method, passing in the required properties:
// Upload the replaced imageconst asset = await upload({type: "image",mimeType: "image/png",url: "URL OR DATA URL GOES HERE",thumbnailUrl: "URL OR DATA URL GOES HERE",parentRef: content.ref,});tsTo learn more about the required properties, see
upload
.Handling derived assets
The
upload
method accepts aparentRef
property. This property doesn't have a visible impact on the app, but it must contain theref
of the originally selected image.Here's why:
Canva licenses assets from a number of creators. By setting the
parentRef
property, Canva can keep track of the original asset and ensure that any licensing requirements are met.A side-effect of this requirement is that apps are not allowed to combine multiple assets into a single asset. This is because the tracking mechanism doesn't account for assets derived from multiple assets.
Step 6: Replace the images
In the loop, replace the current ref
of the image with the new ref
:
for (const content of draft.contents) {// Get a temporary URL for the asset// Download and transform the image// Upload the transformed image// ...// Replace the imagecontent.ref = asset.ref;}
Step 7: Save the changes
By default, any changes to the content are not reflected in the user's design. To persist the changes and update the user's design, call the save
method that's returned by the read
method:
await draft.save();
This method should be called after the loop.
How to replace plaintext
Step 1: Enable the required permissions
In the Developer Portal, enable the following permissions:
canva:design:content:read
canva:design:content:write
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: Get the selected plaintext content
Use the useSelection
hook or register a callback with the selection.registerOnChange
method:
import React from "react";import { useSelection } from "utils/use_selection_hook"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_selection_hook.tsimport { Button, Rows } from "@canva/app-ui-kit";import styles from "styles/components.css";export function App() {const currentSelection = useSelection("plaintext");const isElementSelected = currentSelection.count > 0;async function replaceText() {if (!isElementSelected) {return;}const draft = await currentSelection.read();console.log(draft.contents);}return (<div className={styles.scrollContainer}><Rows spacing="1u"><Buttonvariant="primary"disabled={!isElementSelected}onClick={replaceText}>Replace selected plaintext content</Button></Rows></div>);}
To learn more, see Reading elements.
Step 3: Replace the content of the selected elements
Loop through the contents of the selected elements and replace the text
property with a new value:
const draft = await currentSelection.read();for (const content of draft.contents) {content.text = `${content.text} was modified!`;}
Step 4: Save the changes
By default, any changes to the content are not reflected in the user's design. To persist the changes and update the user's design, call the save
method that's returned by the read
method:
await draft.save();
This method should be called after the loop.
How to replace richtext
Step 1: Enable the required permissions
In the Developer Portal, enable the following permissions:
canva:design:content:read
canva:design:content:write
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: Get the selected richtext content
The useSelection
hook is not yet compatible with richtext selection, so import selection
and SelectionEvent
after installing @canva/design
to register the callback:
import React from "react";import { Button, Rows } from "@canva/app-ui-kit";import { selection, SelectionEvent } from "@canva/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 replaceText() {if (!isElementSelected || !currentSelection) {return;}const draft = await currentSelection.read();for (const content of draft.contents) {const regions = content.readTextRegions();console.log(regions);}}return (<div className={styles.scrollContainer}><Rows spacing="1u"><Buttonvariant="primary"disabled={!isElementSelected}onClick={replaceText}>Replace selected text content</Button></Rows></div>);}
To learn more, see Reading elements.
Step 3: Replace the content of the selected elements
When working with richtext, each content
object is a richtext range that exposes a variety of methods for interacting with that particular portion of richtext.
The following snippet demonstrates how to loop through the selected text regions and format each character:
// Keep track of the current color across text regionslet currentColorIndex = 0;// Loop through all selected rich text contentfor (const content of draft.contents) {// Get the text regionsconst regions = content.readTextRegions();// Loop through each text regionfor (const region of regions) {// Loop through each character in the regions's textfor (let i = 0; i < region.text.length; i++) {// Get the color for the current characterconst colorIndex = (currentColorIndex + i) % RAINBOW_COLORS.length;const color = RAINBOW_COLORS[colorIndex];// Format the current charactercontent.formatText({ start: colorIndex + i, length: 1 }, { color });}// Update the current colorcurrentColorIndex += region.text.length;}}
To learn more, see Richtext ranges.
Step 4: Save the changes
By default, any changes to the content are not reflected in the user's design. To persist the changes and update the user's design, call the save
method that's returned by the read
method:
await draft.save();
This method should be called after the loop.
How to replace videos
Step 1: Enable the required permissions
In the Developer Portal, enable the following permissions:
canva:design:content:read
canva:design:content:write
canva:asset:private:read
canva:asset:private:write
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: Get the content of the selected elements
Use the useSelection
hook or register a callback with the selection.registerOnChange
method:
import React from "react";import { Button, Rows } 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.tsimport 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();console.log(draft.contents); // => [{ ref: "..." }]}return (<div className={styles.scrollContainer}><Rows spacing="1u"><Buttonvariant="primary"disabled={!isElementSelected}onClick={handleClick}>Replace selected video content</Button></Rows></div>);}
The selection event has a read
method that returns an array of the selected videos. Each video is represented as an object with a ref
property. The ref
contains a unique identifier that points to an asset in Canva's backend:
const draft = await currentSelection.read();console.log(draft.contents); // => [{ ref: "..." }]
To learn more, see Reading elements.
Step 3: Download the selected videos
To access the ref
property of each video, loop through the selected videos:
const draft = await currentSelection.read();for (const content of draft.contents) {console.log(content.ref); // => "..."}
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:
-
Import the
getTemporaryUrl
method from the@canva/asset
package:import { getTemporaryUrl } from "@canva/asset";ts -
Call the method, passing in the
ref
and the type of asset:const { url } = await getTemporaryUrl({type: "video",ref: content.ref,});console.log("Temporary URL:", url);ts
Step 4: Transform the videos
Once the app has access to the URL of one or more videos:
- Download the videos.
- Transform the videos.
- Upload the transformed videos to Canva's backend.
Technically speaking, it's possible for this process to take place entirely via the frontend. Generally though, it makes more sense to handle the transformation via the app's backend.
To transform videos via your app's backend:
-
Use the Fetch API to send the URL of the video to the app's backend:
const response = await fetch("http://localhost:3000/invert-video", {method: "post",headers: {"Content-Type": "application/json",},body: JSON.stringify({url,}),});ts -
On the backend, transform the video and return a URL that Canva can use to download the new video.
-
On the frontend, parse the response to access the returned data:
// Parse the response as JSONconst json = await response.json();console.log(json.url); // => "https://..."ts
Step 5: Upload the replaced videos
Once the app's frontend has a URL or data URL for the new video, upload the video to Canva's backend.
To upload the video:
-
Import the
upload
method from the@canva/asset
package:import { getTemporaryUrl, upload } from "@canva/asset";ts -
Call the method, passing in the required properties:
// Upload the replaced videoconst asset = await upload({type: "video",mimeType: "video/mp4",url: "URL GOES HERE",thumbnailImageUrl: "URL GOES HERE",thumbnailVideoUrl: "URL GOES HERE",parentRef: content.ref,});tsTo learn more about the required properties, see
upload
.Handling derived assets
The
upload
method accepts aparentRef
property. This property doesn't have a visible impact on the app, but it must contain theref
of the originally selected video.Here's why:
Canva licenses assets from a number of creators. By setting the
parentRef
property, Canva can keep track of the original asset and ensure that any licensing requirements are met.A side-effect of this requirement is that apps are not allowed to combine multiple assets into a single asset. This is because the tracking mechanism doesn't account for assets derived from multiple assets.
Step 6: Replace the videos
In the loop, replace the current ref
of the video with the new ref
:
for (const content of draft.contents) {// Get a temporary URL for the asset// Download and transform the video// Upload the transformed video// ...// Replace the videocontent.ref = asset.ref;}
Step 7: Save the changes
By default, any changes to the content are not reflected in the user's design. To persist the changes and update the user's design, call the save
method that's returned by the read
method:
await draft.save();
This method should be called after the loop.
Known limitations
- You can't replace one type of element with a different type of element.
Additional considerations
- If multiple elements are selected, the ordering of the elements is not stable and should not be relied upon.
- If something is selected when
selection.registerOnChange
is called, the callback fires immediately.
API reference
Code sample
Images
import React from "react";import { Button, Rows } from "@canva/app-ui-kit";import { getTemporaryUrl, upload, ImageMimeType, ImageRef } from "@canva/asset";import { useSelection } from "utils/use_selection_hook"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_selection_hook.tsimport 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) {// Download and transform the imageconst 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 imageconst asset = await upload({type: "image",url: newImage.dataUrl,mimeType: newImage.mimeType,thumbnailUrl: newImage.dataUrl,parentRef: content.ref,});// Replace the imagecontent.ref = asset.ref;}await draft.save();}return (<div className={styles.scrollContainer}><Rows spacing="1u"><Buttonvariant="primary"disabled={!isElementSelected}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 assetconst { url } = await getTemporaryUrl({type: "image",ref,});// Download the imageconst response = await fetch(url, { mode: "cors" });const imageBlob = await response.blob();// Extract MIME type from the downloaded imageconst mimeType = imageBlob.type;// Warning: This doesn't attempt to handle SVG imagesif (!isSupportedMimeType(mimeType)) {throw new Error(`Unsupported mime type: ${mimeType}`);}// Create an object URL for the imageconst objectURL = URL.createObjectURL(imageBlob);// Define an image element and load image from the object URLconst 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 itconst 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 pixelsconst imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);transformer(ctx, imageData);// Put the transformed image data back onto the canvasctx.putImageData(imageData, 0, 0);// Clean up: Revoke the object URL to free up memoryURL.revokeObjectURL(objectURL);// Convert the canvas content to a data URL with the original MIME typeconst 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
import React from "react";import { Button, Rows } 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.tsimport 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) {content.text = `${content.text} was modified!`;}await draft.save();}return (<div className={styles.scrollContainer}><Rows spacing="1u"><Buttonvariant="primary"disabled={!isElementSelected}onClick={handleClick}>Replace selected plaintext content</Button></Rows></div>);}
Richtext
import React from "react";import { Button, Rows } from "@canva/app-ui-kit";import { selection, SelectionEvent } from "@canva/design";import styles from "styles/components.css";const RAINBOW_COLORS = ["#FF0000","#FF7F00","#FFFF00","#00FF00","#0000FF","#4B0082","#8B00FF",];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;}// Get a snapshot of the currently selected textconst draft = await currentSelection.read();// Keep track of the current color across text regionslet currentColorIndex = 0;// Loop through all selected richtext contentfor (const content of draft.contents) {// Get the text regionsconst regions = content.readTextRegions();// Loop through each text regionfor (const region of regions) {// Loop through each character in the regions’s textfor (let i = 0; i < region.text.length; i++) {// Get the color for the current characterconst colorIndex = (currentColorIndex + i) % RAINBOW_COLORS.length;const color = RAINBOW_COLORS[colorIndex];// Format the current charactercontent.formatText({ start: colorIndex, length: 1 }, { color });}// Update the current colorcurrentColorIndex += region.text.length;}}// Commit the changesawait draft.save();}return (<div className={styles.scrollContainer}><Rows spacing="1u"><Buttonvariant="primary"disabled={!isElementSelected}onClick={handleClick}>Replace selected richtext content</Button></Rows></div>);}
Videos
import React from "react";import { Button, Rows } 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.tsimport { upload } from "@canva/asset";import 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) {// Upload the replacement videoconst 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,});// Replace the videocontent.ref = asset.ref;}await draft.save();}return (<div className={styles.scrollContainer}><Rows spacing="1u"><Buttonvariant="primary"disabled={!isElementSelected}onClick={handleClick}>Replace selected video content</Button></Rows></div>);}