shshadcn-htmx

Components

Aspect Ratio

A ratio-box wrapper that locks a child — image, video, <iframe>, embed, or chart slot — to a fixed width-to-height ratio while it resizes fluidly, killing layout shift. One CSS declaration (native aspect-ratio), zero JavaScript.

Installation

1. Install via the shadcn CLI

npx shadcn@latest add http://localhost/r/aspect-ratio.json

2. Use it

components/ui/aspect-ratio.tsx
import { AspectRatio } from "@/components/ui/aspect-ratio"

// 16:9 photo, cropped to fill (default fit="cover")
<AspectRatio ratio="16/9">
  <img src="/photo.jpg" alt="Mountain lake at dawn" />
</AspectRatio>

// Square, letterboxed so nothing is cropped
<AspectRatio ratio="1/1" fit="contain">
  <img src="/logo.png" alt="Brand logo" />
</AspectRatio>

// Responsive video embed
<AspectRatio ratio="16/9">
  <iframe src="https://www.youtube-nocookie.com/embed/ID" title="Talk" allowfullscreen />
</AspectRatio>
Or copy the source manually
components/ui/aspect-ratio.tsx
/** @jsxImportSource hono/jsx */
import type { Child } from "hono/jsx"
import { cloneElement, isValidElement } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"

// Aspect Ratio — shadcn-htmx, htmx v4 + Tailwind v4.
//
// A ratio-box wrapper that locks a child (image / video / iframe / embed /
// chart slot) to a fixed width-to-height ratio while it resizes fluidly,
// eliminating layout shift. One CSS declaration does all the work — there
// is no JavaScript here.
//
// shadcn/ui's upstream AspectRatio wraps Radix's primitive, which predates
// native browser support and emulates the ratio with a padding-bottom hack
// + absolute positioning. We do NOT copy that: the platform now ships the
// real thing, so we use the native CSS `aspect-ratio` property instead. No
// hacks (see AGENTS.md rule 4).
//   Upstream (anatomy only):
//     repos/shadcn-ui/apps/v4/registry/new-york-v4/ui/aspect-ratio.tsx
//
// Built on:
//   - CSS `aspect-ratio` — defines the desired width-to-height ratio of the
//     box; the browser keeps it as the box resizes. At least one of the
//     box's sizes must be automatic for it to take effect (we leave height
//     auto, width fluid).
//       repos/mdn/files/en-us/web/css/reference/properties/aspect-ratio/index.md
//   - CSS `object-fit` — how a replaced element (img/video) fills the box:
//     `cover` crops to fill, `contain` letterboxes to fit. Note object-fit
//     has no effect on <iframe>/<embed>, which already stretch to the box.
//       repos/mdn/files/en-us/web/css/reference/properties/object-fit/index.md
//   - web.dev "Aspect ratio image card" pattern (`aspect-ratio: 16 / 9`,
//     no padding-top hack):
//       repos/web.dev/src/site/content/en/patterns/layout/aspect-ratio-image-card/index.md
//
// Tailwind v4: `aspect-video` = 16/9, `aspect-square` = 1/1; any other
// ratio is the arbitrary `aspect-[w/h]` utility. `object-cover` /
// `object-contain` map to the object-fit keywords.

export type AspectRatioFit = "cover" | "contain"

// Named ratios mapped to Tailwind's stock aspect utilities; everything else
// falls through to the arbitrary `aspect-[w/h]` form below.
const NAMED_RATIO: Record<string, string> = {
  "1/1": "aspect-square",
  "16/9": "aspect-video",
}

const fitClasses: Record<AspectRatioFit, string> = {
  cover: "object-cover",
  contain: "object-contain",
}

// Turn a ratio prop into a Tailwind class. Accepts:
//   - a number   → 1.78        → aspect-[1.78]
//   - "16/9"     → aspect-video (or aspect-[w/h] for unmapped ratios)
function ratioClass(ratio: number | string): string {
  if (typeof ratio === "number") return `aspect-[${ratio}]`
  const key = ratio.replace(/\s+/g, "")
  return NAMED_RATIO[key] ?? `aspect-[${key}]`
}

