// Copyright (c) Meta Platforms, Inc. and affiliates. /** * @file AspectRatio.tsx * @input Uses React, stylex * @output Exports AspectRatio component, AspectRatioProps, AspectRatioShape, AspectRatioFit * @position AspectRatio component; maintains a specific aspect ratio for its children * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/AspectRatio/AspectRatio.doc.mjs * - /packages/core/src/AspectRatio/AspectRatio.test.tsx * - /packages/core/src/reset.css (AspectRatio fit baseline rules) * - /apps/storybook/stories/AspectRatio.stories.tsx * - /packages/cli/assets/templates/blocks/components/AspectRatio/ (showcase blocks) */ import type { ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import type { BaseProps } from "../BaseProps.js"; import { mergeProps } from "../utils/index.js"; import { themeProps } from "../utils/themeProps.js"; /** * The shape of the aspect ratio container. * - `rectangle`: standard rectangular container (default). * - `ellipse`: clips the container to an ellipse. Combined with the `ratio`, * this renders a circle at `ratio={1}` and an oval at non-square ratios. */ export type AspectRatioShape = 'rectangle' | 'ellipse'; /** * How the child is sized and positioned inside the ratio box. * - `cover`: the child fills the box; media is cropped to preserve its own * aspect ratio (`object-fit: cover`). * - `contain`: the child fills the box; media is letterboxed to stay fully * visible (`object-fit: contain`). * - `center`: the child keeps its natural size and sits centered in the box. */ export type AspectRatioFit = 'cover' | 'contain' | 'center'; export interface AspectRatioProps extends BaseProps { /** Ref forwarded to the root element */ ref?: React.Ref; /** * The aspect ratio as width/height (e.g., 16/9 = 1.777..., 4/3 = 1.333..., 1 for square). * * The ratio is emitted as a class-level declaration — never an inline * style — so it can be made responsive: * - StyleX consumers pass an `aspect-ratio` rule via `xstyle`, including * under `@media`/`@container` conditions. * - Plain-CSS/Tailwind consumers override `aspect-ratio` from their own * (unlayered) rules: the compiled component styles live in the * `astryx-base` cascade layer, and unlayered CSS wins over layered * rules regardless of specificity. * * A responsive ratio is set in CSS, not on the component. For example, a 3:1 * hero can become 3:2 when its container narrows by targeting the element's * class from a container query: * `@container gallery (max-width: 720px) { .gallery-hero { aspect-ratio: 3 / 2; } }`. * * @example * ``` * ... * ``` */ ratio: number; /** * The shape of the container. Both shapes respect the provided `ratio`. * - `rectangle` (default): a standard rectangular container. * - `ellipse`: clips the container to an ellipse — a circle when `ratio={1}`, * or an oval at other ratios. Pair with `fit="cover"` so the media fills * the clipped container. * * @default 'rectangle' * * @example * ``` * * * * ``` */ shape?: AspectRatioShape; /** * How the child is laid out inside the ratio box, so the child does not * have to declare `width`/`height`/`objectFit` itself. * - `cover`: child fills the box; media is cropped (`object-fit: cover`). * - `contain`: child fills the box; media is letterboxed * (`object-fit: contain`). * - `center`: child keeps its natural size, centered in the box. * * `cover`/`contain` child sizing ships as zero-specificity baseline rules * in `reset.css`, keyed on the `data-astryx-aspect-ratio-override` * attribute the component sets on the child's direct parent. Any styles * the child sets itself still win, so children that already size * themselves keep their behavior. When omitted, the child is left * unstyled (the pre-existing contract). `fit` is structural, not visual, * so it is not exposed on the theming surface. * * @example * ``` * * Widescreen image * * ``` */ fit?: AspectRatioFit; /** * Content to render inside the aspect ratio container. * The child element will be positioned absolutely to fill the container. * * Pass a single child. With `fit` set, every direct child is stretched to * fill the box, so a second child is laid out below the first and clipped * out of view; compose an overlay or caption inside one wrapper child. */ children: ReactNode; } const styles = { container: { kVAEAm: "x1n2onr6", kzqmXN: "xh8yej3", kVQacm: "x7giv3", kAzted: "x2lwn1j", kmuXW: "x2lah0s", $$css: true as const }, ellipse: { kaIpWk: "x16rqkct", $$css: true as const } }; // The ratio compiles to a CSS variable + a class-level declaration // (aspect-ratio: var(--x)) instead of a raw inline style, so consumer // overrides — `xstyle` rules, including ones inside @media/@container // queries — can still win. A raw inline `aspect-ratio` would beat any class // (same rationale as Grid's dynamic track values). Non-StyleX consumers // don't need a dedicated hook: the compiled declaration sits in the // `astryx-base` cascade layer, so any unlayered consumer rule wins // regardless of specificity. const dynamicStyles = { ratio: (ratio: number) => [{ kOBAk4: ratio != null ? "x10y9f9r" : ratio, $$css: true as const }, { "--x-aspectRatio": ratio != null ? ratio : undefined }] }; /** * AspectRatio component for maintaining a specific aspect ratio for its children. * * Uses the CSS aspect-ratio property to maintain the ratio. The child element * is positioned absolutely to fill the container, which is useful for images, * videos, embeds, and placeholders. * * Use `fit` to let the component size the child: `cover`/`contain` stretch * the child to fill the box (cropping or letterboxing media), `center` keeps * the child at its natural size. Without `fit`, the child styles itself. * * Use `shape="ellipse"` to clip the container into an ellipse — a circle at * `ratio={1}` or an oval at other ratios. Both shapes respect the provided * `ratio`. * * The ratio is a class-level declaration (not an inline style), so it can be * overridden responsively — via `xstyle` for StyleX consumers, or with a * plain unlayered `aspect-ratio` rule for plain-CSS consumers (component * styles live in the `astryx-base` cascade layer, which unlayered CSS beats). * * Sizing: the box takes its width from its container and derives its height * from the ratio, so it needs an ancestor with a definite width. Two * consequences are worth knowing. Constraining only the height (`height` or * `maxHeight`) clamps the box without releasing the width, so the rendered * ratio no longer matches `ratio`; pass `width: 'auto'` alongside to size from * the height instead. And in a shrink-to-fit parent (`inline-flex`, * `width: fit-content`, a floated box) it contributes no intrinsic width and * collapses to zero. * * @example * ``` * * Widescreen image * * ``` * * @example * ``` * * * * ``` */ export function AspectRatio({ ratio, shape = 'rectangle', fit, children, xstyle, className, style, ref, ...props }: AspectRatioProps) { return
{/* The marker attribute carries the fit value so the reset.css child sizing can use direct-child selectors on this wrapper — the child's actual parent — without depending on AspectRatio's internal structure or on the theming surface (fit is structural, not themeable). The name is namespaced to avoid collisions. */}
{children}
; } AspectRatio.displayName = 'AspectRatio';