Examples
App elements
Assets and media
Fundamentals
Intents
Design interaction
Drag and drop
Design elements
Localization
Content replacement
Image editing overlay
Create custom image editing overlays and filters.
Running this example
To run this example locally:
-
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.
-
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
. -
Clone the starter kit:
git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.gitcd canva-apps-sdk-starter-kitSHELL -
Install dependencies:
npm installSHELL -
Run the example:
npm run start image_editing_overlaySHELL -
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 readinessappProcess.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 supportedif (!isSupported(overlay.open)) {return (<div className={styles.scrollContainer}><UnsupportedAlert /></div>);}// Show overlay controls when overlay is openif (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"><Buttonvariant="secondary"disabled={!isImageReady}onClick={handleInvert}stretch>Invert colors</Button><Buttonvariant="secondary"disabled={!isImageReady}onClick={handleBlur}stretch>Add blur</Button><Buttonvariant="secondary"disabled={!isImageReady}onClick={handleReset}stretch>Reset changes</Button></Rows><Rows spacing="1.5u"><Buttonvariant="primary"disabled={!isImageReady}onClick={handleSave}stretch>Save and close</Button><Buttonvariant="secondary"disabled={!isImageReady}onClick={handleClose}stretch>Close without saving</Button></Rows></Rows></div>);}// Show initial state with open overlay buttonreturn (<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-timepreview.</Text><Buttonvariant="primary"disabled={!overlay.canOpen}onClick={handleOpen}stretch>Edit image</Button>{!overlay.canOpen && <SelectionAlert />}</Rows></div>);};// Alert shown when image overlay is not supportedconst UnsupportedAlert = () => (<Alert tone="warn">Image editing overlay functionality is not supported in the current designtype.</Alert>);// Alert shown when no valid image is selectedconst SelectionAlert = () => (<Alert tone="info">Select a single raster image in your design to enable image editing. Vectorimages 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 imageconst draft = await selection.read();const [image] = draft.contents;if (!image) {return;}// Download the selected imageconst { url } = await getTemporaryUrl({type: "image",ref: image.ref,});const img = await downloadImage(url);// Store reference to original image for reset functionalityoriginalImageRef.current = img;// Render the selected imageconst { 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 readyappProcess.broadcastMessage({ isImageReady: true });} catch {appProcess.broadcastMessage({ isImageReady: false });}};initializeCanvas();}, [selection]);React.useEffect(() => {// Listen for editing commands from the object panelappProcess.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 nothingbreak;}} 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 editingif (context.reason === "completed") {// Get the modified image dataconst { canvas } = getCanvas(canvasRef.current);const dataUrl = canvas.toDataURL("image/png", 1.0);// Upload the modified imageconst asset = await upload({type: "image",mimeType: "image/png",url: dataUrl,thumbnailUrl: dataUrl,aiDisclosure: "none",});// Replace the original image with the modified versionconst draft = await selection.read();draft.contents[0].ref = asset.ref;await draft.save();}// Reset image readiness stateappProcess.broadcastMessage({ isImageReady: false });} catch {// Handle save errors silently}});}, [selection]);return (<canvasref={canvasRef}style={{width: "100%",height: "100%",display: "block",}}/>);};// Utility function to download image from URLconst 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 wayconst 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 overlayDemonstrates 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
- App UI Kit
appProcess.broadcastMessage
appProcess.current.getInfo
appProcess.current.setOnDispose
appProcess.registerOnMessage
getTemporaryUrl
upload
Need help?
- Join our Community Forum(opens in a new tab or window)
- Report issues with this example on GitHub(opens in a new tab or window)