Skip to main content
ASoc
Tutorial

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.

The ASoc Team8 min read

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

ApproachReserves space before loadResponsive by itselfUsed here
aspect-[w/h] on a wrapperYesYes15×
aspect-squareYesYes
aspect-videoYesYes0× — the ratio is spelled numerically
width/height on <img> aloneYes, in modern browsersRatio only, not a fixed boxEverywhere, alongside the above
Fixed h-[220px]YesNo — one height at every widthNot used for media
Padding-bottom percentage hackYesYesNot used — obsolete since aspect-ratio shipped
NothingNo

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

SymptomCauseFix
aspect-* has no visible effectThe element has an explicit height that wins, or it's display: inlineRemove the fixed height; ensure the element is block/flex/grid
The box holds its ratio but the image distortsThe image's own ratio differs from the box'sAdd object-cover to crop, or object-contain to letterbox
Content still jumps when images loadThe ratio is on the image but the wrapper collapsesPut aspect-* on the wrapper when the image is absolutely positioned, as TemplateCard does
The wrapper is right, the crop is wrongobject-cover centres by default, cutting the top off a screenshotAdd object-top — exactly what TemplateGallery does for page screenshots
aspect-[530 / 330] generates nothingSpaces inside brackets break the class nameWrite aspect-[530/330]; use underscores where a real space is required
Ratio correct, image blurryThe served file is narrower than the box at DPR 2Serve ~2× the CSS width — the reason CARD_VARIANT_WIDTH is 1060 for a 530px card
aspect-video and aspect-[16/9] behave differentlyThey don't — the CSS is identicalPick 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.

Keep reading

Tutorial9 min read

Tailwind Button: 15 Utilities, 3 Variants, and Zero Buttons

The component called Button renders a link all 22 times it is used, while 44 real button elements sit elsewhere. The full class list, and the disabled state it was missing.

Read more
Tutorial10 min read

What Is a Tailwind Class? 658 className Attributes, One Compiler Rule

A Tailwind class is a compiler-generated CSS rule that only exists because a literal string appeared in your source. Audited from 658 className attributes and 91 components.

Read more