Responsive Image API

The responsive image system has two layers: URL helpers (lib/images.ts) that map a base image path to the width-stepped, format-swapped variants generated by scripts/prepare_images.ts, and the <responsive-image> component (src/components/responsive-image.ree) that composes those helpers into a complete <picture> element. Both read widths from config/responsive_images.ts.

URL Helpers (lib/images.ts)

Four functions map a base image URL to its generated variant paths. They're exported from lib/images.ts and available in every .ree template through the default helpers object (avif, webp, jpeg, srcset). The width breakpoints come from config/responsive_images.ts.

srcset(url, format)

Builds a srcset descriptor string for one format across all configured widths.

function srcset(url: string, format: "webp" | "jpeg" | "avif"): string;
ParameterTypeDescription
urlstringBase image URL, must contain /responsive/
format"webp" | "jpeg" | "avif"Output format for every srcset candidate

Returns: A comma-separated srcset value string ready for a <source> or <img> element.

Example:

srcset("/images/responsive/hero.png", "webp")
// → "/images/responsive/300/hero.webp 300w, /images/responsive/500/hero.webp 500w, /images/responsive/800/hero.webp 800w, /images/responsive/1440/hero.webp 1440w"

The returned string is used directly in the <responsive-image> component and can be used in hand-built <picture> elements.

webp(url, size?)

Resolves a single image URL to its WebP variant at an optional width.

function webp(url: string, size?: number): string;
ParameterTypeDescription
urlstringBase image URL, must contain /responsive/
sizenumberOptional width breakpoint; omitting returns the full-size variant

Extension mapping: .png.webp, .jpg/.jpeg.webp.

Examples:

webp("/images/responsive/hero.png", 800)   // → "/images/responsive/800/hero.webp"
webp("/images/responsive/hero.png")         // → "/images/responsive/hero.webp"
webp("/images/responsive/photo.jpg", 500)   // → "/images/responsive/500/photo.webp"

jpeg(url, size?)

Resolves a single image URL to its JPEG variant at an optional width.

function jpeg(url: string, size?: number): string;
ParameterTypeDescription
urlstringBase image URL, must contain /responsive/
sizenumberOptional width breakpoint; omitting returns the full-size variant

Extension mapping: Unlike webp(), jpeg() keeps the source extension intact — hero.png stays hero.png (but contains JPEG bytes). Only .webp sources get renamed to .jpg:

jpeg("/images/responsive/hero.png", 300)   // → "/images/responsive/300/hero.png"  (JPEG bytes)
jpeg("/images/responsive/hero.jpg", 300)   // → "/images/responsive/300/hero.jpg"
jpeg("/images/responsive/icon.webp", 300)  // → "/images/responsive/300/icon.jpg"

This is the JPEG extension quirk — see Responsive Images Config for why.

avif(url, size?)

Resolves a single image URL to its AVIF variant at an optional width.

function avif(url: string, size?: number): string;

Same extension mapping as webp(): .png.avif, .jpg/.jpeg.avif.

Note: The current build pipeline (scripts/prepare_images.ts) does not emit .avif files (Bun.Image uses the OS-native codec, which lacks an AV1 encoder on many platforms). The <responsive-image> component keeps the AVIF <source> commented out to match. If you adopt an AVIF-capable encoder, re-enable both the component line and the generation in prepare_images.ts together.

The <responsive-image> Component

src/components/responsive-image.ree renders a complete <picture> element with WebP and JPEG <source> children, driven by the width breakpoints in config/responsive_images.ts.

Props

All props can be passed either as template props or HTML attributes (attribute values take precedence).

Prop / AttributeTypeDefaultDescription
srcstringRequired. No default.Base image URL, e.g. "/images/responsive/hero.png"
altstring""Alt text for the <img>
classstring"w-full"Extra classes for the <picture> element
image_classstring"w-full object-cover"Extra classes for the <img> element
sizesstring"(max-width: 640px) 300px, (max-width: 1680px) 800px, 1440px"The <img> and <source> sizes attribute
loadingstring"lazy""lazy" or "eager"

Any class and image_class values are merged with the defaults using tw_merge() (tailwind-merge), so you can pass class="rounded-lg" and get "w-full rounded-lg" without manually repeating the base classes.

HTML Output

