AspectRatio
Hold a ratio without layout shift, whatever loads inside it.
@luc/uiExamples
Ratio presets
Six named ratios, drawn at the same width so the difference is the shape. The box is reserved before anything loads inside it, which is why a media card never moves when its image arrives.
1 : 1
4 : 3
16 : 9
21 : 9
32 : 9
3 : 4
<AspectRatio ratio="square">…</AspectRatio>
<AspectRatio ratio="photo">…</AspectRatio>
<AspectRatio ratio="video">…</AspectRatio>
<AspectRatio ratio="wide">…</AspectRatio>
<AspectRatio ratio="ultrawide">…</AspectRatio>
<AspectRatio ratio="portrait">…</AspectRatio>A media card
The composition it exists for: the frame holds the space, the card's header is an ordinary child, and the whole card is laid out before the media resolves.
16 : 9
Momentum, 2019–2026
Weekly, log scale
4 : 3
Value, 2019–2026
Weekly, log scale
<Card className="overflow-hidden">
<AspectRatio ratio="video">
<img src="…" alt="…" className="size-full object-cover" />
</AspectRatio>
<CardHeader>
<CardTitle>Momentum, 2019–2026</CardTitle>
<CardDescription>Weekly, log scale</CardDescription>
</CardHeader>
</Card>A gallery
In a grid the ratio is what makes the rows line up: every thumbnail is the same shape before any of them has loaded, so the gallery has no reflow and no jump.
4 : 3
Chart
4 : 3
Report
4 : 3
Study
4 : 3
Screen
4 : 3
Note
4 : 3
Export
<Grid columns={3} gap="md">
<Stack gap="xs">
<AspectRatio ratio="photo">…</AspectRatio>
<Text variant="caption" tone="muted">Chart</Text>
</Stack>
</Grid>A numeric ratio
The presets are a convenience rather than the API: any number works, which is how a chart that has to match a panel's proportions is kept exact.
2.35 : 1
<AspectRatio ratio={2.35}>…</AspectRatio>Usage
import { AspectRatio } from "@luc/ui";<AspectRatio ratio="video">
<img src="…" alt="…" className="size-full object-cover" />
</AspectRatio>A preset name (`square`, `photo`, `video`, `wide`, `portrait`, `ultrawide`) or any number.
Package setup — peer requirements, the stylesheet and the provider — is on installation.
API
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
| Export | Kind | Signature |
|---|---|---|
AspectRatio | 2 documented props | ({ className, ratio, ...props }: AspectRatioProps) => import("@types/react").JSX.Element |
AspectRatio props
Type: AspectRatioProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
asChild?inherited | boolean | undefined | — | |
ratio? | number | AspectPreset | undefined | — | A numeric ratio, or a named preset. Default `1` (square). |
Guidelines
Do
- Use it wherever the media arrives late — an image, a chart canvas, an embed — because the reserved box is what removes the shift when it loads.
- Name the shape with a preset (`video`, `photo`, `wide`) and pass a number only for a ratio the presets do not have.
- Put the frame on the container and let the media fill it (`size-full object-cover`), so the child never decides the height.
Don't
- Do not set a height on the child as well: two sources for one box is how a ratio drifts.
- Do not use it to make a box a fixed pixel size — it holds a ratio, and the width still comes from the layout.
- Do not wrap a paragraph in a ratio box: prose has no ratio, and the text gets clipped rather than scaled.
Accessibility
The box is a `div` carrying a `ratio` style and no role, so what it wraps still needs its own name — `alt` on the image, a title on the frame. Because the space is reserved before the media loads, a reader with images disabled gets an empty box of the right size rather than a collapsed layout, and a screen reader is unaffected either way.
Family
Layout
Space, rhythm and the surfaces a page is made of.
10 documented entries in this family. Open the family index
Related