Container-responsive images via ResizeObserver.
srcset and sizes select images based on the viewport.
But what if your image is inside a sidebar, a grid cell, or a resizable panel?
Slimmage watches the container and requests the right image resolution for the
actual layout — with width-stepping for CDN cache efficiency, ratchet behavior to avoid
redundant downloads, and aspect-ratio hints for zero layout shift.
Install, import, done. One function call per image.
npm install slimmage-core
import { createSlimmage } from 'slimmage-core'; const cleanup = createSlimmage(img, { src: 'https://cdn.example.com/photo.jpg?width=160&quality=85', container: '.card', }); // Later: cleanup();
import { useSlimmage } from 'slimmage-react'; function Photo({ url }) { const ref = useSlimmage({ src: url }); return <img ref={ref} />; }
<script> import { SlimmageImg } from 'slimmage-svelte'; </script> <SlimmageImg src={url} />
import 'slimmage-wc'; <slimmage-img src="https://cdn.example.com/photo.jpg ?width=160&quality=85" ></slimmage-img>
The image URL updates based on the container width — not the viewport. Width is snapped to the nearest 160px step for CDN cacheability. Drag the handle to see it in action.
Without an aspect-ratio hint, content below the image shifts when the image loads.
Slimmage sets aspect-ratio on the img element to reserve the correct space before loading.
This content sits below the image.
This content sits below the image.
Click to clear and reload both images. Watch the content position in the left panel.
Each grid cell is an independent container. Change the column count and each image adapts to its own cell width.
Once a larger image is loaded, shrinking the container doesn't trigger a smaller request. The browser's cached image renders at the smaller size for free.
What happens when the container grows past the source image's native resolution?
Set maxWidth to your source image's pixel width to cap requests. Without it, the CDN would
upscale or serve the original at sizes that add bytes without adding detail.
createSlimmage(img, { src: 'https://cdn.example.com/photo.jpg?width=160&quality=85', maxWidth: 800, // source image is 800px wide onBeforeLoad: (info) => { el.classList.toggle('below-res', info.requestedWidth >= 800); }, });
For responsive backgrounds, CSS container queries are the right tool.
They switch images during the layout pass — zero JS delay, no flash of wrong-size content.
Slimmage stays focused on <img> elements where CSS has no solution.
.hero { container-type: inline-size; container-name: hero; } .hero-bg { background-image: url('photo.jpg?width=480'); } @container hero (min-width: 480px) { .hero-bg { background-image: url('photo.jpg?width=800'); } } @container hero (min-width: 800px) { .hero-bg { background-image: url('photo.jpg?width=1200'); } }
Recommendation: CSS container queries for backgrounds, slimmage for <img> elements. Browser support: Chrome 105+, Firefox 110+, Safari 16+ (>95% global coverage).
Three steps, zero configuration beyond the template URL.
A shared ResizeObserver watches the container element's inline size. Not the viewport — the actual container.
Container width × DPR, snapped to the nearest step (default 160px). Width-stepping means fewer unique URLs and better CDN cache hit rates.
img.src is set with optimal RIAPI parameters. The ratchet prevents re-downloading smaller sizes. Format detection picks WebP or AVIF when supported.
Everything slimmage exports, at a glance.
createSlimmage(img, config)createSlimmage(img: HTMLImageElement, config: SlimmageConfig): () => void
Attach slimmage to an <img>. Returns a cleanup function that disconnects all observers.
| Option | Type | Default | Description |
|---|---|---|---|
| src | string | — | Template URL with RIAPI params (required) |
| container | string | Element | parentElement | CSS selector or element to observe |
| widthStep | number | 160 | Width snap step (px) for CDN cache efficiency |
| maxWidth | number | 4096 | Cap on requested pixel width |
| dprAware | boolean | true | Multiply container width by devicePixelRatio |
| maxDpr | number | 3 | Cap on devicePixelRatio |
| quality | number | 85 | Base JPEG quality (1-100) |
| qualityDprStep | number | 10 | Quality reduction per DPR step above 1× |
| preferredFormat | ImageFormat | 'avif' | Preferred format if browser supports it |
| lazy | boolean | true | Defer load until image enters viewport |
| lazyMargin | string | '200px' | IntersectionObserver rootMargin |
| aspectRatio | number | — | width/height ratio for CLS prevention |
| fetchPriority | 'high' | 'low' | 'auto' | 'auto' | Fetch priority hint |
| onBeforeLoad | (info) => info | void | — | Called before each URL update; can modify or cancel |
| onLoad | (info) => void | — | Called after image loads |
ImageLoadInfo| Field | Type | Description | |
|---|---|---|---|
| containerWidth | number | Container's CSS pixel width | |
| dpr | number | Effective devicePixelRatio used | |
| requestedWidth | number | Stepped pixel width being requested | |
| quality | number | JPEG quality being requested | |
| format | ImageFormat | Image format being requested | |
| url | string | Computed URL to load | |
| previousUrl | string | null | Previous URL (null on first load) | |
| previousWidth | number | Previous requested width (0 on first load) | |
| Function | Signature | ||
|---|---|---|---|
| stepWidth | (idealWidth: number, step: number, maxWidth: number) => number | ||
| effectiveDpr | (dprAware: boolean, maxDpr: number) => number | ||
| computeQuality | (baseQuality: number, dpr: number, qualityDprStep: number) => number | ||
| buildUrl | (templateUrl: string, requestedWidth: number, quality: number, format: ImageFormat) => string | ||