Images And Product Image Sizing
EverShop 2.2.1 changed how storefront product images are sized. Before, each placement carried hardcoded width / height numbers. Now the store owns the aspect ratio and the resolution ceiling, and each placement only chooses a target width — the height is derived.
If your theme passes hardcoded dimensions into product-image components, it is fighting that system: your images will not match the store's configured proportions, and on a store whose original images are not square they will be visibly distorted or cropped inconsistently against core's.
ProductList's imageHeight prop was removedProductList no longer accepts imageHeight. Height is derived from the store's configured aspect ratio. The prop is gone — not deprecated — so a theme still passing it is passing an unknown prop that is silently dropped.
imageWidth survives as an optional base-width override. In almost every case you should omit it too and let the per-layout defaults apply.
Where the ratio comes from
The reference aspect ratio and the maximum resolution both come from the admin Catalog setting — the "original product image size" a merchant configures for their store.
Server-side, getProductImageDimensions() reads the catalogProductImageWidth / catalogProductImageHeight settings, falling back to the catalog.product.image.width / catalog.product.image.height config values, then to 1200 × 1200. The result is injected into the app context on every render as config.catalog.imageDimensions.
Client-side, a theme reads it with one hook.
useCatalogImageDimensions()
import { useCatalogImageDimensions } from '@components/common/useCatalogImageDimensions.js';
const dimensions = useCatalogImageDimensions();
Returns { width: number, height: number } — the store's configured original product image size. Falls back to { width: 1200, height: 1200 } when the value is absent or either dimension is not positive.
This is the store's original size. It is never the size you render at; it is the shape you render in, and the ceiling you must not exceed.
deriveProductImageSize(baseWidth, dimensions)
import { deriveProductImageSize } from '@evershop/evershop/lib/util/deriveProductImageSize';
const { width, height } = deriveProductImageSize(800, dimensions);
| Argument | Type | Description |
|---|---|---|
baseWidth | number | The placement's target width in pixels — roughly 2× the CSS display width (see below). |
original | { width?, height? } | null | undefined | The store's original size, i.e. whatever useCatalogImageDimensions() returned. |
What it does:
- Clamps
widthtooriginal.width— never request more than the store actually has. No upscaling. - Derives
heightfrom the original aspect ratio:round(width × original.height / original.width), floored at1. - Falls back to a square at
baseWidthwhen the original is missing or non-positive.
Pair the two, always:
import { useCatalogImageDimensions } from '@components/common/useCatalogImageDimensions.js';
import { Image } from '@components/common/Image.js';
import { deriveProductImageSize } from '@evershop/evershop/lib/util/deriveProductImageSize';
export function ProductTile({ product }) {
const { width, height } = deriveProductImageSize(
800,
useCatalogImageDimensions()
);
return (
<Image
src={product.image.url}
alt={product.image.alt || product.name}
width={width}
height={height}
sizes="(max-width: 768px) 100vw, 33vw"
loading="lazy"
/>
);
}
Choosing baseWidth: roughly 2× the CSS display width
baseWidth is a rendering decision, not a layout one. CSS still controls how large the image appears; width / height only tell the browser the intrinsic size, drive the aspect-ratio box, and set the ceiling for the generated srcset.
The rule core follows: pick about twice the widest CSS size the image will ever display at, so a DPR-2 (retina) screen has a sharp candidate to pick from. The <Image> srcset covers everything below that.
These are the defaults core uses today:
| Placement | baseWidth | Component |
|---|---|---|
Product list — grid layout | 800 | ProductList |
Product list — list layout | 320 | ProductList |
| Product detail — main image | 1280 | Media |
| Product detail — gallery thumbnails | 200 | Media |
| Product detail — fullscreen view | 1920 | Media |
| Cart, checkout and order-summary thumbnails | 200 | DefaultCartItemList, CartSummaryItems, OrderSummaryItems |
If your theme renders product cards much larger or much smaller than core's, change the baseWidth — not the height.
What <Image> does with those numbers
<Image> turns src + width into a /images proxy URL plus a responsive srcset:
<img
src="/images?src=%2Fassets%2Fshirt.jpg&w=800&q=75"
srcset="/images?src=%2Fassets%2Fshirt.jpg&w=400&q=75 400w,
/images?src=%2Fassets%2Fshirt.jpg&w=600&q=75 600w,
/images?src=%2Fassets%2Fshirt.jpg&w=800&q=75 800w"
sizes="(max-width: 768px) 100vw, 33vw"
width="800"
height="800"
/>
The candidate list is built by buildImageSrcSet, which is exported so art-directed <picture> sources can serve the same resized candidates:
import { buildImageSrcSet } from '@components/common/Image.js';
const srcSet = buildImageSrcSet(src, width, sizes, quality);
Its rules: parse the breakpoints out of sizes, drop anything above width × 3, and — if fewer than two candidates survive — synthesize 50% and 75% of width (never below 200px). The intrinsic width is always included, and the list is deduplicated and sorted ascending.
The /images endpoint is a storefront route that resizes on demand from src, w and q (quality, default 75).
<Image> also stamps three inline styles: maxWidth: 100%, height: auto, and — the important one — aspectRatio: "<width> / <height>".
The aspectRatio: 'auto' escape hatch
Because the ratio is inline, it beats Tailwind height classes and any stylesheet rule. That is deliberate: catalog grids depend on the hard lock for uniform tiles.
When you need an image to fill a container whose shape you control — a hero band, a bento cell, a mosaic tile — clear it through the style prop:
<Image
src={image.url}
alt={alt}
width={1800}
height={1029}
objectFit="cover"
className="absolute inset-0 h-full w-full"
style={{ height: '100%', width: '100%', aspectRatio: 'auto' }}
/>
Core uses exactly this in CategoryInfo (the category hero), BentoGrid, Section, BrandStory, CategoryMosaic, TieredCategories, TrustStrip and CollectionSpotlight.
<Image> itselfA theme that overrides @components/common/Image.js to drop the inline ratio globally will break every catalog grid at once, since the uniform-box behaviour is what the ratio lock exists for. Clear it per call site.
For art-directed <picture> markup (a portrait mobile asset swapped for a landscape desktop one), the inline ratio must go entirely — pass aspectRatio: undefined and put real width / height attributes on each <source> so the browser derives the box from the matched source. Core's Slideshow widget does this.
Migrating a theme to 2.2.1
- Remove
imageHeightfrom every<ProductList>usage. It is no longer a prop. - Remove
imageWidthtoo, unless your tiles are genuinely a different size from core's — then set it to about 2× your CSS width. - Audit any forked product card, gallery or cart-item component for hardcoded
width={…} height={…}on product images, and replace them withderiveProductImageSize(baseWidth, useCatalogImageDimensions()). - Check non-square stores. Set the admin Catalog original image size to something like
1200 × 1600and reload your listing, PDP, cart and checkout. Any placement still rendering a square box is one you missed. - Leave non-product images alone. Banner, slideshow and CMS widget images carry their own stored dimensions; this system is specific to catalog imagery.
Before:
<ProductList products={products} imageWidth={720} imageHeight={720} />
After:
<ProductList products={products} />
See also
- Image — the component reference: props, srcset, loading strategies
- ProductList — the listing component
- Templating — overriding and extending core components
- Upgrading To React 19 — the other breaking changes in the 2.2.x line
Support us
EverShop is an open-source project that relies on community support. If you find our project useful, please consider sponsoring us.