# AspectRatio

A box that keeps its shape whatever is inside it.

- Group: Surfaces
- Import: `import { AspectRatio } from '@misoto22/design'`
- Page: https://ui.misoto22.com/components/aspect-ratio/
- Related: skeleton, card

## When to reach for it

The height has to be known before the content loads — a media grid that would otherwise reflow every time an image arrives.

## Anatomy

- **Box** (required) — A relative, full-width <div> carrying aspect-ratio as an inline style. A style and not a class because Tailwind can only generate what it reads verbatim in the source, and this value arrives at runtime.
- **Children** (required) — Every DIRECT child, taken out of flow and stretched to fill the box. That is what guarantees the ratio holds: nothing inside can contribute a height, so content with no intrinsic size of its own still gets the whole box.
- **Crop** — object-cover on a direct <img> or <video>, so media fills the box rather than being letterboxed inside it. Content that must not be cropped sets object-contain on itself.

## Best practices

### Do

- Reach for it wherever a reflow would otherwise happen when an image lands. That reflow is the layout shift a Core Web Vitals score measures, and reserving the box is the whole fix.
- Set object-contain on the child when the whole picture matters — a logo, a diagram, a screenshot. The default crops, which is right for a photograph and wrong for anything with an edge that means something.
- Give it a width. It is w-full, so inside a container with no width of its own it has no height either, and a box with a ratio and no size is a box that is not there.

### Don’t

- Do not fall back to the padding-top percentage trick beside it. That percentage resolves against the WIDTH, which is why it works at all and also why it breaks as a flex child and eats the element’s own padding.
- Do not put text in it and expect the box to grow. Every child is absolutely positioned, so a paragraph longer than the box is clipped by overflow-hidden rather than pushing it open.

## Accessibility

- A plain box with no role: it constrains geometry and says nothing, so an <img> inside keeps its own alt and nothing is added to the accessible tree.
- Reserving the height before the content arrives is what stops the content under it moving out from under a pointer or a reader mid-tap.

## AspectRatio

A box that keeps its shape whatever is inside it. The one layout primitive that is genuinely hard to hand-roll. The `padding- top: 56.25%` trick everyone reaches for is a percentage of the WIDTH, which is why it works at all and also why it silently breaks the moment the box is a flex or grid child — and it takes the element's own padding with it. The modern `aspect-ratio` property does the same job in one line, and only holds if nothing inside forces a height, which is what the absolute positioning below guarantees. So: the box declares the ratio, and every direct child is stretched to fill it and taken out of flow. That means a child with no intrinsic size at all — an empty `<div>`, a map that measures its container, a skeleton — still gets the full box, and an `<img>` or `<video>` is cropped to cover it rather than letterboxed. Content that must not be cropped should set `object-contain` on itself. Reach for this when the height must be known before the content loads: a media grid that would otherwise reflow every time an image arrives is the usual case, and that reflow is the layout shift a Core Web Vitals score is measuring.

### Props

- `ratio` — `number | string` default `16 / 9`. Width over height. A number (`16 / 9`) or the CSS form (`'16 / 9'`). It is a style rather than a class because Tailwind can only generate what it can read verbatim in the source, and this value arrives at runtime.
- `children` — `ReactNode`.

Also accepts: `HTMLAttributes<HTMLDivElement>`.

## Example — reserved boxes

```tsx
import { AspectRatio, Text } from '@misoto22/design'

<div className="grid gap-4 sm:grid-cols-3">
  {RATIOS.map((ratio) => (
    <div key={ratio} className="flex flex-col gap-2">
      <AspectRatio
        ratio={ratio}
        className="rounded-(--radius) border border-(--rule-2) bg-(--stone)"
      >
        <div />
      </AspectRatio>
      <Text as="span" size="xs" tone="muted" className="font-mono">
        {ratio}
      </Text>
    </div>
  ))}
</div>
```

## Example — a grid that cannot reflow

```tsx
import { AspectRatio, Skeleton, Text } from '@misoto22/design'

<div className="grid w-full gap-4 sm:grid-cols-3">
  {POSTS.map((post) => (
    <article key={post.slug} className="flex flex-col gap-2">
      <AspectRatio ratio="3 / 2" className="rounded-(--radius) border border-(--rule-2)">
        <Skeleton />
      </AspectRatio>
      <Text as="span" size="sm">
        {post.title}
      </Text>
    </article>
  ))}
</div>
```

## Example — the ratio wins

```tsx
import { AspectRatio, Text } from '@misoto22/design'

<div className="grid w-full gap-4 sm:grid-cols-2">
  <div className="flex flex-col gap-2">
    <AspectRatio ratio="16 / 9" className="rounded-(--radius) border border-(--rule-2)">
      <Text size="sm" className="p-3">
        {NOTE}
      </Text>
    </AspectRatio>
    <Text as="span" size="xs" tone="muted" className="font-mono">
      clipped
    </Text>
  </div>
  <div className="flex flex-col gap-2">
    <AspectRatio ratio="16 / 9" className="rounded-(--radius) border border-(--rule-2)">
      <div className="overflow-y-auto p-3 scroll-slim">
        <Text size="sm">{NOTE}</Text>
      </div>
    </AspectRatio>
    <Text as="span" size="xs" tone="muted" className="font-mono">
      overflow-y-auto on the child
    </Text>
  </div>
</div>
```
