shshadcn-htmx

Components

Responsive Image

Art-directed, format-switching image built on native <picture> + <source>. The browser walks the sources by media and type, picks the first match, and falls back to the <img> — all natively, with zero JS.

Installation

1. Install via the shadcn CLI

npx shadcn@latest add http://localhost/r/responsive-image.json

2. Use it

components/ui/responsive-image.tsx
import { ResponsiveImage } from "@/components/ui/responsive-image"

<ResponsiveImage
  src="/img/hero.jpg"
  alt="A surfer at golden hour"
  sources={[
    { srcset: "/img/hero.avif", type: "image/avif" },
    { srcset: "/img/hero.webp", type: "image/webp" },
  ]}
/>
Or copy the source manually
components/ui/responsive-image.tsx
/** @jsxImportSource hono/jsx */
import type { Child } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"

// Responsive Image — shadcn-htmx, htmx v4 + Tailwind v4.
//
// Art-directed / format-switching image. Renders a <picture> with zero or
// more <source> children and exactly one fallback <img>. The browser walks
// the <source> list top-to-bottom, picks the first whose `media` and `type`
// match, and falls back to the <img> when none match (or <picture> isn't
// supported). All selection happens natively — zero JS.
//
// Source of truth:
//   repos/mdn/files/en-us/web/html/reference/elements/picture/index.md
//     - <source media> for art direction + (prefers-color-scheme) theme swaps
//     - <source type> for format negotiation (image/avif, image/webp)
//     - the <img> is required: it sizes the box AND is the universal fallback
//   repos/mdn/files/en-us/web/html/reference/elements/source/index.md
//     - inside <picture>, `srcset` is required and `src` is NOT allowed
//     - <source> is a void element (no closing tag)
//     - `sizes` only takes effect with width (`w`) descriptors, not density (`x`)
//
// Accessibility:
//   - <picture> and <source> have NO corresponding ARIA role; the accessible
//     name + role come entirely from the child <img>'s `alt`. So `alt` is
//     required on the component (empty string only for decorative images).
//   - Per MDN, object-fit / object-position go on the <img>, never <picture>.

// One <source>: a candidate the browser may pick before the fallback <img>.
export type ResponsiveImageSource = {
  // Comma-separated candidate list. Inside <picture> this is REQUIRED and
  // `src` is not allowed. e.g. "hero.avif" or "small.jpg 480w, large.jpg 1200w".
  srcset: string
  // MIME type for format negotiation; unsupported types are skipped without
  // a network hit. e.g. "image/avif", "image/webp".
  type?: string
  // Media condition for art direction / theming, e.g.
  // "(min-width: 800px)" or "(prefers-color-scheme: dark)".
  media?: string
  // Only meaningful with width (`w`) descriptors in srcset.
  sizes?: string
  // Intrinsic dimensions (integers, no units) to reserve layout space.
  width?: number
  height?: number
}

const rootBase = "block overflow-hidden rounded-lg border bg-muted"

// The <img> fills the box; object-fit/position live here per MDN, not on <picture>.
const imgBase = "block size-full object-cover"

type ResponsiveImageProps = {
  // The fallback image — also the element that sizes the box and carries the
  // accessible name. Required by the platform.
  src: string
  // Required: empty string ("") only for purely decorative images.
  alt: string
  // The art-directed / format-switching candidates, in priority order.
  sources?: ResponsiveImageSource[]
  // Intrinsic dimensions on the fallback <img>; reserve space to avoid CLS.
  width?: number
  height?: number
  // Native lazy loading + async decode for off-screen hero/gallery images.
  loading?: "lazy" | "eager"
  decoding?: "sync" | "async" | "auto"
  fetchpriority?: "high" | "low" | "auto"
  // Density/width candidates for the fallback <img> itself (Retina without
  // explicit media queries — see MDN note on <img srcset>).
  srcset?: string
  sizes?: string
  class?: ClassValue
  // Extra classes applied to the inner <img> (e.g. object-contain, aspect-*).
  imgClass?: ClassValue
  id?: string
  // Render extra <source> elements by hand (advanced) instead of `sources`.
  children?: Child
  // Pass-through for htmx / data-* / aria-* on the <picture> root.
  [key: `data-${string}`]: any
  [key: `hx-${string}`]: any
  [key: `aria-${string}`]: any
}

