Tailwind Aspect Ratio: 22 Uses, Zero of Them aspect-video
Why this codebase writes aspect-[16/9] instead, and how the card's ratio has to agree with the img's width, height and the generated file's width.
Tailwind's aspect-* utilities set the native CSS aspect-ratio property, reserving an element's height from its width before any image loads. This codebase uses 22 of them across 7 distinct ratios — and not one is aspect-video, despite aspect-[16/9] appearing three times. That's deliberate, and it's the most useful thing in the census.
The short answer
aspect-square is aspect-ratio: 1 / 1. aspect-video is 16 / 9. aspect-<w>/<h> takes a ratio directly (aspect-3/2), and square brackets accept any value verbatim (aspect-[530/330]). The property makes the browser compute height from the element's rendered width, so a media box holds its space from first paint instead of collapsing to zero and snapping open when the image arrives.
That last clause is the entire point. An image with no reserved box contributes a layout shift when it loads — content below it jumps — and layout shift is a scored Core Web Vital, not a cosmetic complaint.
The full census
Counted across src/:
22 aspect-* instances, 7 distinct ratios
7 aspect-square TechStackCard, PluginCard, 5× in data/techStack.tsx
5 aspect-[530/330] TemplateCard, RelatedTemplateCard, UseCaseCard,
ProductDownloadGroup (×2)
3 aspect-[570/408] FeatureTabs image column
3 aspect-[16/9] TemplateGallery (slides, thumbnails, single-image frame)
2 aspect-[570/440] FeatureTabs image column
1 aspect-[443/224] BlogCard cover
1 aspect-[204/277] Hero's floating preview card
0 aspect-video never used
0 aspect-auto never used
Two clusters do almost all the work: icon wells (aspect-square) and product cover art (aspect-[530/330]). The rest are one- or two-off ratios lifted from a specific design composition.
Why aspect-[16/9] and not aspect-video
They compile to identical CSS. aspect-video resolves to aspect-ratio: 16 / 9; so does aspect-[16/9]. The choice is about what the class says, and it's worth spelling out because "use the named utility" is the reflexive advice.
aspect-video is a semantic name — it means "the shape a video is." The three places this ratio appears are the product screenshot carousel:
// src/components/molecules/TemplateGallery.tsx
<img
className="aspect-[16/9] w-full object-cover object-top"
…
/>
These are static screenshots of a template's pages. Nothing here is a video, nothing will become one, and a future reader grepping aspect-video to find the site's video embeds would land on a screenshot carousel instead. The numeric form describes the geometry without claiming a medium.
This cuts the other way too, and it's a fair objection: if the storefront ever does embed video, aspect-video is exactly the right class for it, and having spent the name on screenshots would be the mistake. That's precisely why it's unspent here.
The ratio that has to agree with three other numbers
aspect-[530/330] is the product card's cover, and it is not a free choice — it's one of four values that all have to match or the card breaks in a specific way:
// 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>
The wrapper reserves 530/330. The <img> carries intrinsic width={530} height={330}, which is the same ratio expressed the way the browser reads it before CSS applies. And the file cardImage() resolves to is built at a matching width:
// src/lib/imageVariants.ts
/** Card thumbnails: 530px CSS wide at DPR 2, so 1060 covers every display. */
export const CARD_VARIANT_WIDTH = 1060;
1060 is 530 × 2 — one CSS pixel of card, two device pixels of image, which is what a retina display asks for and the point past which extra resolution is invisible. The same doc comment records why the derivative exists at all: the source cover is a 2120px JPEG that "costs ~255 KiB to deliver roughly 33 KiB worth of pixels" at card size.
So the aspect ratio isn't decoration. It's the contract that lets a plain <img> behave: the box is reserved before the request starts, the intrinsic attributes agree with the box, and the bytes fetched are sized for the box. Break any one and you get either a layout shift or wasted transfer. This is also why this project can skip next/image entirely — right-sizing happens at build time, so there's no request-time optimizer and no per-image platform cost.
The same pattern repeats wherever an image is fixed-shape, including in the data layer:
// src/data/featureTabs.tsx
<div className="shadow-feature aspect-[570/408] max-w-[570px] overflow-hidden rounded-3xl border border-gray-100">
<img
alt="ASoc Admin dashboards"
decoding="async"
height="408"
loading="lazy"
src="/images/templates/asoc-admin/feature-dashboards.webp"
aspect-[570/408], max-w-[570px], height="408" — the same three numbers again.
aspect-square without an image
Seven instances, and none of them wrap a photo. They're icon wells:
// src/components/molecules/TechStackCard.tsx
<span className="flex aspect-square w-12 items-center justify-center rounded-full bg-[linear-gradient(180deg,#38BDF8_0%,#52CAFF_100%)]">
aspect-square w-12 gives a perfect circle from one dimension. The alternative — h-12 w-12 — produces the same rendered box, and this codebase uses that pairing far more often for icons elsewhere. The difference is what happens when the width changes: a responsive w-11 md:w-14 stays circular by itself under aspect-square, while an h-*/w-* pair needs both values updated at every breakpoint and silently becomes an ellipse the day someone updates only one.
That's the general rule worth taking away. Use aspect-square when the width is the single source of truth and height should follow it. Use h-*/w-* when both are fixed and you want them stated explicitly — which is the far more common case here, since most of this codebase's 139 h-* instances are icon-sizing pairs written out in full.
Comparison: reserving space for media
| Approach | Reserves space before load | Responsive by itself | Used here |
|---|---|---|---|
aspect-[w/h] on a wrapper | Yes | Yes | 15× |
aspect-square | Yes | Yes | 7× |
aspect-video | Yes | Yes | 0× — the ratio is spelled numerically |
width/height on <img> alone | Yes, in modern browsers | Ratio only, not a fixed box | Everywhere, alongside the above |
Fixed h-[220px] | Yes | No — one height at every width | Not used for media |
| Padding-bottom percentage hack | Yes | Yes | Not used — obsolete since aspect-ratio shipped |
| Nothing | No | — | — |
Modern browsers already derive an aspect ratio from an <img>'s width/height attributes, which raises a reasonable question: why also write aspect-[530/330] on the wrapper? Because the wrapper is a <div> with a border and overflow-hidden, and the image inside it is absolute inset-0. An absolutely positioned child contributes nothing to its parent's height, so without the utility the wrapper would collapse regardless of what the image knows about itself.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
aspect-* has no visible effect | The element has an explicit height that wins, or it's display: inline | Remove the fixed height; ensure the element is block/flex/grid |
| The box holds its ratio but the image distorts | The image's own ratio differs from the box's | Add object-cover to crop, or object-contain to letterbox |
| Content still jumps when images load | The ratio is on the image but the wrapper collapses | Put aspect-* on the wrapper when the image is absolutely positioned, as TemplateCard does |
| The wrapper is right, the crop is wrong | object-cover centres by default, cutting the top off a screenshot | Add object-top — exactly what TemplateGallery does for page screenshots |
aspect-[530 / 330] generates nothing | Spaces inside brackets break the class name | Write aspect-[530/330]; use underscores where a real space is required |
| Ratio correct, image blurry | The served file is narrower than the box at DPR 2 | Serve ~2× the CSS width — the reason CARD_VARIANT_WIDTH is 1060 for a 530px card |
aspect-video and aspect-[16/9] behave differently | They don't — the CSS is identical | Pick one per codebase for consistency; this one uses the numeric form outside of actual video |
Frequently asked questions
What is aspect-video in Tailwind?
aspect-ratio: 16 / 9, the standard widescreen video shape. It's a named alias, not special behaviour — aspect-[16/9] produces the same CSS, which is what this codebase writes, since the elements at that ratio are screenshots rather than video.
Do I still need width and height on the <img> if I use aspect-*?
Yes. They're the browser's earliest hint about the image's shape, available before any CSS is applied, and they're what prevents a shift in the window between HTML parse and stylesheet evaluation. Every <img> in this project carries both alongside its wrapper's aspect-*.
Does aspect-ratio need the old padding-bottom hack as a fallback?
No. Native aspect-ratio has been supported across current browsers since Safari 15; the percentage-padding technique is legacy. Nothing in this codebase uses it.
Should I use aspect-square w-12 or h-12 w-12?
aspect-square when the width is the source of truth and the height should track it — especially with responsive widths, where the pair would need updating at every breakpoint. h-*/w-* when both dimensions are fixed and stating them plainly is clearer.
Templates in this post
ASoc Arcade, ASoc Bazaar and ASoc Blitz are Next.js + Tailwind ecommerce templates built on the media conventions audited above — reserved aspect boxes on every product image, intrinsic width/height that agree with them, and pre-sized image files behind plain <img> tags.
Browse the full sets: Next.js shop templates, Tailwind shop templates.
