npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

background-image-cropper

v1.0.1

Published

Optimize CSS background-image downloads by cropping and scaling each layer to exactly the pixels the browser actually paints. Framework-agnostic, zero runtime dependencies, works in every browser.

Readme

background-image-cropper

Optimize CSS background-image downloads by fetching each layer cropped and scaled to exactly the pixels the browser actually paints — nothing more.

▶ Live demo · npm

A CSS background only ever paints a sub-rectangle of its source image (decided by the element's box, background-size, -position, -repeat, -origin, -clip and -attachment). Shipping the full-resolution original wastes bandwidth. This library measures the real painted region from the rendered DOM, then rewrites the background-image URL to point at an image-transform backend (your CDN/proxy) that returns just that region at just the right size — including for devicePixelRatio / retina.

  • Framework-agnostic, zero runtime dependencies.
  • Every browser: ships ESM, CommonJS, and a standalone <script> global; the geometry relies only on getComputedStyle + box metrics.
  • Reactive: recomputes on element resize (ResizeObserver) and DPR changes.
  • Pluggable output: bring your own URL builder, or use the bundled presets.
  • Handles the full matrix: all background-size forms (cover/contain/ auto/lengths/percentages), all background-position forms (keywords, percentages, lengths, calc() edge-offset syntax), every background-repeat mode, all origin/clip boxes, scroll/local/fixed attachment, borders, padding, and multiple comma-separated layers (gradients are left untouched).

Install

npm install background-image-cropper

Quick start

import { cropBackgrounds, weservUrlBuilder } from "background-image-cropper";

// Optimize every .hero background and keep it correct on resize / DPR change.
const cropper = cropBackgrounds(".hero", { urlBuilder: weservUrlBuilder });

// later, if you need to tear it down:
cropper.disconnect();

Plain <script> (no bundler) — exposes a BackgroundImageCropper global:

<script src="https://unpkg.com/background-image-cropper"></script>
<script>
  BackgroundImageCropper.cropBackgrounds(".hero", {
    urlBuilder: BackgroundImageCropper.weservUrlBuilder,
  });
</script>

Providing the source's natural size

To crop precisely (and to resolve cover/contain), the library needs the source image's intrinsic size. It will load the image once to read it, but you can avoid that round-trip with a size hint on the element:

<div class="hero" data-bg-image-width="1600" data-bg-image-height="900"></div>

(The attribute base is configurable via sizeHintAttribute.)

API

cropBackgrounds(target, options?) → BackgroundCropper

Convenience wrapper: constructs a BackgroundCropper, starts optimizing target, and returns it. target is a CSS selector, an Element, or any iterable of elements.

new BackgroundCropper(options?)

| Option | Default | Description | | --- | --- | --- | | urlBuilder | defaultUrlBuilder | Turns a CropPlan into the replacement URL. | | dpr | true | true → track devicePixelRatio; false → force 1; or a fixed number. | | observe | true | Watch each element with ResizeObserver and react to DPR changes. | | sizeHintAttribute | "data-bg-image" | Base name for the -width/-height size hint. | | maxDpr | 3 | Upper bound on the effective DPR. |

Methods: .observe(target), .refresh(), .unobserve(el), .disconnect() (restores originals and tears down all observers/listeners).

URL builders

An UrlBuilder receives a fully-resolved CropPlan and returns a string (or a Promise<string>):

interface CropPlan {
  url: string;                       // the original source URL
  natural: { width; height } | null; // source intrinsic size, if known
  crop: { x; y; width; height } | null; // source-pixel region to take (null = whole image)
  output: { width; height };         // device pixels to deliver it at
  repeat: { x: RepeatMode; y: RepeatMode };
  dpr: number;
}

Bundled builders:

  • defaultUrlBuilder / createProxyUrlBuilder({ base?, quality? }) — targets a path-style proxy with the grammar {base}{W}x{H},fit,cw{CW},ch{CH},q{Q}/{sourceUrl}. Assumes a top-left crop (the grammar has no crop offset).
  • weservUrlBuilder / createWeservUrlBuilder({ base?, quality? }) — targets the free, public wsrv.nl proxy (a.k.a. images.weserv.nl), using &precrop + cx,cy,cw,ch for a source-pixel crop and w,h,fit=fill to resize. Pixel-accurate for every combination and needs no infrastructure of your own.

Custom builder for, say, imgproxy / Cloudinary / imgix:

const cloudinary: UrlBuilder = (plan) => {
  const t = [`w_${plan.output.width}`, `h_${plan.output.height}`, "c_fill"];
  if (plan.crop) {
    t.push(`x_${plan.crop.x}`, `y_${plan.crop.y}`,
           `w_${plan.crop.width}`, `h_${plan.crop.height}`, "c_crop");
  }
  return `https://res.cloudinary.com/demo/image/fetch/${t.join(",")}/${encodeURIComponent(plan.url)}`;
};

How the geometry works

For each layer the library computes, in the element's coordinate space:

  1. the positioning area (from background-origin / -attachment),
  2. the rendered image size (from background-size against that area),
  3. the image position (from background-position),
  4. the clip/visible window (from background-clip),

intersects the placed image with the visible window, and maps the result back to source pixels — giving the crop rectangle and the output size (times a clamped DPR, never upscaled beyond the source). Tiling backgrounds (repeat/round/ space) are never cropped; only the tile is downscaled. local backgrounds are scaled but not cropped, since scrolling can reveal any part of them.

When a genuine sub-region is cropped, the delivered image no longer has the dimensions the original background-size was computed against — so the cropper also sets that layer's background-size, -position and -repeat inline to paint the smaller image exactly over the visible rect (CropPlan.paint). Layers that deliver the whole image (contain, tiling, local, or an image that already fits) keep their original background properties untouched.

Demo

Hosted: https://lsobolew.github.io/background-image-cropper/. Run it locally:

npm install
npm run build
npm run demo   # http://localhost:5173

Each scenario renders the same element twice — an original that loads the full source from dummyimage.com, and an optimized copy rewritten by the library to fetch only the visible region through the wsrv.nl proxy. They render identically while the optimized request downloads far fewer pixels; captions show the live crop and savings. Resize the window to watch crops recompute via ResizeObserver.

Development

npm run typecheck   # tsc --noEmit
npm test            # node --test (pure geometry / CSS / builder units)
npm run build       # tsup → dist/ (ESM + CJS + IIFE global + .d.ts)

License

MIT