Browse docs
Browse docs
A content-shape primitive. Wrap an <img> or <video> (or an iframe embed) and the container holds its proportions regardless of width — no layout jump when the media loads, no whitespace below, no JS measurement.
import { AspectRatio } from '@dashforge/tw';
<AspectRatio ratio={16 / 9}>
<img src="hero.jpg" alt="Hero" className="w-full h-full object-cover" />
</AspectRatio>{/* 16:9 video frame */}
<AspectRatio ratio={16 / 9}>
<video src="demo.mp4" className="w-full h-full object-cover" />
</AspectRatio>
{/* Square thumbnail */}
<AspectRatio ratio={1}>
<img src="avatar.jpg" alt="" className="w-full h-full object-cover" />
</AspectRatio>
{/* 4:3 photo */}
<AspectRatio ratio={4 / 3} sx="rounded-xl overflow-hidden">
<img src="photo.jpg" alt="" className="w-full h-full object-cover" />
</AspectRatio>Pass the ratio as width / height. Number form (16 / 9) and string form ("16 / 9") both work — the string form is more legible at a glance.
Without an aspect ratio lock, the browser doesn't know how tall an image will render until the bytes arrive. During load, the surrounding layout sits at 0 height where the image will go; once loaded, everything BELOW the image jumps down by the image's height. This is Cumulative Layout Shift (CLS) — Google's Core Web Vitals penalise it because it ruins reading flow.
AspectRatio reserves the right amount of space from first paint. The image fades in without disturbing anything else.
import { AspectRatio, Box, Stack, Typography } from '@dashforge/tw';
<Stack direction="row" gap={3} align="start">
<Stack gap={1} sx="flex-1">
<AspectRatio ratio={16 / 9} sx="rounded-md overflow-hidden">
<Box variant="solid" color="primary" sx="w-full h-full flex items-center justify-center">
<Typography variant="caption">16 : 9</Typography>
</Box>
</AspectRatio>
<Typography variant="caption" color="muted" align="center">video / hero</Typography>
</Stack>
<Stack gap={1} sx="flex-1">
<AspectRatio ratio={1} sx="rounded-md overflow-hidden">
<Box variant="solid" color="success" sx="w-full h-full flex items-center justify-center">
<Typography variant="caption">1 : 1</Typography>
</Box>
</AspectRatio>
<Typography variant="caption" color="muted" align="center">avatar / square</Typography>
</Stack>
<Stack gap={1} sx="flex-1">
<AspectRatio ratio={4 / 3} sx="rounded-md overflow-hidden">
<Box variant="solid" color="warning" sx="w-full h-full flex items-center justify-center">
<Typography variant="caption">4 : 3</Typography>
</Box>
</AspectRatio>
<Typography variant="caption" color="muted" align="center">photo / classic</Typography>
</Stack>
</Stack><AspectRatio ratio={1}> square (1:1)</AspectRatio>
<AspectRatio ratio={4 / 3}> classic photo (4:3)</AspectRatio>
<AspectRatio ratio={3 / 2}> 35mm (3:2)</AspectRatio>
<AspectRatio ratio={16 / 9}> video (16:9)</AspectRatio>
<AspectRatio ratio={21 / 9}> ultrawide cinema (21:9)</AspectRatio>
<AspectRatio ratio="9 / 16"> portrait video (9:16)</AspectRatio><AspectRatio ratio={16 / 9} sx="rounded-2xl overflow-hidden">
<img
src="cover.jpg"
alt="Workspace cover"
className="w-full h-full object-cover"
/>
</AspectRatio>The two critical classes on the parent:
rounded-* to round the cornersoverflow-hidden to clip the child to the rounded shape (without it, the child paints rectangular and the rounded corners do nothing visible)On the child <img>:
w-full h-full — fill the locked-ratio containerobject-cover — crop to fill, preserve aspect (vs object-contain which letterboxes)<AspectRatio ratio={21 / 9} sx="rounded-xl overflow-hidden relative">
<img src="hero.jpg" alt="" className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
<div className="absolute bottom-0 left-0 p-8 text-white">
<Typography variant="h1" color="inherit">Welcome</Typography>
</div>
</AspectRatio>AspectRatio renders a single container; multiple absolutely-positioned children stack inside cleanly because the parent has a known height (from aspect-ratio).
<AspectRatio ratio={16 / 9} sx="rounded-xl overflow-hidden">
<iframe
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
title="Video player"
className="w-full h-full"
frameBorder="0"
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</AspectRatio>The 16:9 lock matches YouTube's native player ratio — no black bars, no overflow.
<Grid container spacing={4}>
{projects.map((p) =>
<Grid key={p.id} xs={12} md={4}>
<Box variant="outlined" rounded="xl" sx="overflow-hidden">
<AspectRatio ratio={16 / 9}>
<img src={p.cover} alt={p.title} className="w-full h-full object-cover" />
</AspectRatio>
<Box p={4}>
<Typography variant="h4">{p.title}</Typography>
<Typography variant="body2" color="muted">{p.description}</Typography>
</Box>
</Box>
</Grid>
)}
</Grid>The classic "card grid" — every cover sits at exactly 16:9 regardless of source image dimensions.
{/* Figure with caption */}
<figure>
<AspectRatio as="div" ratio={4 / 3} sx="rounded-lg overflow-hidden">
<img src="diagram.png" alt="" className="w-full h-full object-contain" />
</AspectRatio>
<figcaption className="mt-2 text-sm text-neutral-500 dark:text-neutral-400">
Figure 1 — system architecture
</figcaption>
</figure>
{/* AspectRatio AS the figure */}
<AspectRatio as="figure" ratio={16 / 9}>
<img src="hero.jpg" alt="" className="w-full h-full object-cover" />
</AspectRatio>| Prop | Type | Default | Description |
|---|---|---|---|
ratio | number | string | 1 | Aspect ratio as width / height. Numbers (16/9) or CSS strings ('16 / 9'). |
as | ElementType | 'div' | Override the HTML tag. |
sx | string | — | Utility-class override, merged via tailwind-merge. |
style | CSSProperties | — | Inline styles. The aspectRatio from the prop is merged in; explicit style.aspectRatio wins (later in the merge). |
children | ReactNode | — | The media that fills the locked-ratio container. |
| ...rest | HTMLAttributes<HTMLDivElement> | — | Native attributes pass through. |
ratio resolves to CSS| You pass | CSS aspect-ratio becomes |
|---|---|
ratio={1} | "1 / 1" (rendered) |
ratio={16 / 9} | "1.7777… / 1" |
ratio="16 / 9" | "16 / 9" (passed through unchanged) |
ratio="21/9" (no spaces) | "21/9" (CSS accepts both) |
Numbers get formatted as <value> / 1 for readability in DevTools. Strings pass through unchanged.
Native CSS aspect-ratio: Chrome 88+, Firefox 89+, Safari 15+, Edge 88+ (all shipped 2021). Coverage today is ~98% of global browser usage. No padding-bottom hack, no JS measurement.
rounded-* requires overflow-hidden: without it, the child paints rectangular and the rounded corners on the parent don't clip anything visible. The #1 mistake — included in every example above.object-cover vs object-contain on the <img>:
object-cover — crops to fill, preserves ratio. The image bleeds. Typical for hero photos.object-contain — letterboxes to fit, preserves ratio. The image stays whole. Typical for diagrams, logos.asChild — the value of AspectRatio is the wrapper element WITH the aspect-ratio CSS property. Slot would defeat the purpose.sx="rounded-xl overflow-hidden" for the clipping pattern.<div className="aspect-square"> is fine for one-off square slots. AspectRatio is more useful when the ratio is non-square or varies.<picture> with <source> tags, not AspectRatio (which is a single ratio).