HextaUI

Aspect ratio

Eine Box, die ihre Form behält, bevor Medien geladen sind, beim Laden schimmert, das Medium einblendet und bei einem Fehler auf einen Fallback zurückfällt.

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

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

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

Verhältnisse

ratio akzeptiert eine Zahl, einen "w/h"-String oder einen "w:h"-String.

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

Langsames Laden

Die Box behält ihre Form und schimmert, bis das Bild eintrifft; dann blendet das Bild ein, sodass sich nichts darunter verschiebt. Drücke Reload, um es erneut zu sehen.

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

Defektes Bild

Wenn das Bild fehlschlägt, wird das Defekt-Symbol des Browsers ausgeblendet und stattdessen ein Fallback-Icon angezeigt. Übergib fallback, um es zu ersetzen, oder fallback={null}, um nichts anzuzeigen.

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

Overlay

Absolut positionierte Kinder liegen über dem Media. Die Box beschneidet nichts, sodass Fokusringe auf Overlay-Links sichtbar bleiben.

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

Ohne Platzhalter

placeholder={false} schaltet den Lade-Shimmer, das Einblenden und den Fallback ab, für das einfache shadcn-Verhalten.

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

Überschreibe das Verhältnis an einem Breakpoint mit einer aspect-Klasse. Diese hier ist auf kleinen Bildschirmen quadratisch und ab md md:aspect-video.

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

In einer zentrierten Flex-Spalte

Die Box hat standardmäßig volle Breite, sodass sie die Spalte füllt, statt auf null zu schrumpfen, wenn das Elternelement seine Kinder zentriert.

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

Textinhalt

Kinder, die kein Media sind, erhalten nur die Box und sonst nichts. Positioniere sie selbst.

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

Als Figure

Halte Bildunterschriften außerhalb der Box, damit sie das Verhältnis nicht verändern.

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

Ungültiges Verhältnis

0, negative Zahlen und nicht parsbare Strings fallen auf ein Quadrat zurück und protokollieren in der Entwicklung eine Warnung.

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

Rechts nach links

Overlays, die mit logischen Properties wie start-3 positioniert werden, folgen der Leserichtung.

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>
  )
}
  • Die Box ist aria-busy, solange ihr Media lädt.
  • Der Fallback ist dekorativ und für assistive Technologien verborgen. Der alt-Text des Bildes bleibt verfügbar, wenn es nicht lädt, also schreibe immer einen.
  • Bei reduzierter Bewegung erscheint das Media ohne Einblenden.

Akzeptiert jedes Attribut des Elements, das sie rendert. Media, das direkt darin liegt, ein <img>, <picture> oder <video>, füllt die Box mit object-cover und übernimmt ihren Radius.

PropTypStandard
ratio
number | `${number}/${number}` | `${number}:${number}`1
placeholderZeigt einen Shimmer, solange das Media lädt, und einen Fallback, wenn es fehlschlägt.
booleantrue
fallbackWird angezeigt, wenn das Media fehlschlägt. null zeigt nichts an.
ReactNode<IconPhotoOff />
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="aspect-ratio"Die Box in CSS ansprechen.
data-stateloading, loaded oder error. Wird nur gesetzt, wenn placeholder aktiv ist und die Box Media enthält.
aria-busyVorhanden, solange das Media lädt.
--ratioDas geparste Verhältnis als Zahl.
data-slot="aspect-ratio-placeholder"Der Shimmer, der während des Ladens oder nach einem Fehler angezeigt wird.
data-slot="aspect-ratio-fallback"Der Wrapper um den Fallback.

parseAspectRatio(ratio) wandelt jedes akzeptierte Verhältnis in eine Zahl um und fällt auf 1 zurück. Verwende es, um andere Elemente auf die gleiche Weise zu dimensionieren. Die Typen AspectRatioValue und AspectRatioProps werden ebenfalls exportiert.