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.isPlaceholderor any custom property to mark replaceable images. getCropprovides "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.