HextaUI

Aspect ratio

Un contenedor que mantiene su forma antes de que cargue el contenido multimedia, muestra un brillo mientras carga, hace aparecer el contenido con un fundido y muestra una alternativa si falla.

Sunset over mountains
import { AspectRatio } from "@/components/ui/aspect-ratio"

export function AspectRatioDemo() {
  return (
    <div className="w-full max-w-md">
      <AspectRatio ratio={16 / 9} className="rounded-xl">
        <img src="/preview/landscape.svg" alt="Sunset over mountains" />
      </AspectRatio>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/aspect-ratio.json

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

import { AspectRatio } from "@/components/ui/aspect-ratio"
<AspectRatio ratio={16 / 9} className="rounded-lg">
  <img src="/photo.jpg" alt="Sunset over mountains" />
</AspectRatio>

Proporciones

ratio acepta un número, una cadena "w/h" o una cadena "w:h".

import {
  AspectRatio,
  type AspectRatioValue,
} from "@/components/ui/aspect-ratio"

const ratios: { label: string; ratio: AspectRatioValue }[] = [
  { label: "16 / 9", ratio: 16 / 9 },
  { label: "1", ratio: 1 },
  { label: '"4/3"', ratio: "4/3" },
  { label: '"21:9"', ratio: "21:9" },
]

export function AspectRatioRatios() {
  return (
    <div className="grid w-full max-w-md grid-cols-2 gap-3">
      {ratios.map(({ label, ratio }) => (
        <div key={label} className="flex flex-col gap-1.5">
          <AspectRatio ratio={ratio} className="rounded-lg">
            <img src="/preview/landscape.svg" alt="Sunset over mountains" />
          </AspectRatio>
          <span className="font-mono text-xs text-muted-foreground">
            {label}
          </span>
        </div>
      ))}
    </div>
  )
}

Carga lenta

La caja mantiene su forma y muestra el shimmer hasta que llega la imagen; entonces la imagen aparece con un fundido, así que nada de lo que hay debajo se mueve. Pulsa Reload para verlo de nuevo.

"use client"

import * as React from "react"

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

export function AspectRatioSlowLoad() {
  const [src, setSrc] = React.useState<string | undefined>(undefined)
  const [attempt, setAttempt] = React.useState(0)

  React.useEffect(() => {
    const timer = setTimeout(
      () => setSrc(`/preview/landscape.svg?v=${attempt}`),
      1500
    )
    return () => clearTimeout(timer)
  }, [attempt])

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <AspectRatio ratio={16 / 9} className="rounded-xl">
        <img src={src} alt="Sunset over mountains" />
      </AspectRatio>
      <Button
        variant="outline"
        size="sm"
        className="self-start"
        onClick={() => {
          setSrc(undefined)
          setAttempt(attempt + 1)
        }}
      >
        Reload
      </Button>
    </div>
  )
}

Imagen rota

Cuando la imagen falla, se oculta el glifo de imagen rota del navegador y se muestra un icono de fallback. Pasa fallback para reemplazarlo, o fallback={null} para no mostrar nada.

import { IconMoodSad } from "@tabler/icons-react"

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

export function AspectRatioBrokenImage() {
  return (
    <div className="grid w-full max-w-md grid-cols-2 gap-3">
      <AspectRatio ratio={4 / 3} className="rounded-lg">
        <img src="/preview/does-not-exist.jpg" alt="Team photo" />
      </AspectRatio>
      <AspectRatio
        ratio={4 / 3}
        className="rounded-lg"
        fallback={
          <span className="flex flex-col items-center gap-1 text-xs">
            <IconMoodSad />
            Couldn’t load
          </span>
        }
      >
        <img src="/preview/does-not-exist.jpg" alt="Team photo" />
      </AspectRatio>
    </div>
  )
}

Superposición

Los hijos con posición absoluta se sitúan encima del elemento multimedia. La caja no recorta nada, así que los anillos de foco en los enlaces superpuestos siguen visibles.

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

export function AspectRatioOverlay() {
  return (
    <div className="w-full max-w-md">
      <AspectRatio ratio={16 / 9} className="rounded-xl">
        <img src="/preview/landscape.svg" alt="Sunset over mountains" />
        <div className="absolute inset-x-3 bottom-3 flex items-center justify-between gap-3 rounded-lg bg-background/80 px-3 py-2 text-sm backdrop-blur-sm">
          <span className="truncate font-medium">Dolomites at dusk</span>
          <a
            href="#"
            className="shrink-0 rounded-sm underline underline-offset-4 outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
          >
            View
          </a>
        </div>
      </AspectRatio>
    </div>
  )
}

Sin placeholder

placeholder={false} desactiva el shimmer de carga, el fundido de entrada y el fallback, para el comportamiento básico de shadcn.

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

export function AspectRatioWithoutPlaceholder() {
  return (
    <div className="w-full max-w-md">
      <AspectRatio ratio={16 / 9} placeholder={false} className="rounded-lg">
        <img src="/preview/landscape.svg" alt="Sunset over mountains" />
      </AspectRatio>
    </div>
  )
}

Responsive

Reemplaza la proporción en un breakpoint con una clase aspect. Esta es cuadrada en pantallas pequeñas y md:aspect-video desde md.

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

export function AspectRatioResponsive() {
  return (
    <div className="w-full max-w-md">
      <AspectRatio ratio={1} className="rounded-lg md:aspect-video">
        <img src="/preview/landscape.svg" alt="Sunset over mountains" />
      </AspectRatio>
    </div>
  )
}

Dentro de una columna flex centrada

La caja ocupa todo el ancho por defecto, así que llena la columna en lugar de colapsar a cero cuando el padre centra a sus hijos.

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

export function AspectRatioFlexColumn() {
  return (
    <div className="flex w-full max-w-md flex-col items-center rounded-lg border border-dashed p-3">
      <AspectRatio ratio={16 / 9} className="rounded-lg">
        <img src="/preview/landscape.svg" alt="Sunset over mountains" />
      </AspectRatio>
    </div>
  )
}

Contenido de texto

Los hijos que no son multimedia reciben la caja y nada más. Colócalos tú mismo.

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

export function AspectRatioTextContent() {
  return (
    <div className="w-full max-w-md">
      <AspectRatio ratio={3} className="rounded-lg border">
        <div className="absolute inset-0 grid place-items-center p-4 text-center text-sm text-muted-foreground">
          Any content can sit in the box.
        </div>
      </AspectRatio>
    </div>
  )
}

Como figura

Mantén los pies de foto fuera de la caja para que no cambien la proporción.

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

export function AspectRatioFigure() {
  return (
    <figure className="flex w-full max-w-md flex-col gap-2">
      <AspectRatio ratio={16 / 9} className="rounded-lg">
        <img src="/preview/landscape.svg" alt="Sunset over mountains" />
      </AspectRatio>
      <figcaption className="text-xs text-muted-foreground">
        Dolomites at dusk, photographed from Seceda.
      </figcaption>
    </figure>
  )
}

Proporción no válida

0, los números negativos y las cadenas que no se pueden analizar vuelven a un cuadrado y registran una advertencia en desarrollo.

import {
  AspectRatio,
  type AspectRatioValue,
} from "@/components/ui/aspect-ratio"

export function AspectRatioInvalidRatio() {
  return (
    <div className="grid w-full max-w-md grid-cols-2 gap-3">
      <AspectRatio ratio={0} className="rounded-lg border" />
      <AspectRatio
        ratio={"abc" as AspectRatioValue}
        className="rounded-lg border"
      />
    </div>
  )
}

De derecha a izquierda

Las superposiciones posicionadas con propiedades lógicas como start-3 siguen la dirección de lectura.

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

export function AspectRatioRtl() {
  return (
    <div dir="rtl" className="w-full max-w-md">
      <AspectRatio ratio={16 / 9} className="rounded-lg">
        <img src="/preview/landscape.svg" alt="غروب الشمس فوق الجبال" />
        <span className="absolute start-3 top-3 rounded-md bg-background/80 px-2 py-1 text-xs">
          جديد
        </span>
      </AspectRatio>
    </div>
  )
}
  • La caja tiene aria-busy mientras carga su contenido multimedia.
  • El fallback es decorativo y está oculto para las tecnologías de asistencia. El texto alt de la imagen sigue disponible cuando falla la carga, así que escríbelo siempre.
  • Con movimiento reducido, el contenido multimedia aparece sin fundido.

Acepta todos los atributos del elemento que renderiza. El contenido multimedia colocado directamente dentro, un <img>, <picture> o <video>, llena la caja con object-cover y hereda su radio.

PropTipoPredeterminado
ratio
number | `${number}/${number}` | `${number}:${number}`1
placeholderMuestra un shimmer mientras carga el contenido multimedia y un fallback cuando falla.
booleantrue
fallbackSe muestra cuando falla el contenido multimedia. null no muestra nada.
ReactNode<IconPhotoOff />
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="aspect-ratio"Selecciona la caja en CSS.
data-stateloading, loaded o error. Se establece solo cuando placeholder está activado y la caja contiene contenido multimedia.
aria-busyPresente mientras el contenido multimedia carga.
--ratioLa proporción analizada como número.
data-slot="aspect-ratio-placeholder"El shimmer que se muestra durante la carga o tras un error.
data-slot="aspect-ratio-fallback"El contenedor alrededor del fallback.

parseAspectRatio(ratio) convierte cualquier proporción aceptada en un número, con 1 como valor de respaldo. Úsalo para dimensionar otros elementos de la misma manera. También se exportan los tipos AspectRatioValue y AspectRatioProps.