<picture class="w-full">
  <!-- AVIF source commented out — re-enable with AVIF generation -->
  <source type="image/webp"
          srcset="/images/responsive/300/hero.webp 300w, /images/responsive/500/hero.webp 500w, /images/responsive/800/hero.webp 800w, /images/responsive/1440/hero.webp 1440w"
          sizes="(max-width: 640px) 300px, (max-width: 1680px) 800px, 1440px" />
  <source srcset="/images/responsive/300/hero.png 300w, /images/responsive/500/hero.png 500w, /images/responsive/800/hero.png 800w, /images/responsive/1440/hero.png 1440w"
          sizes="(max-width: 640px) 300px, (max-width: 1680px) 800px, 1440px" />
  <img src="/images/responsive/hero.png" alt="" loading="lazy" class="w-full object-cover" />
</picture>

The browser negotiates the best variant from the srcset candidates based on the sizes hint. WebP-supporting browsers (~97%) use the first <source>; all others fall back to the JPEG <source> and ultimately the <img src>.

Usage

<responsive-image
  src="/images/responsive/hero.png"
  alt="Our team"
  loading="eager"
  sizes="(max-width: 1024px) 100vw, 640px"
  image_class="h-full w-full object-cover"
></responsive-image>

Or as a self-closing tag with attributes:

<div class="image-container">
  {{ props.src = "/images/responsive/hero.png"; props.alt = "Our team"; }}
  <responsive-image
    loading="eager"
    sizes="(max-width: 1024px) 100vw, 640px"
    image_class="h-full w-full object-cover"
  ></responsive-image>
</div>

When using attributes, props.src (or a parent-scope variable) is resolved by the component's template block — attribute values like src="/images/responsive/hero.png" are used directly as fallback.

AVIF Source

The AVIF <source> is commented out in the component:

<!-- AVIF disabled: the Bun.Image build pipeline (scripts/prepare_images.ts) does
     not emit .avif files, and <picture> does not fall back when a chosen <source>
     404s. Re-enable this line together with AVIF generation in the script.
<source type="image/avif" srcset="..." sizes="..." />
-->

<picture> selects the first <source> whose type the browser supports and does not fall back when that source 404s. A live AVIF source with no matching files would show broken images in AVIF-capable browsers. Re-enable both this line and AVIF generation in scripts/prepare_images.ts together.

Template Helpers

All four URL helpers are available in every .ree template without imports. They're registered in the default helpers object (lib/template_helpers.tscreate_default_helpers()):

HelperFunctionUse case
{~ srcset(url, 'webp') }srcset()Build a srcset for a <source> or <img>
{~ webp(url, 800) }webp()Resolve a single image to its WebP variant
{~ jpeg(url, 300) }jpeg()Resolve a single image to its JPEG variant
{~ avif(url, 800) }avif()Resolve a single image to its AVIF variant

Use {~ ... } (unescaped output) — the returned strings are URL paths, not HTML.

Building a Custom <picture>

The <responsive-image> component covers most cases, but you can build a custom <picture> with the helpers directly:

<picture>
  <source type="image/webp"
          srcset="{~ srcset('/images/responsive/hero.png', 'webp') }"
          sizes="(max-width: 768px) 100vw, 50vw" />
  <img src="{= jpeg('/images/responsive/hero.png') }"
       alt="Hero"
       loading="lazy"
       class="w-full" />
</picture>

How It All Connects

config/responsive_images.ts              ← width breakpoints + quality
         │
         ├── scripts/prepare_images.ts   ← generates width-stepped WebP + JPEG files
         │
         ├── lib/images.ts               ← srcset() / webp() / jpeg() / avif() helpers
         │       │
         │       ├── exposed as template helpers (create_default_helpers)
         │       │
         │       └── src/components/responsive-image.ree ← <picture> with WebP + JPEG <source>
         │
         └── lib/template_helpers.ts     ← registers avif, webp, jpeg, srcset in every template

Change responsive_widths in config, re-run bun prepare:images, and both the generated files and the component's srcset update together — no template edits needed.

Relationship to Other Docs

PageCovers
Responsive Images ConfigThe config file: widths, quality, screenshot handling, CLI overrides
This pageThe template API: URL helpers and the <responsive-image> component
Responsive Images RecipeHow-to: adding an image, using the component, SSG wiring