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 { Overlay } from "./overlay";export type LaunchParams = {brushSize: number;};export const App = () => {const context = appProcess.current.getInfo<LaunchParams>();if (context.surface === "object_panel") {return <ObjectPanel />;}if (context.surface === "selected_image_overlay") {return <Overlay context={context} />;}throw new Error(`Invalid surface`);};
TYPESCRIPT
import { createRoot } from "react-dom/client";import { App } from "./app";import "@canva/app-ui-kit/styles.css";import { AppUiProvider } from "@canva/app-ui-kit";const root = createRoot(document.getElementById("root") as Element);function render() {root.render(<AppUiProvider><App /></AppUiProvider>,);}render();if (module.hot) {module.hot.accept("./app", render);}
TYPESCRIPT
import { Rows, FormField, Button, Slider, Text } from "@canva/app-ui-kit";import { useState } from "react";import * as styles from "styles/components.css";import { appProcess } from "@canva/platform";import { useOverlay } from "utils/use_overlay_hook";import type { LaunchParams } from "./app";import type { CloseOpts } from "./overlay";type UIState = {brushSize: number;};const initialState: UIState = {brushSize: 7,};export const ObjectPanel = () => {const {canOpen,isOpen,open,close: closeOverlay,} = useOverlay<"image_selection", CloseOpts>("image_selection");const [state, setState] = useState<UIState>(initialState);const openOverlay = async () => {open({launchParameters: {brushSize: state.brushSize,} satisfies LaunchParams,});};return (<div className={styles.scrollContainer}><Rows spacing="2u"><Text>Edit images directly on the design canvas with an image editingoverlay. Add an image to your design, select it, and click "OpenOverlay" to start editing the selected image.</Text>{isOpen ? (<><FormFieldlabel="Brush size"value={state.brushSize}control={(props) => (<Slider{...props}defaultValue={initialState.brushSize}min={5}max={20}step={1}value={state.brushSize}onChange={(value) =>setState((prevState) => {return {...prevState,brushSize: value,};})}onChangeComplete={(_, value) =>appProcess.broadcastMessage({...state,brushSize: value,})}/>)}/><Buttonvariant="primary"onClick={() => closeOverlay({ reason: "completed" })}stretch>Save Overlay</Button><Buttonvariant="primary"onClick={() => closeOverlay({ reason: "aborted" })}stretch>Cancel Overlay</Button></>) : (<><Buttonvariant="primary"onClick={openOverlay}disabled={!canOpen}stretch>Open Overlay</Button></>)}</Rows></div>);};
TYPESCRIPT
import { useEffect, useRef } from "react";import type { LaunchParams } from "./app";import { getTemporaryUrl, upload } from "@canva/asset";import { useSelection } from "utils/use_selection_hook";import type { AppProcessInfo, CloseParams } from "@canva/platform";import { appProcess } from "@canva/platform";import type { SelectionEvent } from "@canva/design";// App can extend CloseParams type to send extra data when closing the overlay// For example:// type CloseOpts = CloseParams & { message: string }export type CloseOpts = CloseParams;type OverlayProps = {context: AppProcessInfo<LaunchParams>;};type UIState = {brushSize: number;};export const Overlay = (props: OverlayProps) => {const { context: appContext } = props;const selection = useSelection("image");const canvasRef = useRef<HTMLCanvasElement>(null);const isDraggingRef = useRef<boolean>();const uiStateRef = useRef<UIState>({brushSize: 7,});useEffect(() => {if (!selection || selection.count !== 1) {return;}if (!appContext.launchParams ||appContext.surface !== "selected_image_overlay") {return void abort();}// set initial ui stateconst uiState = appContext.launchParams;uiStateRef.current = uiState;// set up canvasconst canvas = canvasRef.current;if (!canvas) {return void abort();}const context = canvas.getContext("2d");if (!context) {return void abort();}canvas.width = window.innerWidth;canvas.height = window.innerHeight;// load and draw image to canvasconst img = new Image();let cssScale = 1;const drawImage = () => {// Set the canvas dimensions to match the original image dimensions to maintain image quality,// when saving the output image back to the design using canvas.toDataUrl()cssScale = window.innerWidth / img.width;canvas.width = img.width;canvas.height = img.height;canvas.style.transform = `scale(${cssScale})`;canvas.style.transformOrigin = "0 0";context.drawImage(img, 0, 0, canvas.width, canvas.height);};img.onload = drawImage;img.crossOrigin = "anonymous";(async () => {const selectedImageUrl = await loadOriginalImage(selection);if (!selectedImageUrl) {return void abort();}img.src = selectedImageUrl;})();window.addEventListener("resize", () => {canvas.width = window.innerWidth;canvas.height = window.innerHeight;if (img.complete) {drawImage();}});canvas.addEventListener("pointerdown", (e) => {isDraggingRef.current = true;});canvas.addEventListener("pointermove", (e) => {if (isDraggingRef.current) {const mousePos = getCanvasMousePosition(canvas, e);context.fillStyle = "white";context.beginPath();context.arc(mousePos.x,mousePos.y,uiStateRef.current.brushSize * (1 / cssScale),0,Math.PI * 2,);context.fill();}});canvas.addEventListener("pointerup", () => {isDraggingRef.current = false;});return void appProcess.current.setOnDispose<CloseOpts>(async ({ reason }) => {// abort if image has not loaded or receive `aborted` signalif (reason === "aborted" || !img.src || !img.complete) {return;}const dataUrl = canvas.toDataURL();const draft = await selection.read();const queueImage = await upload({type: "image",mimeType: "image/png",url: dataUrl,thumbnailUrl: dataUrl,width: canvas.width,height: canvas.height,aiDisclosure: "none",});draft.contents[0].ref = queueImage.ref;await draft.save();},);}, [selection]);useEffect(() => {// set up message handlerreturn void appProcess.registerOnMessage(async (_, message) => {if (!message) {return;}const { brushSize } = message as UIState;uiStateRef.current = {...uiStateRef.current,brushSize,};});}, []);return <canvas ref={canvasRef} />;};const abort = () => appProcess.current.requestClose({ reason: "aborted" });const loadOriginalImage = async (selection: SelectionEvent<"image">) => {if (selection.count !== 1) {return;}const draft = await selection.read();const { url } = await getTemporaryUrl({type: "image",ref: draft.contents[0].ref,});return url;};// get the mouse position relative to the canvasconst getCanvasMousePosition = (canvas: HTMLCanvasElement,event: PointerEvent,) => {const rect = canvas.getBoundingClientRect();const scaleX = canvas.width / rect.width;const scaleY = canvas.height / rect.height;return {x: (event.clientX - rect.left) * scaleX,y: (event.clientY - rect.top) * scaleY,};};
TYPESCRIPT
# Image editing overlayDemonstrates how to create an overlay interface for editing selected images in a design. Shows surface detection for rendering different components based on whether the app is in the object panel or image overlay context.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 general asset handling, or other overlay examples in the apps ecosystem.NOTE: This example differs from what is expected for public apps to pass a Canva review:- Error handling is simplified for demonstration. Production apps must implement comprehensive error handling with clear user feedback and graceful failure modes- State management across surfaces is basic for demonstration. Production apps should implement proper state management and data persistence- Internationalization is not implemented. Production apps must support multiple languages using the `@canva/app-i18n-kit` package to pass Canva review requirements
MARKDOWN
API Reference
- App UI Kit
appProcess.broadcastMessage
appProcess.current.getInfo
appProcess.current.requestClose
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)