AspectRatio

Constrain any child element to a specific aspect ratio, preventing layout shift.

AspectRatio wraps Radix UI AspectRatio to constrain child elements — images, videos, embeds — to a fixed width-to-height ratio. The element fills its container width and maintains the ratio as that width changes, eliminating layout shift during image loads.

Installation

npm install @designforge/ui

Usage

import { AspectRatio } from "@designforge/ui";
 
export default function App() {
  return (
    <AspectRatio ratio={16 / 9}>
      <img
        src="/hero.jpg"
        alt="Hero"
        className="h-full w-full rounded-md object-cover"
      />
    </AspectRatio>
  );
}

Examples

Common ratios

{/* 16:9 — widescreen video / hero */}
<AspectRatio ratio={16 / 9} className="bg-muted rounded-lg overflow-hidden">
  <img src="/video-thumb.jpg" alt="" className="object-cover w-full h-full" />
</AspectRatio>
 
{/* 1:1 — square avatar or product image */}
<div className="w-32">
  <AspectRatio ratio={1}>
    <img src="/avatar.jpg" alt="" className="rounded-full object-cover w-full h-full" />
  </AspectRatio>
</div>
 
{/* 4:3 — classic photo */}
<AspectRatio ratio={4 / 3}>
  <img src="/photo.jpg" alt="" className="object-cover w-full h-full" />
</AspectRatio>
 
{/* 21:9 — cinematic banner */}
<AspectRatio ratio={21 / 9} className="bg-muted">
  <img src="/banner.jpg" alt="" className="object-cover w-full h-full" />
</AspectRatio>

Video embed

Wrapping an iframe in AspectRatio keeps the embed proportional across all viewport widths.

<AspectRatio ratio={16 / 9}>
  <iframe
    src="https://www.youtube.com/embed/dQw4w9WgXcQ"
    title="YouTube video"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
    allowFullScreen
    className="w-full h-full rounded-md"
  />
</AspectRatio>

Skeleton placeholder

Use AspectRatio as the shell for a loading skeleton so the page layout stays stable while data is fetching.

import { Skeleton } from "@designforge/ui";
 
<AspectRatio ratio={16 / 9}>
  <Skeleton className="w-full h-full rounded-md" />
</AspectRatio>

Constrained width

Wrap in a sized container to control the rendered width without touching AspectRatio itself — it always fills its parent.

<div className="max-w-sm">
  <AspectRatio ratio={4 / 3} className="rounded-lg overflow-hidden">
    <img src="/card-image.jpg" alt="Card" className="object-cover w-full h-full" />
  </AspectRatio>
</div>

All common ratios side by side

const ratios = [
  { ratio: 16 / 9, label: "16:9" },
  { ratio: 4 / 3,  label: "4:3"  },
  { ratio: 1,      label: "1:1"  },
  { ratio: 9 / 16, label: "9:16" },
];
 
<div className="grid grid-cols-2 gap-4">
  {ratios.map(({ ratio, label }) => (
    <AspectRatio key={label} ratio={ratio} className="bg-muted rounded-md flex items-center justify-center">
      <span className="text-sm font-semibold text-muted-foreground">{label}</span>
    </AspectRatio>
  ))}
</div>

API Reference

<AspectRatio>

PropTypeDefaultDescription
rationumber1Width divided by height (e.g. 16/9, 4/3, 1).
classNamestringAdditional CSS classes on the outer wrapper element.

The component is implemented using a padding-top trick — the wrapper has position: relative and the child is stretched to fill it with position: absolute; inset: 0. This means the child must be sized to width: 100%; height: 100% (or equivalent Tailwind classes w-full h-full) to fill the constrained area correctly.

Live View

Here is a live contextual rendering of the component directly from our isolated Storybook environment.

Open in Storybook ↗