slimmage

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.

Getting Started

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();

React

import { useSlimmage } from 'slimmage-react';

function Photo({ url }) {
  const ref = useSlimmage({ src: url });
  return <img ref={ref} />;
}

Svelte

<script>
  import { SlimmageImg } from 'slimmage-svelte';
</script>

<SlimmageImg src={url} />

Web Component

import 'slimmage-wc';

<slimmage-img
  src="https://cdn.example.com/photo.jpg
       ?width=160&quality=85"
></slimmage-img>

Drag to Resize

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.

DPR-aware (off — treating as 1×)
Container-responsive image
Request Log

Layout Stability

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.

Without aspect-ratio

This content sits below the image.

With aspect-ratio: 16/9

This content sits below the image.

Click to clear and reload both images. Watch the content position in the left panel.

Dynamic Grid

Each grid cell is an independent container. Change the column count and each image adapts to its own cell width.

3

Ratchet: Never Re-download

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.

Container
480px
Loaded
Ratchet demo
Request Log

Below-Resolution: maxWidth

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.

Below source resolution
Below-resolution demo
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);
  },
});

Background Images: CSS Container Queries

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.

480px
.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');
  }
}

CSS container queries

  • Zero JS delay — applied during layout pass
  • Ideal for above-the-fold LCP backgrounds
  • No runtime overhead, no observers
  • Works with any image CDN / RIAPI server

Slimmage (for <img>)

  • Continuous width optimization, not limited to N breakpoints
  • DPR-aware quality reduction saves bytes on retina
  • Format negotiation (AVIF / WebP fallback)
  • Ratchet prevents redundant downloads on shrink

Recommendation: CSS container queries for backgrounds, slimmage for <img> elements. Browser support: Chrome 105+, Firefox 110+, Safari 16+ (>95% global coverage).

How It Works

Three steps, zero configuration beyond the template URL.

1

Observe

A shared ResizeObserver watches the container element's inline size. Not the viewport — the actual container.

2

Compute

Container width × DPR, snapped to the nearest step (default 160px). Width-stepping means fewer unique URLs and better CDN cache hit rates.

3

Load

img.src is set with optimal RIAPI parameters. The ratchet prevents re-downloading smaller sizes. Format detection picks WebP or AVIF when supported.

API Reference

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.

OptionTypeDefaultDescription
srcstringTemplate URL with RIAPI params (required)
containerstring | ElementparentElementCSS selector or element to observe
widthStepnumber160Width snap step (px) for CDN cache efficiency
maxWidthnumber4096Cap on requested pixel width
dprAwarebooleantrueMultiply container width by devicePixelRatio
maxDprnumber3Cap on devicePixelRatio
qualitynumber85Base JPEG quality (1-100)
qualityDprStepnumber10Quality reduction per DPR step above 1×
preferredFormatImageFormat'avif'Preferred format if browser supports it
lazybooleantrueDefer load until image enters viewport
lazyMarginstring'200px'IntersectionObserver rootMargin
aspectRationumberwidth/height ratio for CLS prevention
fetchPriority'high' | 'low' | 'auto''auto'Fetch priority hint
onBeforeLoad(info) => info | voidCalled before each URL update; can modify or cancel
onLoad(info) => voidCalled after image loads

ImageLoadInfo

FieldTypeDescription
containerWidthnumberContainer's CSS pixel width
dprnumberEffective devicePixelRatio used
requestedWidthnumberStepped pixel width being requested
qualitynumberJPEG quality being requested
formatImageFormatImage format being requested
urlstringComputed URL to load
previousUrlstring | nullPrevious URL (null on first load)
previousWidthnumberPrevious requested width (0 on first load)

Utilities

FunctionSignature
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