Examples
App elements
Assets and media
Fundamentals
Intents
Design interaction
Drag and drop
Design elements
Localization
Content replacement
Masonry
Masonry layout implementation using the App UI Kit.
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 masonrySHELL -
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 {Masonry,MasonryItem,ImageCard,Rows,Text,Placeholder,} from "@canva/app-ui-kit";import { useState, useRef, useEffect, useCallback } from "react";import * as styles from "styles/components.css";import type { QueuedImage } from "@canva/asset";import { upload } from "@canva/asset";import type { ImageDragConfig } from "@canva/design";import { ui } from "@canva/design";import type { Image } from "./fake_api";import { getImages } from "./fake_api";import InfiniteScroll from "react-infinite-scroller";import { generatePlaceholders } from "./utils";import { useAddElement } from "utils/use_add_element";import { useFeatureSupport } from "utils/use_feature_support";const TARGET_ROW_HEIGHT_PX = 100;const NUM_PLACEHOLDERS = 10;const uploadImage = async (image: Image): Promise<QueuedImage> => {// Use Canva's upload API to prepare images for use in designs// This creates a QueuedImage that can be referenced when adding elements to the designconst queuedImage = await upload({type: "image",mimeType: image.url.endsWith(".png") ? "image/png" : "image/jpeg",url: image.url,thumbnailUrl: image.url,width: image.width,height: image.height,aiDisclosure: "none", // Indicates no AI was used to generate this image});return queuedImage;};export const Placeholders = generatePlaceholders({numPlaceholders: NUM_PLACEHOLDERS,height: TARGET_ROW_HEIGHT_PX,}).map((placeholder, index) => (<MasonryItemtargetWidthPx={placeholder.width}targetHeightPx={placeholder.height}key={`placeholder-${index}`}><Placeholder shape="rectangle" /></MasonryItem>));export const App = () => {const [images, setImages] = useState<Image[]>([]);const [isFetching, setIsFetching] = useState(false);const [page, setPage] = useState<number | undefined>(1);const [hasMore, setHasMore] = useState(true);const isSupported = useFeatureSupport();const addElement = useAddElement();const scrollContainerRef = useRef(null);const fetchImages = useCallback(async () => {if (isFetching || !page || !hasMore) {return;}setIsFetching(true);try {const { images: newImages, nextPage } = await getImages(page);setImages((prevImages) => [...prevImages, ...newImages]);setPage(nextPage);setHasMore(nextPage != null);} finally {setIsFetching(false);}}, [isFetching, page, hasMore]);// Load first page on mountuseEffect(() => {fetchImages();}, [fetchImages]);const addImageToDesign = async (image: Image) => {// Upload image to Canva's asset system firstconst queuedImage = await uploadImage(image);// Add the uploaded image as an element to the user's designawait addElement({type: "image",ref: queuedImage.ref, // Reference to the uploaded image assetaltText: {text: "an example image",decorative: undefined,},});};const onDragStart = async (event: React.DragEvent<HTMLElement>,image: Image,) => {// Configure drag-and-drop data for Canva's design surfaceconst dragData: ImageDragConfig = {type: "image",resolveImageRef: () => uploadImage(image), // Lazy upload when drag is completed// Our mock API doesn't return a thumbnail/preview image, but for a production app// you should use real lower resolution thumbnail/preview imagespreviewUrl: image.url,previewSize: {width: image.width,height: image.height,},fullSize: {width: image.width,height: image.height,},};// Use feature detection to support different Canva editor versions:// - startDragToPoint: For fixed designs (posters, social media) that use coordinate-based positioning// - startDragToCursor: For responsive documents (presentations, docs) that slot content into text flowif (isSupported(ui.startDragToPoint)) {ui.startDragToPoint(event, dragData);} else if (isSupported(ui.startDragToCursor)) {ui.startDragToCursor(event, dragData);}};const Images = images.map((image, index) => (<MasonryItemtargetWidthPx={image.width}targetHeightPx={image.height}key={`MasonryItem-${index}`}><ImageCardariaLabel="Add image to design"onClick={() => addImageToDesign(image)}thumbnailUrl={image.url}alt={image.title}onDragStart={(event: React.DragEvent<HTMLElement>) =>onDragStart(event, image)}/></MasonryItem>));return (<div className={styles.scrollContainer} ref={scrollContainerRef}><Rows spacing="2u"><Text>This example demonstrates how apps can use the Masonry component fromthe App UI Kit with static example images.</Text><InfiniteScrollloadMore={fetchImages}hasMore={hasMore && !isFetching}useWindow={false}getScrollParent={() => scrollContainerRef.current}><Masonry targetRowHeightPx={TARGET_ROW_HEIGHT_PX}>{[...Images, ...(isFetching ? Placeholders : [])]}</Masonry></InfiniteScroll></Rows></div>);};
TYPESCRIPT
export type Image = {title: string;url: string;height: number;width: number;};export type PaginatedResponse = {nextPage?: number;pageCount: number;images: Image[];};// Static images from Canva example assets// In a real app, you would fetch images from your API instead of using static dataconst STATIC_IMAGES: Image[] = [{title: "Bee",url: "https://www.canva.dev/example-assets/images/bee.png",width: 640,height: 640,},{title: "Dolphin",url: "https://www.canva.dev/example-assets/images/dolphin.jpg",width: 640,height: 640,},{title: "Canva Logo",url: "https://www.canva.dev/example-assets/images/logo.png",width: 240,height: 240,},{title: "Puppy",url: "https://www.canva.dev/example-assets/images/puppyhood.jpg",width: 400,height: 300,},];const generateImages = (numImages: number) => {// Create variations of the static images to fill the requested number// In a real app, this function wouldn't be needed - you'd get actual image data from your APIconst images: Image[] = [];for (let i = 0; i < numImages; i++) {const baseImage = STATIC_IMAGES[i % STATIC_IMAGES.length];images.push({...baseImage,title: `${baseImage.title} ${Math.floor(i / STATIC_IMAGES.length) + 1}`,});}return images;};// Paginated api example to demo infinite scrolling.// Uses static Canva example images instead of external API.// In a real app, replace this with actual API calls to your image serviceexport const getImages = async (page: number): Promise<PaginatedResponse> => {// Wait 1 second to simulate a fetch request.// In a real app, this would be something like const response = await fetch(`/api/images?page=${page}`)await new Promise((res) => setTimeout(res, 1000));const imagesPerPage = 10;const totalImages = 50;const totalPages = Math.ceil(totalImages / imagesPerPage);const startIndex = (page - 1) * imagesPerPage;const endIndex = startIndex + imagesPerPage;// In a real app, you would make an API call here and get back the paginated results// For example: const { data: pageImages, hasMore } = await apiClient.getImages({ page, limit: imagesPerPage })const allImages = generateImages(totalImages);const pageImages = allImages.slice(startIndex, endIndex);return {pageCount: totalPages,nextPage: page < totalPages ? page + 1 : undefined,images: pageImages,};};
TYPESCRIPT
// For usage information, see the README.md file.import { AppUiProvider } from "@canva/app-ui-kit";import { createRoot } from "react-dom/client";import { App } from "./app";import "@canva/app-ui-kit/styles.css";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
type Placeholder = {height: number;width: number;};export function generatePlaceholders({numPlaceholders,height,}: {numPlaceholders: number;height: number;}): Placeholder[] {return Array.from({ length: numPlaceholders }, (_, i) => {// generate images such that width is 1-1.5x the heightconst width = Math.floor(height * (Math.random() * 0.5 + 1));return {height,width,};});}
TYPESCRIPT
# UI masonry layoutDemonstrates how to implement a masonry grid layout for displaying images with drag-and-drop functionality and infinite scrolling. Shows advanced UI patterns for browsing and selecting assets.For API reference docs and instructions on running this example, see: <https://www.canva.dev/docs/apps/examples/masonry/>.Related examples: See drag_and_drop/drag_and_drop_image for image drag-and-drop, or assets_and_media/digital_asset_management for asset browsing patterns.NOTE: This example differs from what is expected for public apps to pass a Canva review:- Mock data and static assets are used for demonstration purposes only. Production apps should host assets on a CDN/hosting service and use the `upload` function from the `@canva/asset` package- Error handling is simplified for demonstration. Production apps must implement comprehensive error handling with clear user feedback and graceful failure modes- Image optimization and loading states are not implemented. Production apps should optimize images and provide loading feedback for better user experience- 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?
- 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)