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;
| Parameter | Type | Description |
|---|---|---|
url | string | Base 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;
| Parameter | Type | Description |
|---|---|---|
url | string | Base image URL, must contain /responsive/ |
size | number | Optional 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;
| Parameter | Type | Description |
|---|---|---|
url | string | Base image URL, must contain /responsive/ |
size | number | Optional 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 / Attribute | Type | Default | Description |
|---|---|---|---|
src | string | Required. No default. | Base image URL, e.g. "/images/responsive/hero.png" |
alt | string | "" | Alt text for the <img> |
class | string | "w-full" | Extra classes for the <picture> element |
image_class | string | "w-full object-cover" | Extra classes for the <img> element |
sizes | string | "(max-width: 640px) 300px, (max-width: 1680px) 800px, 1440px" | The <img> and <source> sizes attribute |
loading | string | "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.ts → create_default_helpers()):
| Helper | Function | Use 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
| Page | Covers |
|---|---|
| Responsive Images Config | The config file: widths, quality, screenshot handling, CLI overrides |
| This page | The template API: URL helpers and the <responsive-image> component |
| Responsive Images Recipe | How-to: adding an image, using the component, SSG wiring |