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

fast-html-to-image

v0.0.3

Published

Fast, high-fidelity DOM node to image capture

Readme

fast-html-to-image

version

Screenshot any DOM node, in the browser, with high fidelity.

Give it an element and get back a pixel-accurate PNG of exactly what the user sees: computed styles, web fonts, images, pseudo-elements, shadow DOM, form state, canvas frames, and scroll positions included. Zero runtime dependencies.

Install

npm install fast-html-to-image

Usage

import { captureNode } from "fast-html-to-image";

const result = await captureNode(document.querySelector("#card"));

result.width; // CSS px of the capture rect
result.height;

await result.toPngDataUrl(); // "data:image/png;base64,..."
await result.toBlob(); // PNG Blob (e.g. for clipboard or upload)
await result.toJpegBlob(0.9); // lossy JPEG, encodes 3 to 8x faster than PNG
await result.toCanvas(); // HTMLCanvasElement
await result.toSvgDataUrl(); // the intermediate SVG, before rasterization

Browser (IIFE)

<script src="https://unpkg.com/fast-html-to-image/dist/index.global.js"></script>
<script>
  FastHtmlToImage.captureNode(document.querySelector("#card"));
</script>

Region capture

captureRegion screenshots a pixel rectangle of the page in viewport coordinates, so it fits drag-to-select capture. The rect is a plain Rect ({ x, y, width, height }), which is structurally what getBoundingClientRect() returns, so you can pass a DOM rect straight through.

import { captureRegion } from "fast-html-to-image";

const result = await captureRegion({ x: 120, y: 80, width: 400, height: 300 });

// or capture the live box of any element
const result = await captureRegion(element.getBoundingClientRect());

captureRegion takes every shared option plus two region-specific ones: root (the element the region is measured against and captured from, defaulting to document.documentElement) and cullMarginPx (how far outside the region offscreen elements are kept before being pruned for speed).

Prewarming

The first capture on a page pays one-time costs (style probes, stylesheet scan, font fetches). prewarm() pays them ahead of time; prewarm(element) also fills the caches for that element so a later capture of it resolves from cache. Safe to call multiple times.

import { prewarm } from "fast-html-to-image";

prewarm(); // at app idle, before the first capture
prewarm(element); // on hover, before capturing that element on click

Options

captureNode and captureRegion share a common CaptureOptions base and each adds its own target-specific options: CaptureNodeOptions adds clip and bleed; CaptureRegionOptions adds root and cullMarginPx.

Shared (CaptureOptions)

| Option | Type | Default | Description | | ---------------------- | ------------------------------------- | ------------------------- | -------------------------------------------------------------------------- | | scale | number | 1 | Multiplier applied to the output raster size | | pixelRatio | number | window.devicePixelRatio | Device pixel ratio for the output canvas | | backgroundColor | string | inherited | Fill painted behind the capture; pass "transparent" to keep transparency | | embedFonts | boolean | true | Inline used @font-face fonts as data URLs | | filterNode | (element: Element) => boolean | undefined | Return false to exclude an element and its subtree | | resolveIframeContent | (iframe) => Promise<string \| null> | undefined | Supply an image data URL for a cross-origin iframe | | timeoutMs | number | 8000 | Per-resource fetch timeout for images, fonts, and url()s | | abortSignal | AbortSignal | undefined | Cancels an in-flight capture between pipeline stages |

captureNode only (CaptureNodeOptions)

| Option | Type | Default | Description | | ------- | ------------------ | ----------- | ---------------------------------------------------------------------------------- | | clip | Rect | undefined | Crop to a rect in the element's border-box coordinate space (CSS px) | | bleed | number \| "auto" | 0 | Extra padding (CSS px) around the border box so shadows and filters aren't clipped |

captureRegion only (CaptureRegionOptions)

| Option | Type | Default | Description | | -------------- | --------- | --------------------------- | --------------------------------------------------------------------------------- | | root | Element | document.documentElement | Element the region is measured against and captured from | | cullMarginPx | number | see source | How far outside the region offscreen elements are kept before being pruned |

How it works

captureNode runs a read, clone, inline, rasterize pipeline:

  1. Snapshot getComputedStyle for every element in the composed tree, diff each against per-tag defaults, and collapse identical style sets into shared generated classes
  2. Deep-clone the element with live state baked in: shadow DOM flattened, form values reflected, canvas and video frames captured, scroll positions reproduced
  3. Fetch every external resource (images, backgrounds, used @font-face sources) and inline it as a data URL
  4. Serialize the clone into <svg><foreignObject>, which browsers render with the real layout engine
  5. Decode the SVG once and draw it to a canvas shared by every output format

Fidelity

Every change runs a Playwright harness that captures 406 fixtures with captureNode and with the browser's native screenshot, then diffs the two with pixelmatch on Chromium, WebKit, and Firefox. On Chromium, 398 fixtures hold a 0.005 diff budget and most score exactly 0; the exceptions are form controls and transformed roots, where native rendering legitimately differs inside foreignObject.

npm run build && npm test

Benchmarks

npm run bench compares captureNode against snapdom, modern-screenshot, html-to-image, html2canvas, and dom-to-image-more on ten fixtures. The harness also fidelity-scores every capture, so read the speed columns together with the fidelity column: a fast capture that renders the wrong pixels is not comparable to an accurate one.

Cross-origin iframes

Cross-origin iframe content is unreadable from the parent page. Two escape hatches exist before the gray-placeholder fallback:

  • resolveIframeContent option: supply an image data URL for an iframe yourself
  • enableIframeBridge(): call inside the framed page (it must also load this library); the parent capture then requests a capture over postMessage
// inside the cross-origin iframe's page
import { enableIframeBridge } from "fast-html-to-image";
const disableBridge = enableIframeBridge();

Known limitations

  • Cross-origin iframes without the bridge or hook render as gray placeholders sized to their box
  • backdrop-filter only sees content inside the capture root; content behind the root is not captured

Development

npm install
npx playwright install    # browsers for the fidelity suite
npm run build
npm run test:unit
npm test                  # unit + fidelity suite
npm run check             # lint + format

License

MIT