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.

Drag and drop

How apps can support drag and drop.

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

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

Content types

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

Additional content types may be supported in the future.

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:

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

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 Type
startDragToPoint
startDragToCursor
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

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 "utils/use_feature_support"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_feature_support.ts
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>
);
}
TSX
import React from "react";
import { EmbedCard, Rows } from "@canva/app-ui-kit";
import { addElementAtCursor, addElementAtPoint, ui } from "@canva/design";
import { useFeatureSupport } from "utils/use_feature_support"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_feature_support.ts
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>
);
}
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 "utils/use_feature_support"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_feature_support.ts
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>
);
}
TSX
import React from "react";
import { Rows, Text, TypographyCard } from "@canva/app-ui-kit";
import { addElementAtCursor, addElementAtPoint, ui } from "@canva/design";
import { useFeatureSupport } from "utils/use_feature_support"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_feature_support.ts
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>
);
}
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 "utils/use_feature_support"; // https://github.com/canva-sdks/canva-apps-sdk-starter-kit/blob/main/utils/use_feature_support.ts
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>
);
}
TSX