Image editing overlay

Create custom image editing overlays and filters.

Running this example

To run this example locally:

  1. If you haven't already, create a new app in the Developer Portal(opens in a new tab or window). For more information, refer to our Quickstart guide.

  2. In your app's configuration on the Developer Portal(opens in a new tab or window), ensure the "Development URL" is set to http://localhost:8080.

  3. Clone the starter kit:

    git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.git
    cd canva-apps-sdk-starter-kit
    SHELL
  4. Install dependencies:

    npm install
    SHELL
  5. Run the example:

    npm run start image_editing_overlay
    SHELL
  6. Click the Preview URL link shown in the terminal to open the example in the Canva editor.

Example app source code

// For usage information, see the README.md file.
import { appProcess } from "@canva/platform";
import { ObjectPanel } from "./object_panel";
import { SelectedImageOverlay } from "./overlay";
export const App = () => {
const context = appProcess.current.getInfo();
if (context.surface === "object_panel") {
return <ObjectPanel />;
}
if (context.surface === "selected_image_overlay") {
return <SelectedImageOverlay />;
}
throw new Error(`Invalid surface: ${context.surface}`);
};
TYPESCRIPT
// For usage information, see the README.md file.
import { AppUiProvider } from "@canva/app-ui-kit";
import { createRoot } from "react-dom/client";
import "@canva/app-ui-kit/styles.css";
import { App } from "./app";
const root = createRoot(document.getElementById("root") as Element);
function render() {
root.render(
<AppUiProvider>
<App />
</AppUiProvider>,
);
}
render();
// Hot Module Replacement for development (automatically reloads the app when changes are made)
if (module.hot) {
module.hot.accept("./app", render);
}
TYPESCRIPT
import { Alert, Button, Rows, Text, Title } from "@canva/app-ui-kit";
import { appProcess } from "@canva/platform";
import * as React from "react";
import * as styles from "styles/components.css";
import { useOverlay } from "utils/use_overlay_hook";
import { useFeatureSupport } from "utils/use_feature_support";
export const ObjectPanel = () => {
const overlay = useOverlay("image_selection");
const isSupported = useFeatureSupport();
const [isImageReady, setIsImageReady] = React.useState(false);
React.useEffect(() => {
// Listen for messages from the overlay about image readiness
appProcess.registerOnMessage(async (sender, message) => {
if (
typeof message === "object" &&
message != null &&
"isImageReady" in message
) {
setIsImageReady(Boolean(message.isImageReady));
}
});
}, []);
const handleOpen = () => {
overlay.open();
};
const handleInvert = () => {
appProcess.broadcastMessage({ action: "invert" });
};
const handleBlur = () => {
appProcess.broadcastMessage({ action: "blur" });
};
const handleReset = () => {
appProcess.broadcastMessage({ action: "reset" });
};
const handleSave = () => {
overlay.close({ reason: "completed" });
};
const handleClose = () => {
overlay.close({ reason: "aborted" });
};
// Check if overlay functionality is supported
if (!isSupported(overlay.open)) {
return (
<div className={styles.scrollContainer}>
<UnsupportedAlert />
</div>
);
}
// Show overlay controls when overlay is open
if (overlay.isOpen) {
return (
<div className={styles.scrollContainer}>
<Rows spacing="3u">
<Title size="small">Image editing</Title>
<Text>Apply effects to your image with real-time preview.</Text>
<Rows spacing="1.5u">
<Button
variant="secondary"
disabled={!isImageReady}
onClick={handleInvert}
stretch
>
Invert colors
</Button>
<Button
variant="secondary"
disabled={!isImageReady}
onClick={handleBlur}
stretch
>
Add blur
</Button>
<Button
variant="secondary"
disabled={!isImageReady}
onClick={handleReset}
stretch
>
Reset changes
</Button>
</Rows>
<Rows spacing="1.5u">
<Button
variant="primary"
disabled={!isImageReady}
onClick={handleSave}
stretch
>
Save and close
</Button>
<Button
variant="secondary"
disabled={!isImageReady}
onClick={handleClose}
stretch
>
Close without saving
</Button>
</Rows>
</Rows>
</div>
);
}
// Show initial state with open overlay button
return (
<div className={styles.scrollContainer}>
<Rows spacing="3u">
<Title size="small">Image editing overlay</Title>
<Text>
Select a raster image in your design to start editing with real-time
preview.
</Text>
<Button
variant="primary"
disabled={!overlay.canOpen}
onClick={handleOpen}
stretch
>
Edit image
</Button>
{!overlay.canOpen && <SelectionAlert />}
</Rows>
</div>
);
};
// Alert shown when image overlay is not supported
const UnsupportedAlert = () => (
<Alert tone="warn">
Image editing overlay functionality is not supported in the current design
type.
</Alert>
);
// Alert shown when no valid image is selected
const SelectionAlert = () => (
<Alert tone="info">
Select a single raster image in your design to enable image editing. Vector
images and multiple selections are not supported.
</Alert>
);
TYPESCRIPT
import { getTemporaryUrl, upload } from "@canva/asset";
import { appProcess } from "@canva/platform";
import * as React from "react";
import { useSelection } from "utils/use_selection_hook";
export const SelectedImageOverlay = () => {
const selection = useSelection("image");
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const originalImageRef = React.useRef<HTMLImageElement | null>(null);
React.useEffect(() => {
const initializeCanvas = async () => {
try {
// Get the selected image
const draft = await selection.read();
const [image] = draft.contents;
if (!image) {
return;
}
// Download the selected image
const { url } = await getTemporaryUrl({
type: "image",
ref: image.ref,
});
const img = await downloadImage(url);
// Store reference to original image for reset functionality
originalImageRef.current = img;
// Render the selected image
const { canvas, context } = getCanvas(canvasRef.current);
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0, img.width, img.height);
// Notify that image is ready
appProcess.broadcastMessage({ isImageReady: true });
} catch {
appProcess.broadcastMessage({ isImageReady: false });
}
};
initializeCanvas();
}, [selection]);
React.useEffect(() => {
// Listen for editing commands from the object panel
appProcess.registerOnMessage(async (sender, message) => {
if (
typeof message !== "object" ||
message == null ||
!("action" in message)
) {
return;
}
try {
const { canvas, context } = getCanvas(canvasRef.current);
switch (message.action) {
case "invert":
context.filter = "invert(100%)";
context.drawImage(canvas, 0, 0);
break;
case "blur":
context.filter = "blur(3px)";
context.drawImage(canvas, 0, 0);
break;
case "reset":
if (originalImageRef.current) {
context.filter = "none";
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(originalImageRef.current, 0, 0);
}
break;
default:
// Unknown action, do nothing
break;
}
} catch {
// Silently handle effect application errors
}
});
}, []);
React.useEffect(() => {
// Handle overlay disposal (save or close)
return void appProcess.current.setOnDispose(async (context) => {
try {
// Save changes if user completed the editing
if (context.reason === "completed") {
// Get the modified image data
const { canvas } = getCanvas(canvasRef.current);
const dataUrl = canvas.toDataURL("image/png", 1.0);
// Upload the modified image
const asset = await upload({
type: "image",
mimeType: "image/png",
url: dataUrl,
thumbnailUrl: dataUrl,
aiDisclosure: "none",
});
// Replace the original image with the modified version
const draft = await selection.read();
draft.contents[0].ref = asset.ref;
await draft.save();
}
// Reset image readiness state
appProcess.broadcastMessage({ isImageReady: false });
} catch {
// Handle save errors silently
}
});
}, [selection]);
return (
<canvas
ref={canvasRef}
style={{
width: "100%",
height: "100%",
display: "block",
}}
/>
);
};
// Utility function to download image from URL
const downloadImage = async (url: string): Promise<HTMLImageElement> => {
const response = await fetch(url, { mode: "cors" });
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
const img = new Image();
img.crossOrigin = "anonymous";
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error("Image could not be loaded"));
img.src = objectUrl;
});
URL.revokeObjectURL(objectUrl);
return img;
};
// Utility function to get canvas and context in a type-safe way
const getCanvas = (canvas: HTMLCanvasElement | null) => {
if (!canvas) {
throw new Error("HTMLCanvasElement does not exist");
}
const context = canvas.getContext("2d");
if (!context) {
throw new Error("CanvasRenderingContext2D does not exist");
}
return { canvas, context };
};
TYPESCRIPT
# Image editing overlay
Demonstrates how to use image editing overlays to create simple interactive image editing experiences. Shows real-time preview of basic image effects, communication between overlay and object panel, and saving edited images back to the design.
For API reference docs and instructions on running this example, see: https://www.canva.dev/docs/apps/examples/image-editing-overlay/.
Related examples: See assets_and_media/asset_upload for image importing, or design_elements/image_elements for basic image manipulation patterns.
NOTE: This example differs from what is expected for public apps to pass a Canva review:
- ESLint rule `no-console` is disabled for example purposes only. Production apps shouldn't disable linting rules without proper justification
- Image effects are simplified for demonstration. Production apps should implement more sophisticated image processing and comprehensive error handling
- Internationalization isn't implemented. Production apps must support multiple languages using the `@canva/app-i18n-kit` package to pass Canva review requirements
- The example uses basic image effects. Production apps should consider offering customizable parameters (e.g., blur radius, effect intensity) for better user experience
MARKDOWN

API reference

Need help?