export function ResponsiveImage(props: ResponsiveImageProps) {
  const {
    src,
    alt,
    sources = [],
    width,
    height,
    loading,
    decoding,
    fetchpriority,
    srcset,
    sizes,
    class: className,
    imgClass,
    id,
    children,
    ...rest
  } = props

  return (
    <picture
      id={id}
      data-slot="responsive-image"
      class={cn(rootBase, className)}
      {...rest}
    >
      {sources.map((s) => (
        // <source> is a void element; inside <picture> srcset is required and
        // src is not allowed. The first matching source wins.
        <source
          srcset={s.srcset}
          type={s.type}
          media={s.media}
          sizes={s.sizes}
          width={s.width}
          height={s.height}
        />
      ))}
      {children}
      {/* The fallback <img> sizes the box and provides the accessible name.
          object-fit / object-position go HERE per MDN, never on <picture>. */}
      <img
        src={src}
        alt={alt}
        srcset={srcset}
        sizes={sizes}
        width={width}
        height={height}
        loading={loading}
        decoding={decoding}
        fetchpriority={fetchpriority}
        data-slot="responsive-image-img"
        class={cn(imgBase, imgClass)}
      />
    </picture>
  )
}

1. Save the file

Copy responsive-image.html into templates/components/.

2. Use it

templates/components/responsive-image.html
{% from "components/responsive-image.html" import responsive_image %}

{{ responsive_image(
     src="/img/hero.jpg", alt="A surfer at golden hour",
     sources=[
       {"srcset": "/img/hero.avif", "type": "image/avif"},
       {"srcset": "/img/hero.webp", "type": "image/webp"},
     ]) }}
