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:example 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 { addElementAtCursor, addElementAtPoint, 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 { useFeatureSupport } from "@canva/app-hooks";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 = [addElementAtPoint, addElementAtCursor].find((fn) =>isSupported(fn),);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]);useEffect(() => {fetchImages();// Only run once on mount}, []);const addImageToDesign = async (image: Image) => {if (!addElement) {return;}// 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)}disabled={!addElement}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 CDN/database 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];if (baseImage) {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";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
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- 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
addElementAtCursoraddElementAtPointprepareDesignEditorui.startDragToCursorui.startDragToPointupload
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)