Przejdź do treści

Aspect Ratio

Utrzymuje media w stałych proporcjach szerokości do wysokości i rezerwuje dla nich miejsce, zanim treść się załaduje.

Kod źródłowyRejestr
16 : 9
Meridian
import { AspectRatio } from "@/components/ui/aspect-ratio"

export default function AspectRatioDemo() {
  return (
    <figure className="w-full max-w-sm">
      <AspectRatio
        ratio={16 / 9}
        className="flex items-center justify-center rounded-xl bg-muted"
      >
        <span className="font-mono text-sm text-muted-foreground numeric">
          16 : 9
        </span>
      </AspectRatio>
      <figcaption className="mt-2 text-sm text-muted-foreground">
        Meridian
      </figcaption>
    </figure>
  )
}

Instalacja

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/aspect-ratio.json

Użycie

import { AspectRatio } from "@/components/ui/aspect-ratio"
<AspectRatio ratio={16 / 9} className="overflow-hidden rounded-xl">
  <img src="/cover.jpg" alt="…" className="size-full object-cover" />
</AspectRatio>

ratio to szerokość podzielona przez wysokość. Komponent to pozycjonowany <div>, którego rozmiar wyznacza właściwość CSS aspect-ratio — bez JavaScriptu i bez sztuczek z paddingiem — więc działa w komponentach serwerowych, a układ nigdy nie przeskakuje po hydratacji.

Przykłady

Popularne proporcje

Dobierz proporcje do treści, a potem trzymaj się ich w całej kolekcji, żeby siatki były równe.

1:1
Awatary, produkty
4:3
Zdjęcia, miniatury
3:2
Zdjęcia redakcyjne
16:9
Wideo, sekcje hero
21:9
Kinowe banery
import { AspectRatio } from "@/components/ui/aspect-ratio"

const ratios = [
  { label: "1:1", value: 1, use: "Awatary, produkty" },
  { label: "4:3", value: 4 / 3, use: "Zdjęcia, miniatury" },
  { label: "3:2", value: 3 / 2, use: "Zdjęcia redakcyjne" },
  { label: "16:9", value: 16 / 9, use: "Wideo, sekcje hero" },
  { label: "21:9", value: 21 / 9, use: "Kinowe banery" },
]

export default function AspectRatioRatios() {
  return (
    <div className="grid w-full max-w-2xl grid-cols-2 items-end gap-4 sm:grid-cols-5">
      {ratios.map((ratio) => (
        <div key={ratio.label} className="flex flex-col gap-2">
          <AspectRatio
            ratio={ratio.value}
            className="flex items-center justify-center rounded-lg border border-dashed border-brand-7 bg-brand-2"
          >
            <span className="font-mono text-xs font-medium text-brand-11">
              {ratio.label}
            </span>
          </AspectRatio>
          <span className="text-xs text-muted-foreground">{ratio.use}</span>
        </div>
      ))}
    </div>
  )
}

Z Next.js Image

Użyj fill, aby obraz przyjął rozmiar ramki, i object-cover, aby go kadrować zamiast rozciągać. Przekaż sizes, żeby przeglądarka pobrała odpowiednią rozdzielczość.

import Image from "next/image"

<AspectRatio ratio={16 / 9} className="overflow-hidden rounded-xl bg-muted">
  <Image
    src="/showcase/dashboard.png"
    alt="The analytics dashboard in dark mode"
    fill
    sizes="(min-width: 768px) 50vw, 100vw"
    className="object-cover"
  />
</AspectRatio>

Wideo

Wideo i osadzone treści powinny zarezerwować swoją ramkę, zanim się załadują. Kontrolki i metadane nakładaj za pomocą pozycjonowania absolutnego — ramka ma już relative.

12:48
Projektowanie z tokenamiprfct Talks · Odcinek 4
import { PlayIcon } from "lucide-react"

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

export default function AspectRatioVideo() {
  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <AspectRatio
        ratio={16 / 9}
        // Always dark, like a video poster. The fill and the edge keep the
        // frame visible on a dark page too.
        className="dark overflow-hidden rounded-xl border bg-muted"
      >
        <div className="absolute inset-0 flex items-center justify-center">
          <Button
            size="icon-lg"
            variant="default"
            aria-label="Odtwórz film: Projektowanie z tokenami"
            className="size-14 rounded-full"
          >
            <PlayIcon className="size-5! translate-x-px fill-current" />
          </Button>
        </div>
        <span className="absolute right-3 bottom-3 rounded-md bg-background px-1.5 py-0.5 font-mono text-[0.6875rem] text-foreground numeric">
          12:48
        </span>
      </AspectRatio>
      <div className="flex flex-col gap-0.5">
        <span className="text-sm font-semibold text-foreground">
          Projektowanie z tokenami
        </span>
        <span className="text-body-xs text-muted-foreground">
          prfct Talks · Odcinek 4
        </span>
      </div>
    </div>
  )
}
<AspectRatio ratio={16 / 9} className="overflow-hidden rounded-xl">
  <iframe
    src="https://www.youtube-nocookie.com/embed/…"
    title="Designing with tokens"
    allow="accelerometer; encrypted-media; picture-in-picture"
    allowFullScreen
    className="absolute inset-0 size-full"
  />
