Skip to content

Aspect Ratio

Holds media to a fixed width-to-height ratio, reserving its space before the content loads.

SourceRegistry
16 : 9
Meridian
import { AspectRatio } from "@/components/ui/aspect-ratio"

export default function AspectRatioDemo() {
  return (
    <figure className="w-full max-w-sm">
      <AspectRatio
        ratio={16 / 9}
        className="flex items-center justify-center rounded-xl bg-muted"
      >
        <span className="font-mono text-sm text-muted-foreground numeric">
          16 : 9
        </span>
      </AspectRatio>
      <figcaption className="mt-2 text-sm text-muted-foreground">
        Meridian
      </figcaption>
    </figure>
  )
}

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/aspect-ratio.json

Usage

import { AspectRatio } from "@/components/ui/aspect-ratio"
<AspectRatio ratio={16 / 9} className="overflow-hidden rounded-xl">
  <img src="/cover.jpg" alt="…" className="size-full object-cover" />
</AspectRatio>

ratio is width divided by height. The component is a positioned <div> sized with the CSS aspect-ratio property — no JavaScript, no padding hacks — so it works in server components and never shifts after hydration.

Examples

Common ratios

Choose the ratio from the content, then keep it consistent across a collection so grids line up.

1:1
Avatars, products
4:3
Photos, thumbnails
3:2
Editorial images
16:9
Video, hero media
21:9
Cinematic banners
import { AspectRatio } from "@/components/ui/aspect-ratio"

const ratios = [
  { label: "1:1", value: 1, use: "Avatars, products" },
  { label: "4:3", value: 4 / 3, use: "Photos, thumbnails" },
  { label: "3:2", value: 3 / 2, use: "Editorial images" },
  { label: "16:9", value: 16 / 9, use: "Video, hero media" },
  { label: "21:9", value: 21 / 9, use: "Cinematic banners" },
]

export default function AspectRatioRatios() {
  return (
    <div className="grid w-full max-w-2xl grid-cols-2 items-end gap-4 sm:grid-cols-5">
      {ratios.map((ratio) => (
        <div key={ratio.label} className="flex flex-col gap-2">
          <AspectRatio
            ratio={ratio.value}
            className="flex items-center justify-center rounded-lg border border-dashed border-brand-7 bg-brand-2"
          >
            <span className="font-mono text-xs font-medium text-brand-11">
              {ratio.label}
            </span>
          </AspectRatio>
          <span className="text-xs text-muted-foreground">{ratio.use}</span>
        </div>
      ))}
    </div>
  )
}

With Next.js Image

Use fill so the image takes the box's size, and object-cover to crop instead of stretching. Pass sizes so the browser downloads the right resolution.

import Image from "next/image"

<AspectRatio ratio={16 / 9} className="overflow-hidden rounded-xl bg-muted">
  <Image
    src="/showcase/dashboard.png"
    alt="The analytics dashboard in dark mode"
    fill
    sizes="(min-width: 768px) 50vw, 100vw"
    className="object-cover"
  />
</AspectRatio>

Video

Video and embeds should reserve their frame before they load. Overlay controls and metadata with absolute positioning — the box is already relative.

12:48
Designing with tokensprfct Talks · Episode 4
import { PlayIcon } from "lucide-react"

import { AspectRatio } from "@/components/ui/aspect-ratio"
import { Button } from "@/components/ui/button"

export default function AspectRatioVideo() {
  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <AspectRatio
        ratio={16 / 9}
        // Always dark, like a video poster. The fill and the edge keep the
        // frame visible on a dark page too.
        className="dark overflow-hidden rounded-xl border bg-muted"
      >
        <div className="absolute inset-0 flex items-center justify-center">
          <Button
            size="icon-lg"
            variant="default"
            aria-label="Play video: Designing with tokens"
            className="size-14 rounded-full"
          >
            <PlayIcon className="size-5! translate-x-px fill-current" />
          </Button>
        </div>
        <span className="absolute right-3 bottom-3 rounded-md bg-background px-1.5 py-0.5 font-mono text-[0.6875rem] text-foreground numeric">
          12:48
        </span>
      </AspectRatio>
      <div className="flex flex-col gap-0.5">
        <span className="text-sm font-semibold text-foreground">
          Designing with tokens
        </span>
        <span className="text-body-xs text-muted-foreground">
          prfct Talks · Episode 4
        </span>
      </div>
    </div>
  )
}
<AspectRatio ratio={16 / 9} className="overflow-hidden rounded-xl">
  <iframe
    src="https://www.youtube-nocookie.com/embed/…"
    title="Designing with tokens"
    allow="accelerometer; encrypted-media; picture-in-picture"
    allowFullScreen
    className="absolute inset-0 size-full"
  />
