Tailwind object-fit: 7 Uses, One Value, and Why object-contain Is Absent
object-cover 7×, object-contain 0×. The two patterns that establish the box, why gallery slides need object-top, and the wrapper mistake that breaks it.
Tailwind's object-* utilities set the CSS object-fit property, which decides how an image or video fills a box whose size has already been decided by something else. This storefront uses them 7 times across 5 components — and every single one is object-cover. The other four values never appear, which turns out to be the useful finding.
The short answer
object-fit controls how a replaced element (an <img>, <video>, <canvas>) resizes to fill its own box. Tailwind exposes five: object-contain (fit inside, letterboxed), object-cover (fill, crop the overflow), object-fill (stretch, ignore aspect ratio), object-none (native size, clip), and object-scale-down (the smaller of contain and none). object-cover is the only one that never distorts and never letterboxes.
The census
7 object-fit instances, 1 distinct value
3 object-position instances, 1 distinct value
7 object-cover 3 object-top
0 object-contain 0 object-center
0 object-fill 0 object-bottom
0 object-none
0 object-scale-down
Five components hold them:
| File | Instances | Box established by |
|---|---|---|
TemplateGallery.tsx | 3 | aspect-[16/9] w-full |
TemplateCard.tsx | 1 | absolute inset-0 in an aspect-[530/330] parent |
RelatedTemplateCard.tsx | 1 | absolute inset-0 in an aspect-[530/330] parent |
ProductDownloadGroup.tsx | 1 | absolute inset-0 in an aspect-[530/330] parent |
SavedTemplates.tsx | 1 | h-12 w-20 — fixed thumbnail |
One value, five files, two distinct patterns for establishing the box. That second column is where the real content is: object-fit does nothing on its own. It is a tie-breaker that only matters once width and height are both already fixed, and the interesting decision is always how they got fixed.
Pattern 1: the aspect-ratio box
// src/components/molecules/TemplateGallery.tsx
<img
alt={`${name} screenshot`}
className="aspect-[16/9] w-full object-cover object-top"
decoding="async"
height={900}
src={images[0]}
width={1600}
/>
Three classes doing three separate jobs. w-full takes the container's width; aspect-[16/9] derives the height from that width; object-cover then decides what happens to pixels that don't match the resulting box. The width={1600} height={900} attributes are not redundant with the ratio class — they give the browser the intrinsic size before CSS loads, which is what prevents layout shift. The class is what holds the box; the attributes are what let the browser reserve it early.
Note that object-cover is applied directly to the <img> here, not to a wrapper. That's the only place it can go — object-fit styles the replaced content inside the element's own box, so putting it on a parent div does nothing at all. This is the single most common way the utility silently fails.
Pattern 2: absolute fill inside a ratio parent
// src/components/molecules/TemplateCard.tsx
<div className="relative aspect-[530/330] w-full overflow-hidden rounded-xl border border-stroke-secondary">
<img
alt={`${product.name} — ${product.seoLabel}`}
className="absolute inset-0 h-full w-full object-cover"
decoding="async"
fetchPriority={priority ? "high" : undefined}
height={330}
loading={priority ? "eager" : "lazy"}
src={cardImage(product.screenshots[0])}
width={530}
/>
</div>
Here the parent owns the ratio and the image is pinned to it with absolute inset-0 h-full w-full. The two patterns produce the same rendered result, so why both?
Because this card has siblings inside that same box — a blurred hover overlay that must cover the image exactly. relative on the parent gives both children one shared positioning context, and overflow-hidden rounded-xl on the parent clips both to the same corner radius in one place. The gallery's slide has no such sibling, so it takes the shorter route and puts the ratio on the image itself. The rule that falls out: put the ratio on the parent when something else has to align to the image, on the image when nothing does.
h-full w-full is not optional alongside absolute inset-0. inset-0 sets all four offsets to zero, which stretches a normal block element — but a replaced element like <img> has an intrinsic size that wins, so without the explicit h-full w-full it sits at its natural dimensions in the top-left corner and object-cover has no box to work against.
object-top, and why a screenshot is not a photograph
object-cover crops. object-position decides where, and its default is center — correct for photographs, wrong for every image in this repo's galleries:
className="aspect-[16/9] w-full object-cover object-top"
Gallery slides are full-page screenshots of template home pages, sometimes several viewports tall. Cropped from the center, a 16:9 window lands somewhere in the middle of the page body — a slab of feature cards with no header, no headline, no navigation. Cropped from the top, it lands on the hero, which is the part a buyer is actually evaluating. All three object-top instances are in TemplateGallery, and all three exist for that reason.
The four card components don't need it: their covers are pre-cropped to the right ratio at build time by the WebP variant pipeline (cardImage() resolves a -card.webp derivative at 1060w from src/lib/imageVariants.ts), so the delivered file already matches the box and object-cover has almost nothing left to crop. object-position only becomes a decision when the source aspect ratio and the box's aspect ratio genuinely disagree.
Comparison: the five values, and what each one costs
| Utility | CSS | Aspect ratio | Fills the box | Used here |
|---|---|---|---|---|
object-cover | cover | Preserved | Yes — crops overflow | 7× |
object-contain | contain | Preserved | No — letterboxes | 0× |
object-fill | fill | Destroyed | Yes | 0× |
object-none | none | Preserved | No — clips at native size | 0× |
object-scale-down | scale-down | Preserved | No | 0× |
object-fill is the browser default (fill is object-fit's initial value) and the only one that distorts. That it has zero uses here is unsurprising; that object-contain also has zero is the result worth explaining, because contain is the instinctive "don't crop anything" choice.
Letterboxing moves the problem rather than solving it. An image that doesn't fill its box leaves bars, and the bars are whatever the parent's background is — so a mixed-ratio grid of object-contain covers renders as cards with inconsistent amounts of dead space, which reads as broken alignment rather than as faithful cropping. Every image in this codebase sits in a grid or carousel where visual consistency across cards matters more than seeing every pixel of any one cover, and screenshots[0] is authored as a cover crop in the first place. object-contain earns its place when the image's full extent is the content — a logo, a diagram, a product photo on a white field. None of those are in a card grid here.
The fixed-size exception
// src/components/molecules/SavedTemplates.tsx
className="h-12 w-20 shrink-0 rounded-lg border border-stroke-secondary object-cover dark:border-gray-700"
No ratio class and no absolute positioning — just h-12 w-20 (3rem × 5rem), a dashboard row thumbnail. h-12 w-20 is 5:3, the covers are roughly 16:10, so object-cover is doing real cropping work on every one of these. shrink-0 is load-bearing next to it: the thumbnail sits in a flex row, and without it flex would compress the width under pressure from the title text, changing the box's ratio at render time and quietly cropping differently per row.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
object-cover appears to do nothing | It's on a wrapper div, not on the <img> | Move it onto the replaced element — object-fit styles an element's own content |
| Image is stretched or squashed | No object-* class, so the fill default applies | Add object-cover |
Image ignores inset-0 and sits at native size | absolute inset-0 alone doesn't size a replaced element | Add h-full w-full alongside it |
| Cropping cuts off the important part | object-position defaults to center | Add object-top (or object-bottom/object-left) — this is why gallery slides use object-top |
| Grid cards have uneven blank space | object-contain letterboxing inconsistent source ratios | Switch to object-cover, or normalise the sources |
Corners aren't rounded despite rounded-xl | The radius is on the image but the image overflows a parent | Put overflow-hidden rounded-xl on the box that establishes the size |
| Layout shifts while images load | Ratio set in CSS only, no intrinsic size | Keep width/height attributes on the <img> as well as the ratio class |
Frequently asked questions
What is the difference between object-cover and object-contain in Tailwind?
Both preserve the image's aspect ratio. object-cover scales until the box is completely filled and crops whatever overflows; object-contain scales until the image fits entirely and leaves empty bars in the leftover space. Use cover when consistent box dimensions matter more than seeing every pixel, contain when the full image is the content.
Why does object-cover have no effect on my image?
Almost always one of two things: the class is on a wrapper element rather than the <img> itself, or the box has no fixed height, so there is no overflow to crop in the first place. object-fit is only meaningful once both dimensions are decided — pair it with an aspect-* class, explicit h-*/w-*, or absolute inset-0 h-full w-full.
Do I need object-cover if I use Tailwind's aspect-* utilities?
Yes. aspect-[16/9] sets the box's shape; it says nothing about how the image behaves inside it. Without an object-* class the image falls back to fill and distorts to the new ratio. The two utilities are a pair — every aspect-* on an <img> in this codebase has an object-cover beside it.
Where does object-position fit in?
It picks the crop anchor that object-cover and object-none use, defaulting to center. It only matters when the source and the box disagree on ratio. Here it appears 3 times, all object-top, all on full-page screenshots where the top of the page is the part worth showing.
Templates in this post
ASoc Quill, ASoc Rally and ASoc Rank are Next.js + Tailwind landing page templates using the same media conventions audited above — aspect-* boxes paired with object-cover, intrinsic width/height on every <img>, and no layout shift as covers load.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
