Skip to main content

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 removed

ProductList 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);
ArgumentTypeDescription
baseWidthnumberThe placement's target width in pixels — roughly 2× the CSS display width (see below).
original{ width?, height? } | null | undefinedThe store's original size, i.e. whatever useCatalogImageDimensions() returned.

What it does:

  • Clamps width to original.width — never request more than the store actually has. No upscaling.
  • Derives height from the original aspect ratio: round(width × original.height / original.width), floored at 1.
  • Falls back to a square at baseWidth when 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:

PlacementbaseWidthComponent
Product list — grid layout800ProductList
Product list — list layout320ProductList
Product detail — main image1280Media
Product detail — gallery thumbnails200Media
Product detail — fullscreen view1920Media
Cart, checkout and order-summary thumbnails200DefaultCartItemList, 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.

Do not "fix" this in <Image> itself

A 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

  1. Remove imageHeight from every <ProductList> usage. It is no longer a prop.
  2. Remove imageWidth too, unless your tiles are genuinely a different size from core's — then set it to about 2× your CSS width.
  3. Audit any forked product card, gallery or cart-item component for hardcoded width={…} height={…} on product images, and replace them with deriveProductImageSize(baseWidth, useCatalogImageDimensions()).
  4. Check non-square stores. Set the admin Catalog original image size to something like 1200 × 1600 and reload your listing, PDP, cart and checkout. Any placement still rendering a square box is one you missed.
  5. 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.