Polotno Docs
Side panel

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 onSelect handler for both click and drop interactions. Use the optional element argument 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: loadMore receives no arguments, so keep pagination state outside the component. Call loadMore only when isLoading is false to 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

PropTypeDefaultDescription
images`ImageType[]undefined`[]
getPreview(image: ImageType) => stringReturns the image URL used for the thumbnail and drag payload.
onSelect(image: ImageType, pos?, element?, event?) => voidHandles both click and drop; receive drop coordinates and target element when available.
isLoadingbooleanfalseShows the spinner at the end of each column and blocks automatic loadMore triggers.
loadMore`() => voidfalsenull
getCredit(image: ImageType) => React.ReactNodeundefinedRenders a credit overlay over the image; hidden on desktop hover until active.
getImageClassName(image: ImageType) => stringundefinedAdds custom class names to the <img> tag for further styling.
rowsNumbernumber2Sets the number of columns; the grid distributes items evenly by index.
crossOriginstring'anonymous'Controls the crossOrigin attribute on the <img> tag to support canvas exports.
shadowEnabledbooleantrueToggles the drop shadow around image containers.
itemHeight`numberstring`'auto'
erroranyundefinedWhen truthy, the grid shows the localized error message instead of thumbnails.
hideNoResultsbooleanfalseSuppresses 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);
  }}
/>;