type AspectRatioProps = {
  // Width-to-height ratio. A number (e.g. 1.778) or a "w/h" string
  // (e.g. "16/9", "4/3"). Defaults to a 16:9 video frame.
  ratio?: number | string
  // How a replaced child (img/video) fills the box. `cover` crops, `contain`
  // letterboxes. Ignored for non-replaced children (iframe/embed/div).
  fit?: AspectRatioFit
  class?: ClassValue
  id?: string
  // The locked element: an <img>, <video>, <iframe>, <embed>, or any block.
  // A single valid element child is cloned so the sizing + object-fit
  // classes land directly on it (the wrapper only carries the ratio).
  children?: Child
  // Forward hx-*, data-*, aria-*, and standard attributes onto the root.
  [key: string]: unknown
}

const root = "relative block w-full overflow-hidden"

export function AspectRatio(props: AspectRatioProps) {
  const {
    ratio = "16/9",
    fit = "cover",
    class: className,
    id,
    children,
    ...rest
  } = props

  const rootClasses = cn(root, ratioClass(ratio), className)

  // Annotate the single child so it stretches to fill the ratio box and
  // applies object-fit (mirrors button/tooltip cloneElement convention).
  let child: Child = children
  if (isValidElement(children)) {
    const el = children as any
    child = cloneElement(el, {
      "data-slot": "aspect-ratio-content",
      class: cn("size-full", fitClasses[fit], el?.props?.class),
    })
  }

  return (
    <div
      id={id}
      data-slot="aspect-ratio"
      data-ratio={typeof ratio === "number" ? String(ratio) : ratio}
      class={rootClasses}
      {...rest}
    >
      {child}
    </div>
  )
}

1. Save the file

Copy aspect-ratio.html into templates/components/.

2. Use it

templates/components/aspect-ratio.html
{% from "components/aspect-ratio.html" import aspect_ratio %}