</AspectRatio>

Kwadratowa siatka

Proporcje 1:1 sprawiają, że awatary, zdjęcia produktów i miniatury wyglądają jednolicie, nawet gdy obrazy źródłowe nie są jednolite.

  • Ana
  • Jonas
  • Maya
  • Theo
  • Iris
  • Lena
  • Omar
  • Eli
import { AspectRatio } from "@/components/ui/aspect-ratio"

const people = [
  { initials: "AK", name: "Ana" },
  { initials: "JB", name: "Jonas" },
  { initials: "MC", name: "Maya" },
  { initials: "TP", name: "Theo" },
  { initials: "IV", name: "Iris" },
  { initials: "LR", name: "Lena" },
  { initials: "OS", name: "Omar" },
  { initials: "EW", name: "Eli" },
]

export default function AspectRatioSquareGrid() {
  return (
    <ul className="grid w-full max-w-md grid-cols-4 gap-3">
      {people.map((person) => (
        <li key={person.initials} className="flex flex-col gap-1.5">
          <AspectRatio
            ratio={1}
            className="flex items-center justify-center rounded-xl bg-muted text-lg font-medium text-muted-foreground"
          >
            <span aria-hidden="true">{person.initials}</span>
          </AspectRatio>
          <span className="text-center text-xs text-muted-foreground">
            {person.name}
          </span>
        </li>
      ))}
    </ul>
  )
}

Media w karcie

Umieść ramkę z proporcjami na początku karty i dodaj karcie pt-0 — media wypełnią ją od krawędzi do krawędzi, z zachowaniem zaokrąglonych narożników karty.

Nowość
Bursztynowa lampka biurkowa
189 $
import { LampDeskIcon } from "lucide-react"

import { AspectRatio } from "@/components/ui/aspect-ratio"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"

export default function AspectRatioCardMedia() {
  return (
    <Card className="w-full max-w-xs pt-0">
      <AspectRatio
        ratio={4 / 3}
        className="flex items-center justify-center border-b bg-muted"
      >
        <LampDeskIcon
          aria-hidden="true"
          className="size-10 text-muted-foreground"
          strokeWidth={1.25}
        />
        <Badge variant="outline" className="absolute top-3 left-3 bg-card">
          Nowość
        </Badge>
      </AspectRatio>
      <CardHeader>
        <CardTitle>Bursztynowa lampka biurkowa</CardTitle>
        <CardDescription>
          Ręcznie wykańczany mosiądz, ciepłe światło z regulacją jasności.
        </CardDescription>
      </CardHeader>
      <CardContent>
        <span className="text-heading-sm text-foreground numeric">189 $</span>
      </CardContent>
      <CardFooter>
        <Button className="w-full">Dodaj do koszyka</Button>
      </CardFooter>
    </Card>
  )
}

Wytyczne

Kiedy używać

  • Do obrazów, wideo, map i osadzonych treści, których kontener zmienia szerokość wraz z układem.
  • W siatkach i karuzelach — aby każdy kafelek miał ten sam kształt.
  • Wszędzie tam, gdzie wczytywane z opóźnieniem media spychałyby treść w dół.

Kiedy nie używać

  • Do tekstu lub interfejsu, którego wysokość zależy od treści — sztywne proporcje utną zawartość albo zostawią puste miejsce.
  • Gdy kontener ma już stałe wymiary w pikselach — ustaw je bezpośrednio.

Kadruj, nie zniekształcaj

Media w ramce z proporcjami powinny używać object-cover (kadrowanie z wypełnieniem ramki) lub object-contain (wpasowanie z pustymi pasami), nigdy domyślnego rozciągania.

Dobrzeobject-cover wypełnia ramkę i zachowuje proporcje.
ŹleRozciągnięte media wyglądają na zepsute, niezależnie od tego, jak dobre jest zdjęcie.

Dostępność

Aspect Ratio pełni wyłącznie funkcję prezentacyjną; liczą się media w jego wnętrzu.

  • Obrazy potrzebują tekstu alt, który mówi, co przedstawiają, albo alt="", gdy są dekoracyjne.
  • Wideo potrzebuje napisów, a osadzone treści — atrybutu title, który nazywa zawartość.
  • Stabilny układ. Zarezerwowane miejsce sprawia, że treść nie skacze podczas wczytywania mediów. To pomaga wszystkim — a zwłaszcza osobom, które korzystają z powiększenia lub czytają wolniej.

Dokumentacja API

AspectRatio

Renderuje <div> z position: relative i aspect-ratio ustawionym na podstawie ratio. Przyjmuje wszystkie propsy elementu <div>.

PropTypDomyślnie
ratiowymagany

Szerokość podzielona przez wysokość, np. 16 / 9 lub 1.

numberBrak wartości domyślnej
className

Zaokrąglenie, przycinanie zawartości i tło ramki.

stringBrak wartości domyślnej