Layout
Aspect ratio
Holds a width-to-height ratio for an image, video or embed, so the page does not shift as it loads.
- Built on
aspect-ratioobject-fit- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 0.9 kB
- Dependencies
- React
Try its options
Loading its options.
Code
import { AspectRatio } from "@/components/equip/aspect-ratio";
export default function Demo() {
return (
<figure style={{ width: 176, margin: 0 }}>
<AspectRatio ratio="16 / 9">
<div
style={{
display: "grid",
placeItems: "center",
background: "var(--eq-tint-strong)",
color: "var(--eq-ink-2)",
}}
>
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="9" />
<path d="M10 8.5v7l5.5-3.5Z" />
</svg>
</div>
</AspectRatio>
<figcaption style={{ marginTop: 6, fontSize: "var(--eq-text-xs)", color: "var(--eq-ink-2)" }}>
Onboarding walkthrough, 2:14
</figcaption>
</figure>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<figure style="width:176px;margin:0">
<div class="eq-aspect-ratio" style="--_ratio:16 / 9" data-fit="cover" data-radius="m">
<div style="display:grid;place-items:center;background:var(--eq-tint-strong);color:var(--eq-ink-2)">
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="9" />
<path d="M10 8.5v7l5.5-3.5Z" />
</svg>
</div>
</div>
<figcaption style="margin-top:6px;font-size:var(--eq-text-xs);color:var(--eq-ink-2)">Onboarding walkthrough, 2:14</figcaption>
</figure>Examples
Common ratios
The frame keeps its shape at any width, so the page does not jump when an image or embed loads.
1
4:3
16:9
21:9
Code
import { AspectRatio } from "@/components/equip/aspect-ratio";
const ratios = [
{ ratio: "1", use: "Avatar, product" },
{ ratio: "4 / 3", use: "Photo" },
{ ratio: "16 / 9", use: "Video" },
{ ratio: "21 / 9", use: "Banner" },
];
export default function Example() {
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(7rem, 1fr))", gap: 12, width: "100%", alignItems: "end" }}>
{ratios.map(({ ratio, use }) => (
<figure key={ratio} style={{ margin: 0 }}>
<AspectRatio ratio={ratio}>
<div style={{ display: "grid", placeItems: "center", background: "var(--eq-tint-strong)", fontSize: "var(--eq-text-xs)", color: "var(--eq-ink)" }}>
{ratio.replace(" / ", ":")}
</div>
</AspectRatio>
<figcaption style={{ marginTop: 4, fontSize: "var(--eq-text-xs)", color: "var(--eq-ink-2)" }}>{use}</figcaption>
</figure>
))}
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(7rem, 1fr));gap:12px;width:100%;align-items:end">
<figure style="margin:0">
<div class="eq-aspect-ratio" style="--_ratio:1" data-fit="cover" data-radius="m">
<div style="display:grid;place-items:center;background:var(--eq-tint-strong);font-size:var(--eq-text-xs);color:var(--eq-ink)">1</div>
</div>
<figcaption style="margin-top:4px;font-size:var(--eq-text-xs);color:var(--eq-ink-2)">Avatar, product</figcaption>
</figure>
<figure style="margin:0">
<div class="eq-aspect-ratio" style="--_ratio:4 / 3" data-fit="cover" data-radius="m">
<div style="display:grid;place-items:center;background:var(--eq-tint-strong);font-size:var(--eq-text-xs);color:var(--eq-ink)">4:3</div>
</div>
<figcaption style="margin-top:4px;font-size:var(--eq-text-xs);color:var(--eq-ink-2)">Photo</figcaption>
</figure>
<figure style="margin:0">
<div class="eq-aspect-ratio" style="--_ratio:16 / 9" data-fit="cover" data-radius="m">
<div style="display:grid;place-items:center;background:var(--eq-tint-strong);font-size:var(--eq-text-xs);color:var(--eq-ink)">16:9</div>
</div>
<figcaption style="margin-top:4px;font-size:var(--eq-text-xs);color:var(--eq-ink-2)">Video</figcaption>
</figure>
<figure style="margin:0">
<div class="eq-aspect-ratio" style="--_ratio:21 / 9" data-fit="cover" data-radius="m">
<div style="display:grid;place-items:center;background:var(--eq-tint-strong);font-size:var(--eq-text-xs);color:var(--eq-ink)">21:9</div>
</div>
<figcaption style="margin-top:4px;font-size:var(--eq-text-xs);color:var(--eq-ink-2)">Banner</figcaption>
</figure>
</div>Cover and contain
A wide chart in a square frame: cover crops it to fill, contain shows all of it and lets the frame's tint show at the edges.
Code
import { AspectRatio } from "@/components/equip/aspect-ratio";
// An inline image, so the example makes no network request.
const chart =
"data:image/svg+xml," +
encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 100"><rect width="300" height="100" fill="#8a8578"/><path d="M0 80 L50 60 L100 70 L150 35 L200 45 L250 20 L300 28" fill="none" stroke="#f4f1e8" stroke-width="5" stroke-linejoin="round"/></svg>',
);
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 16 }}>
{(["cover", "contain"] as const).map((fit) => (
<figure key={fit} style={{ width: 132, margin: 0 }}>
<AspectRatio ratio={1} fit={fit}>
<img src={chart} alt="Weekly signups rising from 20 to 80" />
</AspectRatio>
<figcaption style={{ marginTop: 4, fontSize: "var(--eq-text-xs)", color: "var(--eq-ink-2)" }}>fit="{fit}"</figcaption>
</figure>
))}
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:flex;flex-wrap:wrap;gap:16px">
<figure style="width:132px;margin:0">
<div class="eq-aspect-ratio" style="--_ratio:1" data-fit="cover" data-radius="m">
<img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20300%20100%22%3E%3Crect%20width%3D%22300%22%20height%3D%22100%22%20fill%3D%22%238a8578%22%2F%3E%3Cpath%20d%3D%22M0%2080%20L50%2060%20L100%2070%20L150%2035%20L200%2045%20L250%2020%20L300%2028%22%20fill%3D%22none%22%20stroke%3D%22%23f4f1e8%22%20stroke-width%3D%225%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E" alt="Weekly signups rising from 20 to 80">
</div>
<figcaption style="margin-top:4px;font-size:var(--eq-text-xs);color:var(--eq-ink-2)">fit="cover"</figcaption>
</figure>
<figure style="width:132px;margin:0">
<div class="eq-aspect-ratio" style="--_ratio:1" data-fit="contain" data-radius="m">
<img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20300%20100%22%3E%3Crect%20width%3D%22300%22%20height%3D%22100%22%20fill%3D%22%238a8578%22%2F%3E%3Cpath%20d%3D%22M0%2080%20L50%2060%20L100%2070%20L150%2035%20L200%2045%20L250%2020%20L300%2028%22%20fill%3D%22none%22%20stroke%3D%22%23f4f1e8%22%20stroke-width%3D%225%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E" alt="Weekly signups rising from 20 to 80">
</div>
<figcaption style="margin-top:4px;font-size:var(--eq-text-xs);color:var(--eq-ink-2)">fit="contain"</figcaption>
</figure>
</div>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
ratio | number | string | "16 / 9" | Width over height, as a number such as 1.5 or a CSS ratio such as "4 / 3". |
fit | "cover" | "contain" | "fill" | "cover" | How an img, video or canvas inside is fitted: cover crops, contain letterboxes, fill stretches. |
radius | "none" | "s" | "m" | "l" | "m" | Corner rounding of the frame. |
...rest | ComponentProps<"div"> | none | Every native div attribute. |
Anatomy
| root | .eq-aspect-ratio | The frame. Its direct children fill it. |
|---|
Notes
- Renders on the server with no client JavaScript.
- Every direct child is stretched over the frame, so content can never change its height.
- The frame shows a light tint until the media arrives, and around contained media.
- Give images a real alt and iframes a title; the frame adds no semantics of its own.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/aspect-ratio.tsx31 lines
import type { ComponentProps, CSSProperties } from "react";
import "./aspect-ratio.css";
export type AspectRatioProps = ComponentProps<"div"> & {
/** Width over height, as a number (1.5) or a CSS ratio ("16 / 9"). */
ratio?: number | string;
/** How media inside is fitted: cover crops to fill, contain letterboxes, fill stretches. */
fit?: "cover" | "contain" | "fill";
/** Corner rounding of the frame. */
radius?: "none" | "s" | "m" | "l";
};
/** Holds a width-to-height ratio; the first child fills it. */
export function AspectRatio({
ratio = "16 / 9",
fit = "cover",
radius = "m",
className,
style,
...rest
}: AspectRatioProps) {
return (
<div
{...rest}
className={["eq-aspect-ratio", className].filter(Boolean).join(" ")}
style={{ "--_ratio": String(ratio), ...style } as CSSProperties}
data-fit={fit}
data-radius={radius}
/>
);
}components/equip/aspect-ratio.css50 lines
@layer equip {
.eq-aspect-ratio {
--_radius: var(--eq-radius-m);
position: relative;
box-sizing: border-box;
display: block;
width: 100%;
aspect-ratio: var(--_ratio, 16 / 9);
overflow: hidden;
border-radius: var(--_radius);
background: var(--eq-tint);
}
.eq-aspect-ratio[data-radius="none"] { --_radius: 0px; }
.eq-aspect-ratio[data-radius="s"] { --_radius: var(--eq-radius-s); }
.eq-aspect-ratio[data-radius="l"] { --_radius: var(--eq-radius-l); }
/* Every direct child fills the frame, so content can never stretch it. */
.eq-aspect-ratio > * {
position: absolute;
inset: 0;
box-sizing: border-box;
width: 100%;
height: 100%;
margin: 0;
border: 0;
}
.eq-aspect-ratio > picture > img {
display: block;
width: 100%;
height: 100%;
}
.eq-aspect-ratio > :is(img, video, canvas),
.eq-aspect-ratio > picture > img {
object-fit: cover;
}
.eq-aspect-ratio[data-fit="contain"] > :is(img, video, canvas),
.eq-aspect-ratio[data-fit="contain"] > picture > img {
object-fit: contain;
}
.eq-aspect-ratio[data-fit="fill"] > :is(img, video, canvas),
.eq-aspect-ratio[data-fit="fill"] > picture > img {
object-fit: fill;
}
}