Blog 4 min read

Why Astro is cropping your images (and how to stop it)

If images rendered with Astro's <Image> component come out clipped or at the wrong aspect ratio, the cause is almost always a width/height pair that doesn't match the source file. How astro:assets fit works, and the ImageMetadata fix.

You render an image with Astro’s <Image> component, and it comes out clipped: the top of a screenshot, the middle of a photo, edges gone. There’s no overflow: hidden in sight and removing object-cover from your classes doesn’t fix it. Where did the crop come from?

From the image service itself. This one bit me on the Lexington theme grid, so here’s the mechanics and the fix.


Astro crops at transform time, not just in CSS

When you give <Image> both a width and a height, Astro doesn’t treat them as layout hints. They’re instructions to the image service: produce a file with exactly these dimensions. And since Astro 5, when the requested ratio doesn’t match the source file’s ratio, the default fit is cover — the service scales the image to fill the box and crops the overflow out of the generated file.

astro
<!-- source file is 1314×985 (landscape) -->
<Image src={thumb} width={1600} height={1881} />
<!-- output file is 1600×1881 (portrait): sides cropped away -->

That’s why no amount of CSS helps: the pixels are gone before the browser ever sees them. object-cover in your classes only controls how the already-cropped file sits in its layout box, which is the misdirection that makes this bug take an hour instead of a minute.


The fix: let the real dimensions through

For local images, Astro already knows the true size. An imported image — or one resolved through a content collection’s image() schema helper — is an ImageMetadata object carrying width, height, and format. Pass that object as src and don’t override the dimensions:

astro
---
import { Image } from "astro:assets";
import thumb from "@/images/mulberry.webp"; // ImageMetadata: 1314×985
---

<Image src={thumb} widths={[400, 800, 1200]} sizes="(max-width: 768px) 400px, 800px" />

No width/height props means Astro uses the intrinsic dimensions and ratio; widths still generates your responsive variants, each at the correct ratio. If you do want a specific display width, set only width — the height is derived from the source ratio, so nothing is cropped.

The same applies with content collections. If your schema uses image():

ts
thumbnail: z.object({ url: image(), alt: z.string() })

then entry.data.thumbnail.url is already ImageMetadata, not a string. Don’t rebuild dimensions by hand — and definitely don’t pass a hardcoded fallback height “just in case.” A hardcoded pair that doesn’t match the file is precisely how the crop gets in.


When you actually want the crop

Sometimes uniform boxes are the point: a card grid where every image must be the same shape regardless of source. Then the explicit pair is the right tool — just make it a decision instead of an accident, and pick the crop anchor deliberately:

astro
<Image src={shot} width={800} height={600} fit="cover" position="top" />

fit="contain" letterboxes instead of cropping, and fit="none" disables resizing entirely. If you’d rather crop in CSS (so the full file ships and the crop is purely visual), keep the intrinsic dimensions and do it with aspect-ratio and object-cover object-top classes instead.


The checklist

Clipped image in Astro? In order:

  1. Are you passing both width and height? Do they match the source ratio? If not, that’s your crop.
  2. Is src a string path when it could be ImageMetadata? Strings force you to supply dimensions; metadata carries the truth.
  3. Only then look at CSS: object-cover with a fixed box, aspect-ratio, overflow.

Rule of thumb: dimensions should have exactly one source of truth — the file. The moment a second one appears, whether a hardcoded height or a stale constant, the ratios will eventually disagree, and fit: cover resolves disagreements with scissors.

/Michael Andreuzza