React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

aspect-ratioresponsiveimagevideocontainerlayoutcomponenttailwind

AspectRatio

A container component that enforces a fixed width-to-height ratio for its children, ensuring images, videos, and other media maintain consistent proportions regardless of container width.

Use Cases

  • Display responsive images that maintain their aspect ratio across screen sizes
  • Embed videos (YouTube, Vimeo) at standard 16:9 without layout shift
  • Create uniform thumbnail grids where all items share the same shape
  • Build square avatar or profile image containers
  • Maintain consistent card image areas in a responsive grid
  • Prevent cumulative layout shift (CLS) by reserving the correct vertical space before media loads
  • Frame maps, charts, or canvases that require a fixed proportion

Simplest Implementation

interface AspectRatioProps {
  ratio?: number;
  children: React.ReactNode;
}
 
export function AspectRatio({ ratio = 16 / 9, children }: AspectRatioProps) {
  return (
    <div className="relative w-full" style={{ paddingBottom: `${(1 / ratio) * 100}%` }}>
      <div className="absolute inset-0">{children}</div>
    </div>
  );
}

Uses the classic padding-bottom trick to establish the height from the width. The inner absolute inset-0 container fills the reserved space and holds the child content. No "use client" is needed since there is no interactivity.

Variations

16:9 Ratio

interface AspectRatioProps {
  children: React.ReactNode;
  className?: string;
}
 
export function AspectRatio16x9({ children, className }: AspectRatioProps) {
  return (
    <div className={`relative aspect-video overflow-hidden rounded-lg ${className ?? ""}`}>
      {children}
    </div>
  );
}
 
// Usage
<AspectRatio16x9>
  <img src="/hero.jpg" alt="Hero" className="h-full w-full object-cover" />
</AspectRatio16x9>

Tailwind's aspect-video utility applies aspect-ratio: 16 / 9 natively. The overflow-hidden with rounded-lg clips the child content to rounded corners. Children should use h-full w-full object-cover to fill the frame without distortion.

4:3 Ratio

interface AspectRatioProps {
  children: React.ReactNode;
  className?: string;
}
 
export function AspectRatio4x3({ children, className }: AspectRatioProps) {
  return (
    <div className={`relative overflow-hidden rounded-lg ${className ?? ""}`} style={{ aspectRatio: "4 / 3" }}>
      {children}
    </div>
  );
}
 
// Usage
<AspectRatio4x3>
  <img src="/photo.jpg" alt="Photo" className="h-full w-full object-cover" />
</AspectRatio4x3>

Tailwind does not ship a built-in aspect-4/3 class by default, so the inline aspectRatio style is the cleanest solution without extending the config. The 4:3 ratio suits photographs, product images, and traditional display content.

1:1 Square

interface AspectRatioProps {
  children: React.ReactNode;
  className?: string;
}
 
export function AspectRatioSquare({ children, className }: AspectRatioProps) {
  return (
    <div className={`relative aspect-square overflow-hidden rounded-lg ${className ?? ""}`}>
      {children}
    </div>
  );
}
 
// Usage
<AspectRatioSquare className="w-24">
  <img src="/avatar.jpg" alt="User avatar" className="h-full w-full object-cover" />
</AspectRatioSquare>

Uses Tailwind's aspect-square for a 1:1 ratio. Ideal for profile images, product thumbnails in square grids, and social media post previews. Set a width on the outer container and the height follows automatically.

With Image Fill (Next.js)

import Image from "next/image";
 
interface AspectRatioImageProps {
  src: string;
  alt: string;
  ratio?: number;
  className?: string;
}
 
export function AspectRatioImage({
  src,
  alt,
  ratio = 16 / 9,
  className,
}: AspectRatioImageProps) {
  return (
    <div
      className={`relative overflow-hidden rounded-lg ${className ?? ""}`}
      style={{ aspectRatio: String(ratio) }}
    >
      <Image
        src={src}
        alt={alt}
        fill
        sizes="(max-width: 768px) 100vw, 50vw"
        className="object-cover"
      />
    </div>
  );
}
 
// Usage
<AspectRatioImage src="/banner.jpg" alt="Banner" ratio={21 / 9} />

Combines the aspect ratio container with the Next.js Image component in fill mode. The sizes prop is critical for performance -- it tells the browser which image size to download at each viewport width.

With Video Embed

interface AspectRatioVideoProps {
  src: string;
  title: string;
  className?: string;
}
 
export function AspectRatioVideo({ src, title, className }: AspectRatioVideoProps) {
  return (
    <div className={`relative aspect-video overflow-hidden rounded-lg ${className ?? ""}`}>
      <iframe
        src={src}
        title={title}
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowFullScreen
        className="absolute inset-0 h-full w-full border-0"
      />
    </div>
  );
}
 
// Usage
<AspectRatioVideo
  src="https://www.youtube.com/embed/dQw4w9WgXcQ"
  title="Video tutorial"
/>

The iframe is positioned absolutely inside the aspect ratio container so it stretches to fill the exact dimensions. The border-0 removes the default iframe border. Always include a descriptive title for accessibility.

Custom Ratio

interface AspectRatioProps {
  ratio: number;
  children: React.ReactNode;
  className?: string;
}
 
export function AspectRatio({ ratio, children, className }: AspectRatioProps) {
  return (
    <div
      className={`relative overflow-hidden ${className ?? ""}`}
      style={{ aspectRatio: String(ratio) }}
    >
      {children}
    </div>
  );
}
 
// Usage examples
<AspectRatio ratio={21 / 9} className="rounded-lg">
  <img src="/ultrawide.jpg" alt="Ultrawide" className="h-full w-full object-cover" />
