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> => {// Upload the image directly since we're using static URLs from Canva's example assetsconst 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",});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) => {const queuedImage = await uploadImage(image);await addElement({type: "image",ref: queuedImage.ref,altText: {text: "an example image",decorative: undefined,},});};const onDragStart = async (event: React.DragEvent<HTMLElement>,image: Image,) => {const dragData: ImageDragConfig = {type: "image",resolveImageRef: () => uploadImage(image),// 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,},};if (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
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();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)