Utils API
Helper components and hooks for building custom side panels
When building a custom side panel feel free to choose any React components you prefer. You can build your own one from scratch as well.
Displaying a grid of media previews is a common requirement inside custom side panels. You can reuse <ImagesGrid /> to cover layout, lazy loading, and drag-and-drop without rebuilding those behaviors yourself.
ImagesGrid component
Use <ImagesGrid /> to render any collection as clickable previews. You provide the data array and the preview accessor, and the component renders responsive columns, shows loading states, and wires drag events into the canvas. Items can be clicked or dragged, letting you decide how to create or update elements in the workspace.
- Render arbitrary data as image thumbnails with lazy loading
- Trigger canvas updates on click or drag
- Request more data automatically when the list runs low
Basic usage
Start with a minimal panel that passes the image collection, a preview getter, and an onSelect callback. The handler receives the original item plus optional drop coordinates and the element that was targeted on the canvas.
import { ImagesGrid } from 'polotno/side-panel/images-grid';
const images = [
{ id: '1', url: 'https://picsum.photos/seed/1/600/800' },
{ id: '2', url: 'https://picsum.photos/seed/2/600/800' },
];
export const TemplatesPanel = ({ store }) => (
<ImagesGrid
images={images}
getPreview={(item) => item.url}
onSelect={(image, pos, element) => {
const width = 200;
const height = 200;
const x = (pos?.x ?? store.width / 2) - width / 2;
const y = (pos?.y ?? store.height / 2) - height / 2;
store.activePage?.addElement({
type: 'image',
src: image.url,
width,
height,
x,
y,
});
}}
isLoading={false}
/>
);Tip: The component calls your
onSelecthandler for both click and drop interactions. Use the optionalelementargument to detect when a user drops onto an existing node and branch your logic.
Infinite loading
loadMore fires when the scroll position approaches the bottom or when the grid finishes rendering but still cannot fill the viewport. Pair it with isLoading and optional API helpers to keep results flowing.
import { useInfiniteAPI } from 'polotno/utils/use-api';
const { data, isLoading, loadMore } = useInfiniteAPI({
getAPI: ({ page }) => `https://example.com/images?page=${page}`,
getSize: (firstResult) => firstResult.total_pages,
});
const images = data?.flatMap((page) => page.results) ?? [];
<ImagesGrid
images={images}
getPreview={(item) => item.src}
onSelect={(image) => addImageToStore(image)}
isLoading={isLoading}
loadMore={loadMore}
hideNoResults={false}
/>;Tip:
loadMorereceives no arguments, so keep pagination state outside the component. CallloadMoreonly whenisLoadingisfalseto avoid overlapping requests.
Drag and drop behavior
Dragging an item registers a temporary DOM drop listener through Polotno’s registerNextDomDrop. You receive canvas coordinates and the optional target element, enabling both “create new” and “replace existing” flows with a single handler. The component clears the listener after each drag cycle so you do not need manual cleanup.
Note: If you build a custom drop zone outside of the canvas, call
registerNextDomDrop(null)when the drag ends to avoid dangling handlers.
Styling options
Control the layout with rowsNumber (columns), explicit itemHeight, and the shadowEnabled flag. Every image is responsive by default, and credits can be shown via getCredit to comply with provider requirements.
Note: On mobile breakpoints the credit overlay stays visible, so keep the element short and avoid interactive children.
Prop reference
| Prop | Type | Default | Description |
|---|---|---|---|
images | `ImageType[] | undefined` | [] |
getPreview | (image: ImageType) => string | — | Returns the image URL used for the thumbnail and drag payload. |
onSelect | (image: ImageType, pos?, element?, event?) => void | — | Handles both click and drop; receive drop coordinates and target element when available. |
isLoading | boolean | false | Shows the spinner at the end of each column and blocks automatic loadMore triggers. |
loadMore | `() => void | false | null |
getCredit | (image: ImageType) => React.ReactNode | undefined | Renders a credit overlay over the image; hidden on desktop hover until active. |
getImageClassName | (image: ImageType) => string | undefined | Adds custom class names to the <img> tag for further styling. |
rowsNumber | number | 2 | Sets the number of columns; the grid distributes items evenly by index. |
crossOrigin | string | 'anonymous' | Controls the crossOrigin attribute on the <img> tag to support canvas exports. |
shadowEnabled | boolean | true | Toggles the drop shadow around image containers. |
itemHeight | `number | string` | 'auto' |
error | any | undefined | When truthy, the grid shows the localized error message instead of thumbnails. |
hideNoResults | boolean | false | Suppresses the default “no results” label when the data array is empty. |
How to use useInfiniteAPI hook?
In scenarios where an API is called to display a list of images within a side panel grid, various React tools are at your disposal. This includes utilizing fetch methods within hooks or leveraging any suitable library for the task. Additionally, Polotno provides the useInfiniteAPI hook as a convenient option. The useInfiniteAPI hook serves as a wrapper around the swr library, simplifying the process of fetching data.
import { useInfiniteAPI } from 'polotno/utils/use-api';
export const SidePanel = () => {
const { data, isLoading, loadMore, isReachingEnd, hasMore, reset, error } = useInfiniteAPI({
// a function that will return a URL to request
getAPI: ({ page, query }) => `https://example.com/api?page=${page}&query=${query}`,
// default search query for the first call call
defaultQuery: '',
// timeout before making a new call when you change search query, useful for debouncing
timeout: 500,
// a function that should return number of pages available from the first API response
// usually API response has a "totalPages", "size" or other field that tells you how many pages are available
getSize: (firstResult) => firstResult.total_pages,
// a function to make an API request
// here is example of default fetch function
// you can customize it to add for example headers
fetchFunc: (url) => fetch(url).then((r) => r.json()),
});
// data - is an array of responses from the API
// each item in the array corresponds to a page
// loadMore - function to be called when you want to request for more data
// ImagesGrid will use it when you scrape the bottom of the list
// isReachingEnd - true if you are at the end of the list
// hasMore - true if you can request for more data
// reset - function to be called when you want to reset the list
// error - error object if something went wrong
};How to drop elements from side panel into workspace?
If you prefer not to use <ImagesGrid /> component, you will have to handle drag&drop of DOM elements implementation. However, Polotno has some tools to listen to drop events on the workspace. You can use this:
import { unstable_registerNextDomDrop } from 'polotno/config';
// then in your components inside side panel you can do something like this:
<img
draggable
src={url}
onDragStart={() => {
registerNextDomDrop((pos, element) => {
// "pos" - is relative mouse position of drop
// "element" - is element from your store in case when DOM object is dropped on another element
// you can just create new element on drop position
// or you can update existing element
// for example we can drop image from side panel into existing 'image' element in the workspace
if (element && element.type === 'image') {
// you can update any property you want, src, clipSrc, border, etc
element.set({ src: url });
return;
}
// or we can just create a new element
store.activePage.addElement({
type: 'image',
src: url,
x: pos.x,
y: pos.y,
width: 100,
height: 100,
});
});
}}
onDragEnd={() => {
registerNextDomDrop(null);
}}
/>;