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.json2. Use it
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
/** @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
{% 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
{# 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
{{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
{{/*
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
<.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
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
<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
<!--
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.
<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.
<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>Further reading
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.
<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>
| Prop | Type | Default | Description |
|---|---|---|---|
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> |
sources | ResponsiveImageSource[] | — | 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 / sizes | string | — | 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 / height | number | — | 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> |
imgClass | string | — | 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> |
class | string | — | Extra Tailwind classes appended to the root element. |
hx-* | any | — | Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference |
* required