Aspect Ratio
Utrzymuje media w stałych proporcjach szerokości do wysokości i rezerwuje dla nich miejsce, zanim treść się załaduje.
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.jsonUż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.
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.
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.
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.
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ą, alboalt="", 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>.