AspectRatio
Un componente contenedor que aplica una relación ancho-alto fija para sus hijos, asegurando que imágenes, videos y otro contenido multimedia mantengan proporciones consistentes independientemente del ancho del contenedor.
Casos de Uso
- Mostrar imágenes responsivas que mantengan su relación de aspecto en distintos tamaños de pantalla
- Incrustar videos (YouTube, Vimeo) en estándar 16:9 sin layout shift
- Crear cuadrículas uniformes de miniaturas donde todos los elementos comparten la misma forma
- Construir contenedores cuadrados de avatares o imágenes de perfil
- Mantener áreas de imagen de tarjeta consistentes en una cuadrícula responsiva
- Prevenir cumulative layout shift (CLS) reservando el espacio vertical correcto antes de que se cargue el contenido multimedia
- Enmarcar mapas, gráficos o canvas que requieren una proporción fija
Implementación Más Simple
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>
);
}Utiliza el clásico truco padding-bottom para establecer la altura desde el ancho. El contenedor absolute inset-0 interno llena el espacio reservado y contiene el contenido hijo. No se necesita "use client" ya que no hay interactividad.
Variaciones
Ratio 16:9
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>
);
}
// Uso
<AspectRatio16x9>
<img src="/hero.jpg" alt="Hero" className="h-full w-full object-cover" />
</AspectRatio16x9>La utilidad aspect-video de Tailwind aplica aspect-ratio: 16 / 9 nativamente. El overflow-hidden con rounded-lg recorta el contenido hijo a esquinas redondeadas. Los hijos deben usar h-full w-full object-cover para llenar el marco sin distorsión.
Ratio 4:3
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>
);
}
// Uso
<AspectRatio4x3>
<img src="/photo.jpg" alt="Photo" className="h-full w-full object-cover" />
</AspectRatio4x3>Tailwind no proporciona una clase aspect-4/3 integrada por defecto, por lo que el estilo aspectRatio en línea es la solución más limpia sin extender la configuración. La relación 4:3 es adecuada para fotografías, imágenes de productos y contenido de pantalla tradicional.
Cuadrado 1:1
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>
);
}
// Uso
<AspectRatioSquare className="w-24">
<img src="/avatar.jpg" alt="User avatar" className="h-full w-full object-cover" />
</AspectRatioSquare>Utiliza aspect-square de Tailwind para una relación 1:1. Ideal para imágenes de perfil, miniaturas de productos en cuadrículas cuadradas y vistas previas de publicaciones en redes sociales. Establece un ancho en el contenedor externo y la altura sigue automáticamente.
Con 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>
);
}
// Uso
<AspectRatioImage src="/banner.jpg" alt="Banner" ratio={21 / 9} />Combina el contenedor de relación de aspecto con el componente Image de Next.js en modo fill. El prop sizes es crítico para el rendimiento -- le dice al navegador qué tamaño de imagen descargar en cada ancho de viewport.
Con Incrustación de Video
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>
);
}
// Uso
<AspectRatioVideo
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
title="Video tutorial"
/>El iframe se posiciona absolutamente dentro del contenedor de relación de aspecto para que se estire y llene las dimensiones exactas. El border-0 elimina el borde predeterminado del iframe. Siempre incluye un title descriptivo para accesibilidad.
Ratio Personalizado
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>
);
}
// Ejemplos de uso
<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>Acepta cualquier relación numérica para dimensiones no estándar. El prop ratio es una división simple (width / height), lo que la hace intuitiva: 21/9 para ultraancho, 3/4 para retrato, 2/1 para un banner ancho.
Implementación Compleja
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>
);
}
);
// Componente complementario para patrón común imagen-en-ratio
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>
);
}
);
// Uso
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>
);
}Aspectos clave:
- Nombres de ratio preestablecidos -- aliases de string como
"video","photo"y"portrait"eliminan la necesidad de recordar valores numéricos y mejoran la legibilidad del código en el sitio de llamada. - Alternativa numérica -- los ratios numéricos personalizados aún son soportados junto a preestablecidos, por lo que casos extremos como
2.35(cinemascope) se cubren sin extender el mapa de preestablecidos. - Restricción maxHeight -- previene que el contenedor de relación de aspecto crezca demasiado alto en pantallas anchas. Cuando se alcanza la altura máxima, el ancho se convierte en la dimensión restringida.
- Componente AspectImage complementario -- un componente separado envuelve el patrón común de
Imagede Next.js dentro de una caja de relación de aspecto, proporcionando valoressizespredeterminados sensatos y un fondo de placeholder gris. - forwardRef en ambos componentes -- los refs fluyen a través del DOM para que observadores de intersección, librerías de animación o efectos vinculados a scroll puedan adjuntarse al contenedor.
- Color de fondo placeholder -- el
bg-gray-100enAspectImageproporciona un área de esqueleto visible mientras la imagen se carga, previniendo una brecha invisible antes del primer pintado.
Cosas a Tener en Cuenta
-
Propiedad CSS
aspect-rationo soportada en Safari antiguo -- Versiones de Safari anteriores a 15 no soportan la propiedadaspect-rationativa. Si necesitas soportar esos navegadores, usa el hack padding-bottom en su lugar. -
Next.js
Imageconfillrequiere un padre posicionado -- el componenteImageen modofillusaposition: absolute, por lo que el contenedor debe tenerposition: relative. Olvidar esto hace que la imagen se rompa fuera del contenedor. -
Desajuste
object-covervsobject-contain-- usarobject-coverrecorta la imagen para llenar el marco, mientras queobject-containla cuadra con letterboxing. Elegir la incorrecta recorta contenido importante o deja espacios feos. -
Layout shift cuando la relación se carga dinámicamente -- si la relación viene de un CMS o API y no se conoce en tiempo de compilación, el contenedor se renderiza sin altura hasta que JavaScript hidrata. Incluye la relación en el HTML renderizado en servidor o usa una relación alternativa.
-
Prop
sizesfaltante en Next.js Image -- sinsizes, Next.js usa por defecto100vw, causando que el navegador descargue una imagen mucho más grande de la necesaria en viewports pequeños. Siempre calcula puntos de quiebresizesrealistas. -
Conflicto del truco padding porcentual y flexbox -- el truco padding-bottom porcentual se calcula relativo al ancho del padre, pero dentro de una columna flex la referencia de ancho puede ser inesperada. La propiedad CSS
aspect-rationativa evita completamente este problema.
Relacionado
- Card -- Las tarjetas frecuentemente usan contenedores de relación de aspecto para sus áreas de imagen
- Skeleton -- Los cargadores de skeleton se benefician de envolturas de relación de aspecto para reservar espacio
- Avatar -- Los avatares son esencialmente contenedores de relación de aspecto 1:1
- Modal -- Los modales que muestran contenido multimedia necesitan contenedores conscientes de ratios