{% call aspect_ratio(ratio="16/9") %}
  <img src="/photo.jpg" alt="Mountain lake at dawn" class="size-full object-cover">
{% endcall %}
View source
templates/components/aspect-ratio.html
{# Aspect Ratio macro — shadcn-htmx, htmx v4 + Tailwind v4.
   Mirrors registry/ui/aspect-ratio.tsx.

   Locks the slotted child to a fixed width-to-height ratio with the native
   CSS `aspect-ratio` property (no padding-top hack) and `object-fit` for
   replaced elements. Zero JS.
     MDN aspect-ratio: repos/mdn/files/en-us/web/css/reference/properties/aspect-ratio/index.md
     MDN object-fit:   repos/mdn/files/en-us/web/css/reference/properties/object-fit/index.md
     web.dev pattern:  repos/web.dev/src/site/content/en/patterns/layout/aspect-ratio-image-card/index.md

   The slotted child (passed via {{ caller() }}) should carry
   `size-full object-cover` / `object-contain` itself so the fit lands on
   the replaced element — mirroring how the .tsx clones the child.

   Usage:
     {% from "components/aspect-ratio.html" import aspect_ratio %}
     {% call aspect_ratio(ratio="16/9") %}
       <img src="/photo.jpg" alt="…" class="size-full object-cover">
     {% endcall %} #}

{% macro aspect_ratio(ratio="16/9", id=none, extra_class="", **attrs) %}
{%- set ratio_class -%}
{%- if ratio == "1/1" -%}aspect-square{%- elif ratio == "16/9" -%}aspect-video{%- else -%}aspect-[{{ ratio | replace(' ', '') }}]{%- endif -%}
{%- endset -%}
<div
  {%- if id %} id="{{ id }}"{% endif %}
  data-slot="aspect-ratio"
  data-ratio="{{ ratio }}"
  class="relative block w-full overflow-hidden {{ ratio_class }} {{ extra_class }}"
  {%- for k, v in attrs.items() %} {{ k|replace('_','-') }}="{{ v }}"{% endfor %}>{{ caller() }}</div>
{% endmacro %}

1. Save the file

Add aspect-ratio.tmpl alongside your templates.

2. Use it

components/aspect-ratio.tmpl
{{template "aspect-ratio" (dict
  "Ratio" "16/9"
  "Body" "<img src=\"/photo.jpg\" alt=\"Mountain lake\" class=\"size-full object-cover\">")}}
View source
components/aspect-ratio.tmpl
{{/*
  Aspect Ratio template — shadcn-htmx, htmx v4 + Tailwind v4.
  Mirrors registry/ui/aspect-ratio.tsx.

  Locks the slotted child to a fixed width-to-height ratio with the native
  CSS `aspect-ratio` property (no padding-top hack) and `object-fit` for
  replaced elements. Zero JS.
    MDN aspect-ratio: repos/mdn/files/en-us/web/css/reference/properties/aspect-ratio/index.md
    MDN object-fit:   repos/mdn/files/en-us/web/css/reference/properties/object-fit/index.md
    web.dev pattern:  repos/web.dev/src/site/content/en/patterns/layout/aspect-ratio-image-card/index.md

      type AspectRatioArgs struct {
          Ratio string      // "16/9" (default) | "1/1" | "4/3" | …
          ID    string
          Class string
          Body  template.HTML // the slotted child, already carrying
                              // size-full object-cover / object-contain
      }
*/}}

{{define "aspect-ratio"}}
{{- $ratio := or .Ratio "16/9" -}}
{{- $ratioClass := printf "aspect-[%s]" $ratio -}}
{{- if eq $ratio "1/1" -}}{{- $ratioClass = "aspect-square" -}}{{- else if eq $ratio "16/9" -}}{{- $ratioClass = "aspect-video" -}}{{- end -}}
<div {{if .ID}}id="{{.ID}}" {{end}}data-slot="aspect-ratio" data-ratio="{{$ratio}}" class="relative block w-full overflow-hidden {{$ratioClass}} {{.Class}}">{{htmlSafe .Body}}</div>
{{end}}

1. Save the file

Drop aspect_ratio.ex into lib/my_app_web/components/.

2. Use it

lib/my_app_web/components/aspect_ratio.ex
<.aspect_ratio ratio="16/9">
  <img src="/photo.jpg" alt="Mountain lake at dawn" class="size-full object-cover" />
</.aspect_ratio>
View source
lib/my_app_web/components/aspect_ratio.ex
defmodule ShadcnHtmx.Components.AspectRatio do
  @moduledoc """
  Aspect Ratio — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.

  Mirrors registry/ui/aspect-ratio.tsx.

  Locks the slotted child (image / video / iframe / embed / chart) to a
  fixed width-to-height ratio with the native CSS `aspect-ratio` property
  (no padding-top hack) and `object-fit` for replaced elements. Zero JS.

    * MDN aspect-ratio:
      repos/mdn/files/en-us/web/css/reference/properties/aspect-ratio/index.md
    * MDN object-fit:
      repos/mdn/files/en-us/web/css/reference/properties/object-fit/index.md
    * web.dev pattern:
      repos/web.dev/src/site/content/en/patterns/layout/aspect-ratio-image-card/index.md

  The slotted child should carry `size-full object-cover` / `object-contain`
  so the fit lands on the replaced element — mirroring how the .tsx clones
  the child.

  ## Examples

      <.aspect_ratio ratio="16/9">
        <img src="/photo.jpg" alt="…" class="size-full object-cover" />
      </.aspect_ratio>

      <.aspect_ratio ratio="1/1">
        <img src="/avatar.jpg" alt="…" class="size-full object-cover" />
      </.aspect_ratio>
  """

  use Phoenix.Component

  @root "relative block w-full overflow-hidden"

  attr :ratio, :string, default: "16/9"
  attr :class, :string, default: nil
  attr :rest, :global
  slot :inner_block, required: true

  def aspect_ratio(assigns) do
    assigns =
      assigns
      |> assign(:root, @root)
      |> assign(:ratio_class, ratio_class(assigns.ratio))

    ~H"""
    <div
      data-slot="aspect-ratio"
      data-ratio={@ratio}
      class={[@root, @ratio_class, @class]}
      {@rest}
    >
      {render_slot(@inner_block)}
    </div>
    """
  end

  defp ratio_class("1/1"), do: "aspect-square"
  defp ratio_class("16/9"), do: "aspect-video"
  defp ratio_class(ratio), do: "aspect-[#{String.replace(ratio, " ", "")}]"
end

1. Save the file

Paste the markup; relies only on theme tokens.

2. Use it

snippets/aspect-ratio.html
<div data-slot="aspect-ratio" data-ratio="16/9"
     class="relative block w-full overflow-hidden aspect-video">
  <img src="/photo.jpg" alt="Mountain lake at dawn"
       data-slot="aspect-ratio-content" class="size-full object-cover">
</div>
View source
snippets/aspect-ratio.html
<!--
  shadcn-htmx — raw HTML aspect-ratio snippets.

  Locks the slotted child to a fixed width-to-height ratio with the native
  CSS `aspect-ratio` property (no padding-top hack). Replaced elements
  (img/video) get `object-fit` via object-cover / object-contain; iframe /
  embed already stretch to the box (object-fit has no effect on them).
  Zero JavaScript — Tailwind utilities only.

    MDN aspect-ratio: repos/mdn/files/en-us/web/css/reference/properties/aspect-ratio/index.md
    MDN object-fit:   repos/mdn/files/en-us/web/css/reference/properties/object-fit/index.md
    web.dev pattern:  repos/web.dev/src/site/content/en/patterns/layout/aspect-ratio-image-card/index.md

  ROOT:
    relative block w-full overflow-hidden  +  the ratio utility
  RATIO utility:
    1/1  → aspect-square
    16/9 → aspect-video
    any  → aspect-[w/h]   e.g. aspect-[4/3]
-->

<!-- 16:9 image, cropped to fill (object-cover) -->
<div data-slot="aspect-ratio" data-ratio="16/9"
     class="relative block w-full overflow-hidden aspect-video">
  <img src="/photo.jpg" alt="Mountain lake at dawn"
       data-slot="aspect-ratio-content"
       class="size-full object-cover">
</div>

<!-- 1:1 square, letterboxed to fit (object-contain) -->
<div data-slot="aspect-ratio" data-ratio="1/1"
     class="relative block w-full overflow-hidden aspect-square">
  <img src="/logo.png" alt="Brand logo"
       data-slot="aspect-ratio-content"
       class="size-full object-contain">
</div>

<!-- 16:9 responsive iframe (video embed) — object-fit has no effect here;
     the iframe simply stretches to the ratio box. -->
<div data-slot="aspect-ratio" data-ratio="16/9"
     class="relative block w-full overflow-hidden aspect-video">
  <iframe src="https://www.youtube-nocookie.com/embed/VIDEO_ID"
          title="Embedded video"
          data-slot="aspect-ratio-content"
          class="size-full" allowfullscreen></iframe>
</div>

<!-- Arbitrary 4:3 ratio with a plain content slot (e.g. a chart canvas) -->
<div data-slot="aspect-ratio" data-ratio="4/3"
     class="relative block w-full overflow-hidden aspect-[4/3]">
  <div data-slot="aspect-ratio-content"
       class="flex size-full items-center justify-center bg-muted text-muted-foreground">
    4 / 3 slot
  </div>
</div>

Examples

16:9 — fluid image with no layout shift

The wrapper carries the ratio; the <img> stretches to fill it and is cropped with object-cover.

The wrapper reserves the right amount of space before the image loads, so there is no reflow when it arrives — the classic cause of Cumulative Layout Shift. The ratio is the native CSS aspect-ratio property (Tailwind's aspect-video = 16/9); no padding-top hack, no JavaScript.

Gradient placeholder, 640 by 360
<AspectRatio ratio="16/9" class="rounded-lg border">
  <img src="/photo.jpg" alt="Mountain lake at dawn" />
</AspectRatio>
{% call aspect_ratio(ratio="16/9", extra_class="rounded-lg border") %}
  <img src="/photo.jpg" alt="Mountain lake at dawn" class="size-full object-cover">
{% endcall %}
{{template "aspect-ratio" (dict "Ratio" "16/9" "Class" "rounded-lg border"
  "Body" "<img src=\"/photo.jpg\" alt=\"Mountain lake\" class=\"size-full object-cover\">")}}
<.aspect_ratio ratio="16/9" class="rounded-lg border">
  <img src="/photo.jpg" alt="Mountain lake at dawn" class="size-full object-cover" />
</.aspect_ratio>
<div class="w-full max-w-md">
  <div data-slot="aspect-ratio" data-ratio="16/9" class="relative block w-full overflow-hidden aspect-video rounded-lg border">
    <img src="data:image/svg+xml;utf8,%3Csvg%20xmlns%3D&#39;http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg&#39;%20width%3D&#39;640&#39;%20height%3D&#39;360&#39;%3E%3Cdefs%3E%3ClinearGradient%20id%3D&#39;g&#39;%20x1%3D&#39;0&#39;%20y1%3D&#39;0&#39;%20x2%3D&#39;1&#39;%20y2%3D&#39;1&#39;%3E%3Cstop%20offset%3D&#39;0&#39;%20stop-color%3D&#39;%252306b6d4&#39;%2F%3E%3Cstop%20offset%3D&#39;1&#39;%20stop-color%3D&#39;%25237c3aed&#39;%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D&#39;640&#39;%20height%3D&#39;360&#39;%20fill%3D&#39;url(%2523g)&#39;%2F%3E%3Ctext%20x%3D&#39;50%25&#39;%20y%3D&#39;50%25&#39;%20fill%3D&#39;white&#39;%20font-family%3D&#39;sans-serif&#39;%20font-size%3D&#39;28&#39;%20text-anchor%3D&#39;middle&#39;%20dominant-baseline%3D&#39;middle&#39;%3E640%20%C3%97%20360%3C%2Ftext%3E%3C%2Fsvg%3E" alt="Gradient placeholder, 640 by 360" data-slot="aspect-ratio-content" class="size-full object-cover">
  </img>
</div>
</div>

Cover vs contain — how the child fills the box

fit="cover" (default) crops to fill; fit="contain" letterboxes so nothing is cut off.

Both boxes are the same 1:1 square. The left uses object-cover — the image is scaled up and cropped to fill. The right uses object-contain — the whole image is shown, with empty space around it. Use cover for photos, contain for logos or diagrams you must not crop. (object-fit has no effect on <iframe> or <embed>.)

Cover fit, cropped to fill
Contain fit, letterboxed
<AspectRatio ratio="1/1" fit="cover">
  <img src="/photo.jpg" alt="…" />
</AspectRatio>

<AspectRatio ratio="1/1" fit="contain">
  <img src="/logo.png" alt="…" />
</AspectRatio>
{% call aspect_ratio(ratio="1/1") %}
  <img src="/photo.jpg" alt="…" class="size-full object-cover">
{% endcall %}

{% call aspect_ratio(ratio="1/1") %}
  <img src="/logo.png" alt="…" class="size-full object-contain">
{% endcall %}
{{template "aspect-ratio" (dict "Ratio" "1/1"
  "Body" "<img src=\"/photo.jpg\" class=\"size-full object-cover\">")}}

{{template "aspect-ratio" (dict "Ratio" "1/1"
  "Body" "<img src=\"/logo.png\" class=\"size-full object-contain\">")}}
<.aspect_ratio ratio="1/1">
  <img src="/photo.jpg" alt="…" class="size-full object-cover" />
</.aspect_ratio>

<.aspect_ratio ratio="1/1">
  <img src="/logo.png" alt="…" class="size-full object-contain" />
</.aspect_ratio>
<div class="grid w-full max-w-md grid-cols-2 gap-4">
  <div data-slot="aspect-ratio" data-ratio="1/1" class="relative block w-full overflow-hidden aspect-square rounded-lg border bg-muted">
    <img src="data:image/svg+xml;utf8,%3Csvg%20xmlns%3D&#39;http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg&#39;%20width%3D&#39;640&#39;%20height%3D&#39;360&#39;%3E%3Cdefs%3E%3ClinearGradient%20id%3D&#39;g&#39;%20x1%3D&#39;0&#39;%20y1%3D&#39;0&#39;%20x2%3D&#39;1&#39;%20y2%3D&#39;1&#39;%3E%3Cstop%20offset%3D&#39;0&#39;%20stop-color%3D&#39;%252306b6d4&#39;%2F%3E%3Cstop%20offset%3D&#39;1&#39;%20stop-color%3D&#39;%25237c3aed&#39;%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D&#39;640&#39;%20height%3D&#39;360&#39;%20fill%3D&#39;url(%2523g)&#39;%2F%3E%3Ctext%20x%3D&#39;50%25&#39;%20y%3D&#39;50%25&#39;%20fill%3D&#39;white&#39;%20font-family%3D&#39;sans-serif&#39;%20font-size%3D&#39;28&#39;%20text-anchor%3D&#39;middle&#39;%20dominant-baseline%3D&#39;middle&#39;%3E640%20%C3%97%20360%3C%2Ftext%3E%3C%2Fsvg%3E" alt="Cover fit, cropped to fill" data-slot="aspect-ratio-content" class="size-full object-cover">
  </img>
</div>
<div data-slot="aspect-ratio" data-ratio="1/1" class="relative block w-full overflow-hidden aspect-square rounded-lg border bg-muted">
  <img src="data:image/svg+xml;utf8,%3Csvg%20xmlns%3D&#39;http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg&#39;%20width%3D&#39;640&#39;%20height%3D&#39;360&#39;%3E%3Cdefs%3E%3ClinearGradient%20id%3D&#39;g&#39;%20x1%3D&#39;0&#39;%20y1%3D&#39;0&#39;%20x2%3D&#39;1&#39;%20y2%3D&#39;1&#39;%3E%3Cstop%20offset%3D&#39;0&#39;%20stop-color%3D&#39;%252306b6d4&#39;%2F%3E%3Cstop%20offset%3D&#39;1&#39;%20stop-color%3D&#39;%25237c3aed&#39;%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20width%3D&#39;640&#39;%20height%3D&#39;360&#39;%20fill%3D&#39;url(%2523g)&#39;%2F%3E%3Ctext%20x%3D&#39;50%25&#39;%20y%3D&#39;50%25&#39;%20fill%3D&#39;white&#39;%20font-family%3D&#39;sans-serif&#39;%20font-size%3D&#39;28&#39;%20text-anchor%3D&#39;middle&#39;%20dominant-baseline%3D&#39;middle&#39;%3E640%20%C3%97%20360%3C%2Ftext%3E%3C%2Fsvg%3E" alt="Contain fit, letterboxed" data-slot="aspect-ratio-content" class="size-full object-contain">
</img>
</div>
</div>

Further reading

Responsive video embed

Wrap an <iframe> so a video embed stays 16:9 at every width — the most common reason to reach for this component.

Drop an <iframe> inside and it stretches to the ratio box at every viewport width. Always give the iframe a title so its content has an accessible name. The placeholder below stands in for the embed.

iframe embed (16 : 9)
<AspectRatio ratio="16/9" class="rounded-lg border">
  <iframe
    src="https://www.youtube-nocookie.com/embed/ID"
    title="Conference talk"
    allowfullscreen
  />
</AspectRatio>
{% call aspect_ratio(ratio="16/9", extra_class="rounded-lg border") %}
  <iframe src="https://www.youtube-nocookie.com/embed/ID"
          title="Conference talk" class="size-full" allowfullscreen></iframe>
{% endcall %}
{{template "aspect-ratio" (dict "Ratio" "16/9" "Class" "rounded-lg border"
  "Body" "<iframe src=\"…/embed/ID\" title=\"Conference talk\" class=\"size-full\" allowfullscreen></iframe>")}}
<.aspect_ratio ratio="16/9" class="rounded-lg border">
  <iframe src="https://www.youtube-nocookie.com/embed/ID"
          title="Conference talk" class="size-full" allowfullscreen></iframe>
</.aspect_ratio>
<div class="w-full max-w-md">
  <div data-slot="aspect-ratio" data-ratio="16/9" class="relative block w-full overflow-hidden aspect-video rounded-lg border">
    <div class="size-full object-cover flex size-full items-center justify-center bg-muted text-sm text-muted-foreground" data-slot="aspect-ratio-content">iframe embed (16 : 9)</div>
  </div>
</div>

API Reference

<AspectRatio>

PropTypeDefaultDescription
rationumber|string"16/9"
Width-to-height ratio. A number (1.778) or a "w/h" string ("16/9", "4/3", "1/1"). Maps to Tailwind's aspect-* utility; "1/1" -> aspect-square, "16/9" -> aspect-video, anything else -> aspect-[w/h].MDNaspect-ratio
fit"cover"|"contain""cover"
How a replaced child (img/video) fills the box. cover scales up and crops; contain letterboxes so nothing is cut off. Has no effect on iframe/embed children.MDNobject-fit
children*Child
The locked element: an <img>, <video>, <iframe>, <embed>, or any block. A single valid element child is cloned so size-full + object-fit classes land directly on it.
classstring
Extra Tailwind classes appended to the root element.
hx-*any
Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference

* required