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:example 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";import type { DesignEditorIntent } from "@canva/intents/design";import { prepareDesignEditor } from "@canva/intents/design";async function render() {const root = createRoot(document.getElementById("root") as Element);root.render(<AppUiProvider><App /></AppUiProvider>,);}const designEditor: DesignEditorIntent = { render };prepareDesignEditor(designEditor);// 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 { useFeatureSupport, useOverlay } from "@canva/app-hooks";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 "@canva/app-hooks";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();const [image] = draft.contents;if (!image) {return;}image.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- The code structure is simplified: Production apps using [intents](https://www.canva.dev/docs/apps/intents/) are recommended to call the prepareDesignEditor function from src/intents/design_editor/index.tsx
MARKDOWN
API reference
- App UI Kit
appProcess.broadcastMessageappProcess.current.getInfoappProcess.current.setOnDisposeappProcess.registerOnMessagegetTemporaryUrlprepareDesignEditorupload
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)