</AspectRatio>
 
<AspectRatio ratio={3 / 4} className="rounded-lg">
  <img src="/portrait.jpg" alt="Portrait" className="h-full w-full object-cover" />
</AspectRatio>

Accepts any numeric ratio for non-standard dimensions. The ratio prop is a simple division (width / height), making it intuitive: 21/9 for ultrawide, 3/4 for portrait, 2/1 for a wide banner.

Complex Implementation

import { forwardRef, type CSSProperties } from "react";
import Image from "next/image";
 
type PresetRatio = "square" | "video" | "photo" | "portrait" | "ultrawide";
 
interface AspectRatioProps {
  ratio?: number | PresetRatio;
  children: React.ReactNode;
  maxHeight?: number;
  className?: string;
  style?: CSSProperties;
}
 
const presetRatios: Record<PresetRatio, number> = {
  square: 1,
  video: 16 / 9,
  photo: 4 / 3,
  portrait: 3 / 4,
  ultrawide: 21 / 9,
};
 
function resolveRatio(ratio: number | PresetRatio): number {
  return typeof ratio === "string" ? presetRatios[ratio] : ratio;
}
 
export const AspectRatio = forwardRef<HTMLDivElement, AspectRatioProps>(
  function AspectRatio({ ratio = "video", children, maxHeight, className, style }, ref) {
    const numericRatio = resolveRatio(ratio);
 
    return (
      <div
        ref={ref}
        className={`relative overflow-hidden ${className ?? ""}`}
        style={{
          aspectRatio: String(numericRatio),
          maxHeight: maxHeight ? `${maxHeight}px` : undefined,
          ...style,
        }}
      >
        {children}
      </div>
    );
  }
);
 
// Companion component for common image-in-ratio pattern
interface AspectRatioImageProps {
  ratio?: number | PresetRatio;
  src: string;
  alt: string;
  priority?: boolean;
  sizes?: string;
  maxHeight?: number;
  className?: string;
  imageClassName?: string;
}
 
export const AspectImage = forwardRef<HTMLDivElement, AspectRatioImageProps>(
  function AspectImage(
    {
      ratio = "video",
      src,
      alt,
      priority = false,
      sizes = "(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw",
      maxHeight,
      className,
      imageClassName,
    },
    ref
  ) {
    return (
      <AspectRatio
        ref={ref}
        ratio={ratio}
        maxHeight={maxHeight}
        className={`bg-gray-100 ${className ?? ""}`}
      >
        <Image
          src={src}
          alt={alt}
          fill
          priority={priority}
          sizes={sizes}
          className={`object-cover ${imageClassName ?? ""}`}
        />
      </AspectRatio>
    );
  }
);
 
// Usage
function Gallery() {
  const images = [
    { src: "/img1.jpg", alt: "Mountain landscape" },
    { src: "/img2.jpg", alt: "City skyline" },
    { src: "/img3.jpg", alt: "Ocean sunset" },
  ];
 
  return (
    <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
      {images.map((img) => (
        <AspectImage
          key={img.src}
          ratio="photo"
          src={img.src}
          alt={img.alt}
          className="rounded-xl"
        />
      ))}
    </div>
  );
}

Key aspects:

  • Preset ratio names -- string aliases like "video", "photo", and "portrait" eliminate the need to remember numeric values and improve code readability at the call site.
  • Numeric fallback -- custom numeric ratios are still supported alongside presets, so edge cases like 2.35 (cinemascope) are covered without extending the preset map.
  • maxHeight constraint -- prevents the aspect ratio container from growing too tall on wide screens. When the max height is reached, the width becomes the constrained dimension instead.
  • Companion AspectImage -- a separate component wraps the common pattern of Next.js Image inside an aspect ratio box, providing sensible sizes defaults and a gray placeholder background.
  • forwardRef on both components -- refs flow through to the DOM so intersection observers, animation libraries, or scroll-linked effects can attach to the container.
  • Background color placeholder -- the bg-gray-100 on AspectImage provides a visible skeleton area while the image loads, preventing an invisible gap before the first paint.

Gotchas

  • aspect-ratio CSS property not supported in older Safari -- Safari versions before 15 do not support the native aspect-ratio property. If you need to support those browsers, use the padding-bottom hack instead.

  • Next.js Image with fill requires a positioned parent -- the Image component in fill mode uses position: absolute, so the container must have position: relative. Forgetting this causes the image to break out of the container.

  • object-cover vs object-contain mismatch -- using object-cover crops the image to fill the frame, while object-contain letterboxes it. Choosing the wrong one either cuts off important content or leaves ugly gaps.

  • Layout shift when ratio is loaded dynamically -- if the ratio comes from a CMS or API and is not known at build time, the container renders with no height until JavaScript hydrates. Include the ratio in server-rendered HTML or use a fallback ratio.

  • Missing sizes prop on Next.js Image -- without sizes, Next.js defaults to 100vw, causing the browser to download an image much larger than needed on small viewports. Always calculate realistic sizes breakpoints.

  • Percentage-based padding trick and flexbox conflict -- the padding-bottom percentage trick calculates relative to the parent width, but inside a flex column the reference width may be unexpected. The native aspect-ratio CSS property avoids this issue entirely.

  • Card -- Cards frequently use aspect ratio containers for their image areas
  • Skeleton -- Skeleton loaders benefit from aspect ratio wrappers to reserve space
  • Avatar -- Avatars are essentially 1:1 aspect ratio containers
  • Modal -- Modals displaying media content need ratio-aware containers