</AspectRatio>

Square grid

A 1:1 ratio keeps avatars, product shots and thumbnails uniform even when the source images aren't.

  • Ana
  • Jonas
  • Maya
  • Theo
  • Iris
  • Lena
  • Omar
  • Eli
import { AspectRatio } from "@/components/ui/aspect-ratio"

const people = [
  { initials: "AK", name: "Ana" },
  { initials: "JB", name: "Jonas" },
  { initials: "MC", name: "Maya" },
  { initials: "TP", name: "Theo" },
  { initials: "IV", name: "Iris" },
  { initials: "LR", name: "Lena" },
  { initials: "OS", name: "Omar" },
  { initials: "EW", name: "Eli" },
]

export default function AspectRatioSquareGrid() {
  return (
    <ul className="grid w-full max-w-md grid-cols-4 gap-3">
      {people.map((person) => (
        <li key={person.initials} className="flex flex-col gap-1.5">
          <AspectRatio
            ratio={1}
            className="flex items-center justify-center rounded-xl bg-muted text-lg font-medium text-muted-foreground"
          >
            <span aria-hidden="true">{person.initials}</span>
          </AspectRatio>
          <span className="text-center text-xs text-muted-foreground">
            {person.name}
          </span>
        </li>
      ))}
    </ul>
  )
}

Card media

Put the ratio box first in a card with pt-0 on the card, and the media runs edge to edge with the card's rounded corners.

New
Amber desk lamp
$189
import { LampDeskIcon } from "lucide-react"

import { AspectRatio } from "@/components/ui/aspect-ratio"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"

export default function AspectRatioCardMedia() {
  return (
    <Card className="w-full max-w-xs pt-0">
      <AspectRatio
        ratio={4 / 3}
        className="flex items-center justify-center border-b bg-muted"
      >
        <LampDeskIcon
          aria-hidden="true"
          className="size-10 text-muted-foreground"
          strokeWidth={1.25}
        />
        <Badge variant="outline" className="absolute top-3 left-3 bg-card">
          New
        </Badge>
      </AspectRatio>
      <CardHeader>
        <CardTitle>Amber desk lamp</CardTitle>
        <CardDescription>
          Hand-finished brass, warm dimmable light.
        </CardDescription>
      </CardHeader>
      <CardContent>
        <span className="text-heading-sm text-foreground numeric">$189</span>
      </CardContent>
      <CardFooter>
        <Button className="w-full">Add to cart</Button>
      </CardFooter>
    </Card>
  )
}

Guidelines

When to use

  • For images, video, maps and embeds whose container width changes with the layout.
  • In grids and carousels, to keep every tile the same shape.
  • Wherever late-loading media would otherwise push content down.

When not to use

  • For text or UI whose height depends on its content — fixing the ratio will clip or leave gaps.
  • When the container already has fixed pixel dimensions; set them directly.

Crop, don't distort

Media inside a ratio box should use object-cover (crop to fill) or object-contain (letterbox), never the default stretch.

Doobject-cover fills the frame and keeps proportions.
Don’tStretched media looks broken, no matter how good the photo is.

Accessibility

Aspect Ratio is purely presentational; what matters is the media inside it.

  • Images need alt text that says what the image shows, or alt="" when it's decorative.
  • Video needs captions, and embeds need a title that names the content.
  • Layout stability. Reserving space prevents content from jumping as media loads, which helps everyone — and especially people using magnification or reading slowly.

API reference

AspectRatio

Renders a <div> with position: relative and aspect-ratio set from ratio. Accepts every <div> prop.

PropTypeDefault
ratiorequired

Width divided by height, e.g. 16 / 9 or 1.

numberNo default
className

Rounding, overflow and background for the frame.

stringNo default