Masonry

Masonry layout implementation using the App UI Kit.

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 masonry
    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 {
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 assets
const 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) => (
<MasonryItem
targetWidthPx={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 mount
useEffect(() => {
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 images
previewUrl: 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) => (
<MasonryItem
targetWidthPx={image.width}
targetHeightPx={image.height}
key={`MasonryItem-${index}`}
>
<ImageCard
ariaLabel="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 from
the App UI Kit with static example images.
</Text>
<InfiniteScroll
loadMore={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 data
const 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 API
const 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 service
export 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 height
const width = Math.floor(height * (Math.random() * 0.5 + 1));
return {
height,
width,
};
});
}
TYPESCRIPT
# UI masonry layout
Demonstrates 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?