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 { 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 editing
overlay. Add an image to your design, select it, and click "Open
Overlay" to start editing the selected image.
</Text>
{isOpen ? (
<>
<FormField
label="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,
})
}
/>
)}
/>
<Button
variant="primary"
onClick={() => closeOverlay({ reason: "completed" })}
stretch
>
Save Overlay
</Button>
<Button
variant="primary"
onClick={() => closeOverlay({ reason: "aborted" })}
stretch
>
Cancel Overlay
</Button>
</>
) : (
<>
<Button
variant="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 state
const uiState = appContext.launchParams;
uiStateRef.current = uiState;
// set up canvas
const 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 canvas
const 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` signal
if (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 handler
return 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 canvas
const 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 overlay
Demonstrates 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

Need Help?