Polotno Docs
Demos

Replace a Placeholder Image with a User Upload

Mark placeholder images and replace them with user-selected photos in Polotno

Use this approach when you want to give users the ability to quickly replace a default placeholder image in their design with a custom photo. This is useful for flyers, posters, and other templates where large images can be swapped out.

1. Mark an Image as a Placeholder

const width = 600;
const height = 400;
// Add a placeholder image element
// you can make you own with your own text
// placehold.co is just an example, better to use your own image
store.activePage.addElement(
  {
    type: 'image',
    src: 'https://placehold.co/600x400?text=Click+to+add+image',
    x: (store.width - width) / 2,
    y: (store.height - height) / 2,
    width,
    height,
    // custom property to identify placeholder
    custom: {
      isPlaceholder: true,
    },
  },
  { skipSelect: true }
);

2. Detect When the Placeholder Is Selected

Watch for changes in store.selectedElements. If a placeholder image is selected, show an “Upload Your Picture” button or open a file picker immediately.

3. Replace the Placeholder Image

Once a user selects an image file, set the element’s src to the URL of that file and clear out custom.isPlaceholder.

Implementation Example

Below is a minimal React-based implementation. The key points are:

  • A hidden file input used to pick images
  • A MobX reaction or state check to detect when the user selects the placeholder
  • Replacing the placeholder image with the uploaded file
import React from 'react';
import { reaction } from 'mobx';
import { getImageSize, getCrop } from 'polotno/utils/image';

export function usePlaceholderSelection(store: any) {
  const fileInputRef = React.useRef<HTMLInputElement | null>(null);

  // react to selected elements
  React.useEffect(() => {
    // Dispose reaction when component unmounts
    const dispose = reaction(
      () => store.selectedElements,
      (selectedElements: any[]) => {
        // Check if at least one selected element is a placeholder
        const placeholder = selectedElements.find(
          (el) => el.custom?.isPlaceholder
        );
        if (placeholder) {
          // Trigger the hidden file input
          fileInputRef.current?.click();
        }
      }
    );
    return () => dispose();
  }, [store]);

  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = async (loadEvent) => {
      // we will use dataURL to set the image
      // but it is recommended to upload image to the server first
      // for better performance and smaller JSON export
      const dataURL = loadEvent.target?.result as string;
      // Find the currently selected placeholder element
      const placeholder = store.selectedElements.find(
        (el: any) => el.custom?.isPlaceholder
      );
      if (!placeholder) return;

      // Get new image dimensions and calculate crop to fill placeholder (cover)
      const { width, height } = await getImageSize(dataURL);
      const crop = getCrop(placeholder, { width, height });

      placeholder.set({
        src: dataURL,
        ...crop,
        custom: { isPlaceholder: false },
      });
    };
    reader.readAsDataURL(file);
    // Reset the file input so selecting the same file triggers onChange again
    e.target.value = '';
  };

  // Return a hidden file input to be triggered by the reaction
  return (
    <input
      ref={fileInputRef}
      type="file"
      style={{ display: 'none' }}
      accept="image/*"
      onChange={handleFileChange}
    />
  );
}

3. Use the Hook in Your App

import React from 'react';
import { PolotnoContainer, SidePanelWrap, WorkspaceWrap } from 'polotno';
import { Toolbar } from 'polotno/toolbar/toolbar';
import { PagesTimeline } from 'polotno/pages-timeline';
import { ZoomButtons } from 'polotno/toolbar/zoom-buttons';
import { SidePanel } from 'polotno/side-panel';
import { Workspace } from 'polotno/canvas/workspace';

export const App = ({ store }: { store: any }) => {
  const fileInput = usePlaceholderSelection(store);

  return (
    <PolotnoContainer style={{ width: '100vw', height: '100vh' }}>
      <SidePanelWrap>
        <SidePanel store={store} />
      </SidePanelWrap>
      <WorkspaceWrap>
        <Toolbar store={store} downloadButtonEnabled />
        <Workspace store={store} />
        {fileInput}
        <ZoomButtons store={store} />
        <PagesTimeline store={store} />
      </WorkspaceWrap>
    </PolotnoContainer>
  );
};

Optional: Control How New Images Fit

If aspect ratios differ and you want predictable results, use getImageSize and getCrop from polotno/utils/image. getCrop gives a CSS-like cover behavior (fills the placeholder and may crop edges).

For a contain effect, resize the element itself using scale = Math.min(boxW / imgW, boxH / imgH) and center the result (keep cropX = cropY = 0).

import { getImageSize, getCrop } from 'polotno/utils/image';

// After you get the uploaded file as dataURL
const { width, height } = await getImageSize(dataURL);
const crop = getCrop(placeholder, { width, height });

placeholder.set({
  src: dataURL,
  ...crop,
  custom: { isPlaceholder: false },
});

Alternative UI Approaches

  • Manual Button: Display a button in the Toolbar or Tooltip that opens the file dialog.
  • Side Panel Section: Create a custom side panel tab that appears only when a placeholder is selected.

Tips

  • Use custom.isPlaceholder or any custom property to mark replaceable images.
  • getCrop provides "cover" behavior (fills the placeholder, may crop edges).
  • Crop values (cropX, cropY, cropWidth, cropHeight) are normalized 0–1 values.
  • For best results, upload images to a server first instead of using data URLs.

Live demo

On this page