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.
Search across all documentation pages
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.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
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.
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.
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.
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.
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.
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.
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:
"video", "photo", and "portrait" eliminate the need to remember numeric values and improve code readability at the call site.2.35 (cinemascope) are covered without extending the preset map.Image inside an aspect ratio box, providing sensible sizes defaults and a gray placeholder background.bg-gray-100 on AspectImage provides a visible skeleton area while the image loads, preventing an invisible gap before the first paint.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.
Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