HextaUI

Aspect ratio

Une boîte qui garde sa forme avant le chargement du média, scintille pendant le chargement, fait apparaître le média en fondu et bascule sur un repli en cas d’échec.

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

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

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

Ratios

ratio accepte un nombre, une chaîne "w/h" ou une chaîne "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>
  )
}

Chargement lent

La boîte garde sa forme et scintille jusqu’à l’arrivée de l’image, qui apparaît ensuite en fondu : rien en dessous ne bouge. Appuyez sur Reload pour le revoir.

"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>
  )
}

Image cassée

Lorsque l’image échoue, le glyphe d’image cassée du navigateur est masqué et une icône de repli s’affiche à la place. Passez fallback pour la remplacer, ou fallback={null} pour ne rien afficher.

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>
  )
}

Surimpression

Les enfants positionnés de façon absolue se placent par-dessus le média. La boîte ne rogne rien, donc les anneaux de focus des liens en surimpression restent 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>
  )
}

Sans placeholder

placeholder={false} désactive le shimmer de chargement, le fondu d’apparition et le contenu de repli, pour le comportement shadcn standard.

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

Remplacez le ratio à un point de rupture avec une classe aspect. Celui-ci est carré sur petit écran et md:aspect-video à partir de 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>
  )
}

Dans une colonne flex centrée

La boîte occupe toute la largeur par défaut : elle remplit la colonne au lieu de s’effondrer à zéro lorsque le parent centre ses enfants.

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>
  )
}

Contenu texte

Les enfants qui ne sont pas des médias reçoivent la boîte et rien d’autre. Positionnez-les vous-même.

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>
  )
}

Comme figure

Gardez les légendes en dehors de la boîte pour qu’elles ne modifient pas le ratio.

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>
  )
}

Ratio invalide

0, les nombres négatifs et les chaînes non analysables se rabattent sur un carré et consignent un avertissement en développement.

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 droite à gauche

Les surimpressions positionnées avec des propriétés logiques comme start-3 suivent le sens de lecture.

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 boîte est aria-busy pendant le chargement de son média.
  • Le contenu de repli est décoratif et masqué aux technologies d’assistance. Le texte alt de l’image reste disponible si elle ne se charge pas : écrivez-en toujours un.
  • Avec la réduction des animations, le média apparaît sans fondu.

Accepte tous les attributs de l’élément qu’il rend. Un média placé directement à l’intérieur, un <img>, <picture> ou <video>, remplit la boîte avec object-cover et hérite de son rayon.

PropTypePar défaut
ratio
number | `${number}/${number}` | `${number}:${number}`1
placeholderAffiche un shimmer pendant le chargement du média et un contenu de repli en cas d’échec.
booleantrue
fallbackAffiché lorsque le média échoue. null n’affiche rien.
ReactNode<IconPhotoOff />
render
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="aspect-ratio"Ciblez la boîte en CSS.
data-stateloading, loaded ou error. Défini uniquement lorsque placeholder est activé et que la boîte contient un média.
aria-busyPrésent pendant le chargement du média.
--ratioLe ratio analysé, sous forme de nombre.
data-slot="aspect-ratio-placeholder"Le shimmer affiché pendant le chargement ou après une erreur.
data-slot="aspect-ratio-fallback"L’enveloppe autour du contenu de repli.

parseAspectRatio(ratio) convertit tout ratio accepté en nombre, avec 1 par défaut. Utilisez-le pour dimensionner d’autres éléments de la même façon. Les types AspectRatioValue et AspectRatioProps sont également exportés.