A Strapi plugin that allows you to seamlessly crop, resize, and edit images without leaving the admin panel.
released August 6, 2026
npm install strapi-plugin-croppable-mediaCroppable Media adds a Strapi custom field for selecting one image and creating multiple editor-controlled crops from it.
It is useful for sites where the same image appears in several layouts, such as hero banners, cards, grids, social previews, and portrait slots.
npm install strapi-plugin-croppable-mediaEnable the plugin in config/plugins.ts.
export default () => ({
'croppable-media': {
enabled: true,
},
});For local plugin development, use resolve.
export default () => ({
'croppable-media': {
enabled: true,
resolve: './src/plugins/croppable-media-v5',
},
});Open the Croppable Media plugin page in the Strapi admin panel to manage configuration.
The dashboard stores settings in Strapi's plugin store. Values from config/plugins.ts are only used as initial defaults and fallback values.
cropSizesDefines the crop tabs shown to editors.
cropSizes: [
{ label: 'hero', ratio: [21, 9], maxWidth: 2400 },
{ label: 'portrait', ratio: [3, 4], maxWidth: 900 },
]Each crop size supports:
| Property | Type | Required | Description |
|---|---|---|---|
label | string | yes | Format key saved in the field value. |
ratio | [number, number] | yes | Crop aspect ratio as width and height. |
maxWidth | number | no | Maximum generated crop width. |
maxFileSizeMaximum target crop data size before the plugin reduces output quality and width.
Default:
maxFileSize: 2000000fallbackMaxWidthWidth used when the output optimizer needs to shrink a crop that does not have a crop-specific maxWidth.
Default:
fallbackMaxWidth: 1920deletePreviousCropDeletes an existing provider file with the same crop hash before uploading the replacement crop.
This is enabled by default so re-cropping an existing crop keeps the same predictable hash and replaces the provider object. The crop hash changes only when a different source asset hash or field asset hash is used.
Default:
deletePreviousCrop: trueSet this to false if you want to keep previous provider objects instead of replacing them.
Add a custom field in Content-Type Builder.
Choose:
Custom fields -> Croppable MediaThe field stores the selected upload file data plus generated crop formats in JSON.
Example field shape:
{
"id": 1,
"name": "image.webp",
"url": "/uploads/image.webp",
"formats": {
"hero": {
"url": "https://cdn.example.com/generated-hero.webp",
"width": 2400,
"height": 1028
},
"square": {
"url": "https://cdn.example.com/generated-square.webp",
"width": 1000,
"height": 1000
}
}
}Use the crop format that matches the layout. Each configured crop size is saved under image.formats by its label.
For example, this crop config:
cropSizes: [
{ label: 'hero', ratio: [21, 9], maxWidth: 2400 },
{ label: 'card', ratio: [4, 3], maxWidth: 1200 },
{ label: 'square', ratio: [1, 1], maxWidth: 1000 },
]Creates crop URLs like:
entry.image.formats.hero.url;
entry.image.formats.card.url;
entry.image.formats.square.url;getImageURLUse a small formatter helper in the frontend so components do not need to know the fallback rules.
type CroppableImageFormat = {
url?: string | null;
};
type CroppableImage = {
url?: string | null;
formats?: Record<string, CroppableImageFormat | undefined> | null;
};
type GetImageURLOptions = {
baseURL?: string;
};
export const getImageURL = (
image?: CroppableImage | null,
format?: string,
options: GetImageURLOptions = {}
) => {
if (!image) {
return '';
}
const url = format ? image.formats?.[format]?.url || image.url : image.url;
if (!url) {
return '';
}
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) {
return url;
}
const baseURL = options.baseURL || process.env.NEXT_PUBLIC_STRAPI_URL || '';
return `${baseURL}${url}`;
};Usage:
const heroURL = getImageURL(entry.image, 'hero');
const cardURL = getImageURL(entry.image, 'card');
const originalURL = getImageURL(entry.image);If the requested crop does not exist, getImageURL falls back to image.url.
<img
src={getImageURL(entry.image, 'hero')}
alt={entry.image?.alternativeText || ''}
/>import Image from 'next/image';
<Image
src={getImageURL(entry.image, 'card')}
alt={entry.image?.alternativeText || ''}
width={1200}
height={900}
/>const imageFormats = {
hero: 'hero',
listingCard: 'card',
authorAvatar: 'square',
};
const src = getImageURL(entry.image, imageFormats.listingCard);npm install
npm run build
npm run watchMIT
Share your work with the community and get it listed in the Strapi ecosystem for everyone to discover and use.
Submit