View source
templates/components/responsive-image.html
{# Responsive Image macro — shadcn-htmx, htmx v4 + Tailwind v4.
   Mirrors registry/ui/responsive-image.tsx. Native <picture> + <source>s + <img>.

   `sources` is a list of dicts: { srcset, type, media, sizes, width, height }.
   The browser picks the first matching <source>; the <img> is the fallback
   and carries the accessible name via `alt` (empty "" only for decorative).

   Usage:
     {% from "components/responsive-image.html" import responsive_image %}
     {{ responsive_image(
          src="/img/hero.jpg", alt="A surfer at golden hour",
          sources=[
            {"srcset": "/img/hero.avif", "type": "image/avif"},
            {"srcset": "/img/hero.webp", "type": "image/webp"},
          ]) }} #}

{% macro responsive_image(src, alt, sources=[], width=none, height=none,
                          loading=none, decoding=none, fetchpriority=none,
                          srcset=none, sizes=none, extra_class="", img_class="",
                          id=none, attrs={}) %}
<picture data-slot="responsive-image"{% if id %} id="{{ id }}"{% endif %}
         class="block overflow-hidden rounded-lg border bg-muted {{ extra_class }}"
         {%- for k, v in attrs.items() %} {{ k|replace('_','-') }}="{{ v }}"{% endfor %}>
  {%- for s in sources %}
  <source srcset="{{ s.srcset }}"
          {%- if s.type %} type="{{ s.type }}"{% endif %}
          {%- if s.media %} media="{{ s.media }}"{% endif %}
          {%- if s.sizes %} sizes="{{ s.sizes }}"{% endif %}
          {%- if s.width %} width="{{ s.width }}"{% endif %}
          {%- if s.height %} height="{{ s.height }}"{% endif %}>
  {%- endfor %}
  <img src="{{ src }}" alt="{{ alt }}" data-slot="responsive-image-img"
       {%- if srcset %} srcset="{{ srcset }}"{% endif %}
       {%- if sizes %} sizes="{{ sizes }}"{% endif %}
       {%- if width %} width="{{ width }}"{% endif %}
       {%- if height %} height="{{ height }}"{% endif %}
       {%- if loading %} loading="{{ loading }}"{% endif %}
       {%- if decoding %} decoding="{{ decoding }}"{% endif %}
       {%- if fetchpriority %} fetchpriority="{{ fetchpriority }}"{% endif %}
       class="block size-full object-cover {{ img_class }}">
</picture>
{% endmacro %}

1. Save the file

Add responsive-image.tmpl alongside your templates.

2. Use it

components/responsive-image.tmpl
{{template "responsive-image" (dict
    "Src" "/img/hero.jpg" "Alt" "A surfer at golden hour"
    "Sources" (list
        (dict "Srcset" "/img/hero.avif" "Type" "image/avif")
        (dict "Srcset" "/img/hero.webp" "Type" "image/webp")))}}
View source
components/responsive-image.tmpl
{{/*
  Responsive Image template — shadcn-htmx, htmx v4 + Tailwind v4.

  Mirrors registry/ui/responsive-image.tsx. Native <picture> + <source>s + <img>.
  The browser picks the first matching <source>; the <img> is the universal
  fallback and carries the accessible name via Alt (empty "" only decorative).

      type SourceArgs struct {
          Srcset, Type, Media, Sizes string
          Width, Height              int
      }
      type ResponsiveImageArgs struct {
          Src, Alt, Srcset, Sizes              string
          Loading, Decoding, Fetchpriority     string
          Width, Height                        int
          Sources                              []SourceArgs
          ExtraClass, ImgClass, ID             string
      }

  Usage:
      {{template "responsive-image" (dict
          "Src" "/img/hero.jpg" "Alt" "A surfer at golden hour"
          "Sources" (list
              (dict "Srcset" "/img/hero.avif" "Type" "image/avif")
              (dict "Srcset" "/img/hero.webp" "Type" "image/webp")))}}
*/}}

{{define "responsive-image"}}
<picture data-slot="responsive-image"{{if .ID}} id="{{.ID}}"{{end}}
         class="block overflow-hidden rounded-lg border bg-muted {{or .ExtraClass ""}}"{{with .Attrs}} {{. | htmlSafe}}{{end}}>
  {{- range .Sources}}
  <source srcset="{{.Srcset}}"{{if .Type}} type="{{.Type}}"{{end}}{{if .Media}} media="{{.Media}}"{{end}}{{if .Sizes}} sizes="{{.Sizes}}"{{end}}{{if .Width}} width="{{.Width}}"{{end}}{{if .Height}} height="{{.Height}}"{{end}}>
  {{- end}}
  <img src="{{.Src}}" alt="{{.Alt}}" data-slot="responsive-image-img"{{if .Srcset}} srcset="{{.Srcset}}"{{end}}{{if .Sizes}} sizes="{{.Sizes}}"{{end}}{{if .Width}} width="{{.Width}}"{{end}}{{if .Height}} height="{{.Height}}"{{end}}{{if .Loading}} loading="{{.Loading}}"{{end}}{{if .Decoding}} decoding="{{.Decoding}}"{{end}}{{if .Fetchpriority}} fetchpriority="{{.Fetchpriority}}"{{end}}
       class="block size-full object-cover {{or .ImgClass ""}}">
</picture>
{{end}}

1. Save the file

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

2. Use it

lib/my_app_web/components/responsive_image.ex
<.responsive_image src={~p"/img/hero.jpg"} alt="A surfer at golden hour">
  <:source srcset={~p"/img/hero.avif"} type="image/avif" />
  <:source srcset={~p"/img/hero.webp"} type="image/webp" />
</.responsive_image>
View source
lib/my_app_web/components/responsive_image.ex
defmodule ShadcnHtmx.Components.ResponsiveImage do
  @moduledoc """
  Responsive Image — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.

  Mirrors registry/ui/responsive-image.tsx. A native `<picture>` with zero or
  more `<source>` children and exactly one fallback `<img>`. The browser walks
  the source list top-to-bottom, picks the first whose `media`/`type` match,
  and falls back to the `<img>` when none match. Zero JS.

  Source of truth:
    repos/mdn/files/en-us/web/html/reference/elements/picture/index.md
    repos/mdn/files/en-us/web/html/reference/elements/source/index.md

  Accessibility: `<picture>`/`<source>` carry no ARIA role; the accessible name
  comes from the fallback `<img>`'s `alt` (empty "" only for decorative images).

  ## Examples

      <.responsive_image src={~p"/img/hero.jpg"} alt="A surfer at golden hour">
        <:source srcset={~p"/img/hero.avif"} type="image/avif" />
        <:source srcset={~p"/img/hero.webp"} type="image/webp" />
      </.responsive_image>
  """

  use Phoenix.Component

  attr :src, :string, required: true
  attr :alt, :string, required: true
  attr :srcset, :string, default: nil
  attr :sizes, :string, default: nil
  attr :width, :integer, default: nil
  attr :height, :integer, default: nil
  attr :loading, :string, default: nil, values: ~w(lazy eager) ++ [nil]
  attr :decoding, :string, default: nil, values: ~w(sync async auto) ++ [nil]
  attr :fetchpriority, :string, default: nil, values: ~w(high low auto) ++ [nil]
  attr :class, :string, default: nil
  attr :img_class, :string, default: nil
  attr :id, :string, default: nil
  attr :rest, :global

  slot :source do
    attr :srcset, :string, required: true
    attr :type, :string
    attr :media, :string
    attr :sizes, :string
    attr :width, :integer
    attr :height, :integer
  end

  def responsive_image(assigns) do
    ~H"""
    <picture
      id={@id}
      data-slot="responsive-image"
      class={["block overflow-hidden rounded-lg border bg-muted", @class]}
      {@rest}
    >
      <source
        :for={s <- @source}
        srcset={s.srcset}
        type={s[:type]}
        media={s[:media]}
        sizes={s[:sizes]}
        width={s[:width]}
        height={s[:height]}
      />
      <img
        src={@src}
        alt={@alt}
        srcset={@srcset}
        sizes={@sizes}
        width={@width}
        height={@height}
        loading={@loading}
        decoding={@decoding}
        fetchpriority={@fetchpriority}
        data-slot="responsive-image-img"
        class={["block size-full object-cover", @img_class]}
      />
    </picture>
    """
  end
end

1. Save the file

Paste the markup; relies only on theme tokens.

2. Use it

snippets/responsive-image.html
<picture data-slot="responsive-image"
  class="block overflow-hidden rounded-lg border bg-muted">
  <source srcset="/img/hero.avif" type="image/avif">
  <source srcset="/img/hero.webp" type="image/webp">
  <img src="/img/hero.jpg" alt="A surfer at golden hour"
       data-slot="responsive-image-img" class="block size-full object-cover">
</picture>
View source
snippets/responsive-image.html
<!--
  shadcn-htmx — raw HTML responsive-image snippet.

  Mirrors registry/ui/responsive-image.tsx. Native <picture> + <source>s +
  fallback <img>. The browser picks the first matching <source> by media/type
  and falls back to the <img>, which sizes the box and carries the accessible
  name via `alt` (empty "" only for decorative images). Tailwind tokens only.
-->

<!-- Format negotiation: AVIF → WebP → JPEG fallback -->
<picture data-slot="responsive-image"
  class="block overflow-hidden rounded-lg border bg-muted">
  <source srcset="/img/hero.avif" type="image/avif">
  <source srcset="/img/hero.webp" type="image/webp">
  <img src="/img/hero.jpg" alt="A surfer at golden hour"
       data-slot="responsive-image-img" class="block size-full object-cover">
</picture>

<!-- Theme swap: dark vs light asset via prefers-color-scheme -->
<picture data-slot="responsive-image"
  class="block overflow-hidden rounded-lg border bg-muted">
  <source srcset="/img/diagram-dark.png" media="(prefers-color-scheme: dark)">
  <source srcset="/img/diagram-light.png" media="(prefers-color-scheme: light)">
  <img src="/img/diagram-light.png" alt="System architecture diagram"
       data-slot="responsive-image-img" class="block size-full object-contain">
</picture>

<!-- Art direction: wide crop on large screens, square on small -->
<picture data-slot="responsive-image"
  class="block overflow-hidden rounded-lg border bg-muted">
  <source srcset="/img/banner-wide.jpg" media="(min-width: 800px)" width="1200" height="400">
  <img src="/img/banner-square.jpg" alt="Conference keynote stage"
       width="600" height="600" loading="lazy" decoding="async"
       data-slot="responsive-image-img" class="block size-full object-cover">
</picture>

Examples

Format negotiation — AVIF → WebP → JPEG

Offer modern formats first; the browser skips any type it can't decode and lands on the <img> fallback.

Each <source> declares a type; per MDN the browser compares it against the formats it can display and skips unsupported ones without a network request. The trailing <img> is mandatory — it sizes the box and is the universal fallback.

Surfer at golden hour
<ResponsiveImage
  src="/img/hero.jpg"
  alt="Surfer at golden hour"
  sources={[
    { srcset: "/img/hero.avif", type: "image/avif" },
    { srcset: "/img/hero.webp", type: "image/webp" },
  ]}
/>
{{ responsive_image(
     src="/img/hero.jpg", alt="Surfer at golden hour",
     sources=[
       {"srcset": "/img/hero.avif", "type": "image/avif"},
       {"srcset": "/img/hero.webp", "type": "image/webp"},
     ]) }}
{{template "responsive-image" (dict
    "Src" "/img/hero.jpg" "Alt" "Surfer at golden hour"
    "Sources" (list
        (dict "Srcset" "/img/hero.avif" "Type" "image/avif")
        (dict "Srcset" "/img/hero.webp" "Type" "image/webp")))}}
<.responsive_image src={~p"/img/hero.jpg"} alt="Surfer at golden hour">
  <:source srcset={~p"/img/hero.avif"} type="image/avif" />
  <:source srcset={~p"/img/hero.webp"} type="image/webp" />
</.responsive_image>
<picture data-slot="responsive-image" class="block overflow-hidden rounded-lg border bg-muted mx-auto max-w-md">
  <source srcset="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22600%22%20height%3D%22338%22%20viewBox%3D%220%200%20600%20338%22%3E%3Crect%20width%3D%22600%22%20height%3D%22338%22%20fill%3D%22%231e293b%22%2F%3E%3Ctext%20x%3D%22300%22%20y%3D%22169%22%20font-family%3D%22sans-serif%22%20font-size%3D%2230%22%20fill%3D%22%23ffffff%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%3EWide%20600x338%3C%2Ftext%3E%3C%2Fsvg%3E" type="image/svg+xml"/>
  <img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22600%22%20height%3D%22338%22%20viewBox%3D%220%200%20600%20338%22%3E%3Crect%20width%3D%22600%22%20height%3D%22338%22%20fill%3D%22%237c2d12%22%2F%3E%3Ctext%20x%3D%22300%22%20y%3D%22169%22%20font-family%3D%22sans-serif%22%20font-size%3D%2230%22%20fill%3D%22%23ffffff%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%3EJPEG%20fallback%3C%2Ftext%3E%3C%2Fsvg%3E" alt="Surfer at golden hour" data-slot="responsive-image-img" class="block size-full object-cover"/>
</picture>

Light / dark swap — prefers-color-scheme

Serve a different asset per OS theme. The media query is evaluated natively by the <picture>; no JS, no theme class.

Change your OS / browser colour scheme to see the asset swap. This responds to the platform (prefers-color-scheme) media feature — the real web-standard mechanism the <picture> element supports — not a userland theme toggle. Ideal for diagrams or screenshots that need a matching background.

System architecture diagram
<ResponsiveImage
  src="/img/diagram-light.png"
  alt="System architecture diagram"
  imgClass="object-contain"
  sources={[
    { srcset: "/img/diagram-dark.png",  media: "(prefers-color-scheme: dark)" },
    { srcset: "/img/diagram-light.png", media: "(prefers-color-scheme: light)" },
  ]}
/>
{{ responsive_image(
     src="/img/diagram-light.png", alt="System architecture diagram",
     img_class="object-contain",
     sources=[
       {"srcset": "/img/diagram-dark.png",  "media": "(prefers-color-scheme: dark)"},
       {"srcset": "/img/diagram-light.png", "media": "(prefers-color-scheme: light)"},
     ]) }}
{{template "responsive-image" (dict
    "Src" "/img/diagram-light.png" "Alt" "System architecture diagram"
    "ImgClass" "object-contain"
    "Sources" (list
        (dict "Srcset" "/img/diagram-dark.png"  "Media" "(prefers-color-scheme: dark)")
        (dict "Srcset" "/img/diagram-light.png" "Media" "(prefers-color-scheme: light)")))}}
<.responsive_image src={~p"/img/diagram-light.png"} alt="System architecture diagram" img_class="object-contain">
  <:source srcset={~p"/img/diagram-dark.png"}  media="(prefers-color-scheme: dark)" />
  <:source srcset={~p"/img/diagram-light.png"} media="(prefers-color-scheme: light)" />
</.responsive_image>
<picture data-slot="responsive-image" class="block overflow-hidden rounded-lg border bg-muted mx-auto max-w-md">
  <source srcset="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22600%22%20height%3D%22338%22%20viewBox%3D%220%200%20600%20338%22%3E%3Crect%20width%3D%22600%22%20height%3D%22338%22%20fill%3D%22%23020617%22%2F%3E%3Ctext%20x%3D%22300%22%20y%3D%22169%22%20font-family%3D%22sans-serif%22%20font-size%3D%2230%22%20fill%3D%22%23ffffff%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%3EDark%20asset%3C%2Ftext%3E%3C%2Fsvg%3E" media="(prefers-color-scheme: dark)"/>
  <source srcset="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22600%22%20height%3D%22338%22%20viewBox%3D%220%200%20600%20338%22%3E%3Crect%20width%3D%22600%22%20height%3D%22338%22%20fill%3D%22%23e2e8f0%22%2F%3E%3Ctext%20x%3D%22300%22%20y%3D%22169%22%20font-family%3D%22sans-serif%22%20font-size%3D%2230%22%20fill%3D%22%23ffffff%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%3ELight%20asset%3C%2Ftext%3E%3C%2Fsvg%3E" media="(prefers-color-scheme: light)"/>
  <img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22600%22%20height%3D%22338%22%20viewBox%3D%220%200%20600%20338%22%3E%3Crect%20width%3D%22600%22%20height%3D%22338%22%20fill%3D%22%23e2e8f0%22%2F%3E%3Ctext%20x%3D%22300%22%20y%3D%22169%22%20font-family%3D%22sans-serif%22%20font-size%3D%2230%22%20fill%3D%22%23ffffff%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%3ELight%20asset%3C%2Ftext%3E%3C%2Fsvg%3E" alt="System architecture diagram" data-slot="responsive-image-img" class="block size-full object-cover object-contain"/>
</picture>

Art direction — crop per viewport + lazy

A wide crop on large screens, a square crop on small ones, plus loading="lazy" and decoding="async" on the fallback.

Resize the window past 800px to switch crops. Art direction is the canonical <picture> use case — a different composition, not just a different size. Set width/height to reserve layout space and avoid CLS.

Conference keynote stage
<ResponsiveImage
  src="/img/banner-square.jpg"
  alt="Conference keynote stage"
  width={600} height={600}
  loading="lazy" decoding="async"
  sources={[
    { srcset: "/img/banner-wide.jpg", media: "(min-width: 800px)", width: 1200, height: 400 },
  ]}
/>
{{ responsive_image(
     src="/img/banner-square.jpg", alt="Conference keynote stage",
     width=600, height=600, loading="lazy", decoding="async",
     sources=[
       {"srcset": "/img/banner-wide.jpg", "media": "(min-width: 800px)", "width": 1200, "height": 400},
     ]) }}
{{template "responsive-image" (dict
    "Src" "/img/banner-square.jpg" "Alt" "Conference keynote stage"
    "Width" 600 "Height" 600 "Loading" "lazy" "Decoding" "async"
    "Sources" (list
        (dict "Srcset" "/img/banner-wide.jpg" "Media" "(min-width: 800px)" "Width" 1200 "Height" 400)))}}
<.responsive_image src={~p"/img/banner-square.jpg"} alt="Conference keynote stage"
  width={600} height={600} loading="lazy" decoding="async">
  <:source srcset={~p"/img/banner-wide.jpg"} media="(min-width: 800px)" width={1200} height={400} />
</.responsive_image>
<picture data-slot="responsive-image" class="block overflow-hidden rounded-lg border bg-muted mx-auto max-w-md">
  <source srcset="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22600%22%20height%3D%22338%22%20viewBox%3D%220%200%20600%20338%22%3E%3Crect%20width%3D%22600%22%20height%3D%22338%22%20fill%3D%22%231e293b%22%2F%3E%3Ctext%20x%3D%22300%22%20y%3D%22169%22%20font-family%3D%22sans-serif%22%20font-size%3D%2230%22%20fill%3D%22%23ffffff%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%3EWide%20600x338%3C%2Ftext%3E%3C%2Fsvg%3E" media="(min-width: 800px)" width="600" height="338"/>
  <img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22400%22%20height%3D%22400%22%20viewBox%3D%220%200%20400%20400%22%3E%3Crect%20width%3D%22400%22%20height%3D%22400%22%20fill%3D%22%230f766e%22%2F%3E%3Ctext%20x%3D%22200%22%20y%3D%22200%22%20font-family%3D%22sans-serif%22%20font-size%3D%2230%22%20fill%3D%22%23ffffff%22%20text-anchor%3D%22middle%22%20dominant-baseline%3D%22middle%22%3ESquare%20400x400%3C%2Ftext%3E%3C%2Fsvg%3E" alt="Conference keynote stage" width="400" height="400" loading="lazy" decoding="async" data-slot="responsive-image-img" class="block size-full object-cover"/>
</picture>

API Reference

<ResponsiveImage>

PropTypeDefaultDescription
src*string
URL of the fallback <img>. Also the element that sizes the box and carries the accessible name. Required by the platform — the browser shows it when no <source> matches.MDN<img src>
alt*string
Alternative text on the fallback <img>. <picture>/<source> have no ARIA role, so this is the only accessible name. Use an empty string only for purely decorative images.MDN<img alt>
sourcesResponsiveImageSource[]
Candidate <source> elements in priority order, each { srcset, type?, media?, sizes?, width?, height? }. The browser picks the first whose media + type match. Inside <picture>, srcset is required and src is not allowed.MDN<source> in <picture>
srcset / sizesstring
Density or width candidates on the fallback <img> itself (Retina without explicit media queries). sizes only takes effect with width (w) descriptors, not density (x).MDN<img srcset>
width / heightnumber
Intrinsic dimensions on the fallback <img> (integers, no units). Set them to reserve layout space and avoid layout shift (CLS).MDN<img width/height>
loading"lazy"|"eager"
Native lazy loading. lazy defers off-screen images until they near the viewport.MDN<img loading>
decoding"sync"|"async"|"auto"
Hint for how the browser decodes the image relative to painting other content. async avoids blocking the main thread.MDN<img decoding>
fetchpriority"high"|"low"|"auto"
Relative fetch priority. Use high for the LCP hero image, low for below-the-fold decoration.MDN<img fetchpriority>
imgClassstring
Extra Tailwind classes on the inner <img> (e.g. object-contain, aspect-video). object-fit / object-position belong here, not on <picture>.MDNobject-fit goes on <img>
classstring
Extra Tailwind classes appended to the root element.
hx-*any
Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference

* required