Your hero stops jumping and still crops: two properties, two jobs
· 9 min read
CSS aspect-ratio stops the page jumping but not the crop. What aspect-ratio, object-fit and width/height attributes each fix, and why none works alone.

Set aspect-ratio: 16 / 9 on a hero image and one thing goes right. The browser knows the height before the file arrives, so the paragraph underneath is not shoved down the page mid-load. That is the CSS aspect ratio property doing its single job, and it does that job well, the same whether the image is a 16:9 video still, a 4:3 photograph, or a screenshot somebody pasted in at the wrong shape.
The second thing does not happen. The box is now a fixed shape, but nothing has told the browser what to do with a picture that does not fill it. It stretches, it bars, or it crops, and which one you get is decided by a property you have not written yet. aspect-ratio guarantees the frame. It does not choose the framing.
The clearest evidence for that claim sits in our own repository. The ToolSura aspect ratio calculator emits three lines of CSS for any ratio you type. Two of them, plus a pre-CSS-era fallback, define the box. None of them (and nothing else in the app) tells the browser what happens to the picture inside it.
What the calculator gives you
The CSS in that snippet comes from one function. buildCss(ratioW, ratioH) occupies lines 170 to 191 of src/lib/aspect-ratio-tools.ts, takes two numbers, and returns an object with exactly three fields. Type 16 and 9 into the tool and the panel shows three copyable rows captioned CSS, Tailwind and Legacy fallback:
aspect-ratio: 16 / 9;
aspect-video
padding-top: 56.25%;
| Field | What it holds at 16:9 | Does it branch? |
|---|---|---|
aspectRatio |
aspect-ratio: 16 / 9; |
No. One string shape for every input. |
tailwind |
aspect-video |
Yes: the only conditional in the function, lines 176 to 183. |
paddingFallback |
padding-top: 56.25%; |
No. |
The conditional is easy to misdescribe, so it is worth reading properly. The check on line 176 is w === 1 && h === 1, and it is a Tailwind-value branch, not a CSS one: it exists to hand back aspect-square instead of the generic aspect-1/1. Line 178 does the same for 16:9 with aspect-video. Line 180 catches any pair of integers as aspect-4/3, and line 183 falls back to the arbitrary-value form aspect-[1.91/1] for anything non-integer. Every ratio produces the same shape of CSS declaration. Only the Tailwind value varies.
Two details before the turn. The ratio is reduced first: reduceRatio runs on line 171, so 32:18 and 16:9 both print aspect-ratio: 16 / 9;. And there is no selector. The function returns bare declarations with nothing in front of them, because a ratio calculator cannot know your class name. There is no @media block either, so nothing in that output changes at a breakpoint.
The property it never emits
object-fit appears nowhere in the ToolSura app.
A repository-wide search for that string across src/ returns no files at all: not the 227-line library, not the client component that drives the page, not the snippet panel that renders the three rows you have just read. Inside the tool the other five candidates behave the same way: no max-width, no width declaration, no height declaration, no object-position, and no element selector in front of the bare declarations.
This is not a defect report. The calculator's job is arithmetic (reduce the ratio, print the declaration, compute the old padding-top technique) and a framing decision is not arithmetic. Choosing whether a photograph loses its edges or gains letterbox bars is a judgement about the picture, and no division can make it for you.
It is still the sharpest demonstration of the point available. Here is a working, tested tool that produces the box specification and not the picture specification. A ratio is necessary and not sufficient, and the distance between those two words is one grep away.
What aspect-ratio does to the box
MDN's aspect-ratio reference states the rule that catches people most often: "At least one of the box's sizes needs to be automatic in order for aspect-ratio to have any effect. If neither the width nor height is an automatic size, then the provided aspect ratio has no effect on the box's preferred sizes."
In practice this works:
.hero { width: 100%; aspect-ratio: 16 / 9; }
And this does nothing at all, silently:
.hero { width: 100%; height: 420px; aspect-ratio: 16 / 9; }
No error, no warning, no highlight in developer tools. The declaration parses, sits in the cascade, and is discarded because both dimensions are now definite. If your ratio "is not working", count your fixed dimensions before changing anything else.
There is a second mode that matters more for images than the first. When both auto and a ratio are given (aspect-ratio: auto 16 / 9), MDN describes the sequence precisely: the specified ratio is used "until the content is loaded", after which "the intrinsic aspect ratio of the loaded content is used". That reservation was for the jump, not for the final shape. Plain aspect-ratio: 16 / 9 keeps the ratio you wrote. aspect-ratio: auto 16 / 9 uses it as a placeholder and then defers to the file.
Either way the property's remit ends at the box. What goes inside that box is a second decision, and it has a second property. For what a ratio means and how named ratios behave, see Aspect Ratio Explained; this post is about the layout.
The five values of object-fit
object-fit "sets how the content of a replaced element, such as an <img> or <video>, should be resized to fit its container." It applies to replaced elements only. A <div> with a background image is governed by background-size instead, which has its own values and its own syntax.
| Value | What it does | What a reader sees |
|---|---|---|
fill |
The initial value. Content fills the content box; when the ratios disagree it is stretched. | A distorted photo |
contain |
Scaled to keep its own ratio and fit inside the box; MDN calls the result "letterboxed" or "pillarboxed". | Bars above and below, or at the sides |
cover |
Scaled to keep its own ratio and fill the whole box; content is "clipped to fit". | The crop. Edges gone. |
none |
Content is not resized at all. | Overflow at native size |
scale-down |
Behaves as none or contain, whichever yields the smaller concrete size. |
Never enlarges past native |
Four of those five exist to solve a mismatch that aspect-ratio creates and cannot resolve. Pick one and the crop stops being an accident:
.hero img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
Once cover has chosen to crop, object-position decides which part survives. Its default is the centre of the box, which for a portrait photograph cropped to a wide hero takes the top of the head first. object-position: 50% 30% biases the crop upward. That property is not in the app either, so the calculator offers no value for it.
The third leg: width and height
Before CSS touches an image, the browser has already parsed the tag. Two attributes there do the same reservation job as aspect-ratio, earlier and at no cost.
MDN's <img> reference puts it directly: "Including height and width enables the aspect ratio of the image to be calculated by the browser prior to the image being loaded. This aspect ratio is used to reserve the space needed to display the image, reducing or even preventing a layout shift when the image is downloaded and painted to the screen."
<img src="hero.jpg" width="1600" height="900" alt="…">
Two numbers, no stylesheet, no cascade, no specificity fight. They also work where no CSS reaches at all: a CMS content field, an email template, a Markdown renderer you do not control.
Their limitation is that they scale nothing. An image with an intrinsic width of 1600 will overflow a 400-pixel column whatever you put in the attributes, which is why they travel with two lines of their own:
img { max-width: 100%; height: auto; }
What this has to do with layout shift
That reservation exists for a reason, and the reason has a name. Cumulative Layout Shift is the metric for visual stability, and web.dev defines the underlying event in one line: "A layout shift occurs any time a visible element changes its position from one rendered frame to the next." Individual shifts group into a burst: a session window, where shifts arrive "in rapid succession with less than 1-second in between each shift and a maximum of 5 seconds for the total window duration". The page is judged on its largest such burst.
web.dev's guidance is unambiguous about the target: sites "should strive to have a CLS score of 0.1 or less", with good values at 0.1 or below and "Poor values" above 0.25. Its list of causes names "images with unknown dimensions" among the usual suspects.
The nuance in that article is what makes the whole system make sense, and it is the sentence most people quote only halfway:
"Note that layout shifts only occur when existing elements change their start position. If a new element is added to the DOM or an existing element changes size, it doesn't count as a layout shift — as long as the change doesn't cause other visible elements to change their start position."
So the image's own box resizing is not the score. What is scored is what that resizing does to everything around it. An image that arrives late and tall shoves the headline, the byline and the two paragraphs beneath it downward, and all of those count. Reserve the box in advance and none of them move.
That is the entire argument for the aspect-ratio declaration, and it stops exactly where the box stops. Fix the shift and you have earned a fixed frame. What fills that frame is still undecided.
Responsive images are a different lever
Choosing which file to download and deciding what shape the frame is are separate problems with separate syntax. srcset and <picture> select from candidates the browser then fetches; aspect-ratio and object-fit shape the box that file lands in. The same frame rendered from a 4:3 crop of a 16:9 file and from a 16:9 crop of a 4:3 file is the same box: the difference is one object-fit value, not one extra entry in a srcset. Getting file selection right while leaving the box unspecified is the most common way a fast, well-optimised hero still looks wrong. The image tools guide covers choosing and preparing the files that reach this point; this post is about the frame they land in.
The whole system, in one block
Every problem above has an owner, and three of them are answered by three different mechanisms:
<figure class="hero">
<img src="team.jpg" width="1600" height="900" alt="…">
</figure>
.hero {
aspect-ratio: 16 / 9;
}
.hero img {
display: block;
width: 100%;
height: 100%;
max-width: 100%;
object-fit: cover;
object-position: 50% 35%;
}
widthandheighton the tag reserve the space before the CSS loads. This is the layout-shift fix.aspect-ratioon the container fixes the shape of the frame, at every viewport rather than the one you tested.object-fit: coverdecides what happens to a picture that does not fill that frame. Swap it tocontainand you get bars instead of a crop.object-positiondecides which slice survives the crop. Move it to50% 50%and whatever you had framed is gone.
Only one of those four came out of the calculator with the ratio, and the calculator hands you that line and stops there. That is not a gap in the arithmetic. It is the reason the step after computing a ratio is writing the declaration. Open the calculator when you need the numbers; these four lines are what you do with them. And if the picture is simply the wrong shape, the image resizer hands you a different file instead.
Related Tools & Further Reading
- Aspect Ratio Explained — what a ratio is, the six-rung 16:9 ladder, and what a 4:3 to 16:9 switch costs
- CSS Flexbox Generator — build the layout that hosts the hero; Flexbox vs Grid covers when the parent grid is the constraint
- Image Resizer — change the file when the crop you need is not the crop you have
- Compress Images for Web — the other half of hero weight, once the frame is right

Written by
Sunil Kapoor
My attention goes to the two decisions that are made once and then applied to a hundred files. Aspect ratio is the first. A resize that forces a source image into a fixed frame either crops or distorts, and both are visible. The choice of which depends on the destination: a profile photo wants a centre crop, a banner wants letterboxing or a regenerated design, and a product photo wants neither.
Deciding once and applying it to everything is how a batch job produces two hundred files with the subject cut off. Upscaling is the second. A larger image cannot be recovered from a smaller one, so enlarging produces a file with more pixels and no more detail.
It can still be the right choice for a fixed layout, and I cover when it is acceptable versus when it is a way of pretending a missing asset exists. Resampling method matters more at large reductions than people expect. Averaging and similar methods avoid the aliasing that nearest-neighbour produces, and aliasing shows up as jagged edges that look like a sharpening problem and are not.
Sharpening should come after resizing, never before, because sharpening a small image amplifies the very artefacts resizing introduces. Colour profiles need deciding once too. Resampling in the wrong space changes the appearance of the result, so a profile-aware resize and a naive one produce visibly different images from the same source. I end every page on output naming, because a batch that overwrites its own source has no undo.