← All components

AspectRatio

A box that keeps its shape, so nothing below jumps when the picture arrives.

An abstract plate in pale blue and sand
16 : 9
The same kind of plate, cropped to a portrait
4 : 5

The idea

CSS `aspect-ratio` gives the box its height before the image has loaded, which is the whole reason to reserve the space. The media inside is cropped to fill rather than squashed to fit — a photo cropped at the edges is still the photo. An inset hairline is laid over it, not round it, so a pale image does not bleed into a white page and the edge follows the radius exactly.

Install

terminal1 line
npm i mihirmodi-ui motion

The tokens, the theme script and the routing provider are a one-time job — see Setup. Or skip the package entirely and take the source at the foot of this page.

Usage

AspectRatio — usage7 lines
import { AspectRatio } from "mihirmodi-ui";

// The box has its height before the image arrives, so nothing
// below it jumps. The image is cropped to fill, never squashed.
<AspectRatio ratio={16 / 9} className="rounded-[12px]">
  <img src="/plates/03.jpg" alt="The survey site at dawn" />
</AspectRatio>

Props

PropNotes
rationumberWidth over height.
edgebooleanThe inset hairline over the media.
classNamestringSet the radius and width here.

Source

Exactly what the package ships — this is read out of the source at build time, not a copy. Take it and adapt it if you would rather not add a dependency.

packages/ui/src/components/layout/AspectRatio.tsx45 lines
import type { CSSProperties, ReactNode } from "react";

export type AspectRatioProps = {
  /** Width over height: `16 / 9`, `4 / 3`, `1`. */
  ratio?: number;
  /** One image, video or iframe. It is stretched to the box and cropped, never squashed. */
  children: ReactNode;
  /**
   * An inset hairline over the media, so a pale photo does not bleed into a
   * white page. On by default; turn it off for media with its own edge.
   */
  edge?: boolean;
  /** On the box — set the radius and width here. */
  className?: string;
  style?: CSSProperties;
};

/**
 * A box that keeps its shape while its width changes, for media.
 *
 * The CSS `aspect-ratio` property does the work, so the box has its height
 * before the image has arrived — nothing below it jumps when the picture
 * loads, which is the whole reason to reserve the space. The media inside is
 * `object-cover`: a photo cropped at the edges is still the photo, and one
 * squashed to fit is a different one.
 *
 * The hairline is drawn over the media on an inset, not round the box as a
 * border, so it follows the radius exactly and cannot shift the media by half
 * a pixel. It is the same alpha grey as every other edge, so on a dark photo
 * it vanishes and on a white one it is just enough.
 */
export function AspectRatio({ ratio = 16 / 9, children, edge = true, className = "", style }: AspectRatioProps) {
  return (
    <div
      className={`relative w-full overflow-hidden bg-gray-alpha-100 [&>img]:absolute [&>img]:inset-0 [&>img]:h-full [&>img]:w-full [&>img]:object-cover [&>video]:absolute [&>video]:inset-0 [&>video]:h-full [&>video]:w-full [&>video]:object-cover [&>iframe]:absolute [&>iframe]:inset-0 [&>iframe]:h-full [&>iframe]:w-full ${
        edge
          ? "after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_0.5px_var(--color-gray-alpha-400)] after:content-['']"
          : ""
      } ${className}`}
      style={{ aspectRatio: String(ratio), ...style }}
    >
      {children}
    </div>
  );
}