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

screensmith

v0.1.1

Published

CLI and API for capturing website screenshots with Playwright

Readme

ScreenSmith

A command-line tool for capturing website screenshots.

Give ScreenSmith a URL and get a PNG or JPEG back — viewport or full page, at any size. It waits for pages to finish loading, scrolls through lazy content, handles embedded iframes, and supports dark mode, print media, timezone, and motion preferences so screenshots match what users actually see. Captures can be saved locally or uploaded directly to S3 or Cloudflare R2.

npm version License: MIT Runtime: Bun

Features

  • Viewport and full-page capture
  • Element capture via CSS selector
  • Rectangular clip regions within the viewport
  • PNG and JPEG output with configurable JPEG quality
  • Configurable viewport size, device scale factor, and timeouts
  • Page load strategies (load, domcontentloaded, networkidle)
  • Wait for a CSS selector or extra render delay
  • Lazy-load scrolling for long pages
  • Dark mode and reduced-motion emulation
  • Screen vs print CSS media type
  • Transparent PNG backgrounds
  • IANA timezone emulation
  • Custom user agent
  • Hide elements by CSS selector before capture
  • Embedded iframe handling
  • Direct upload to S3 or Cloudflare R2 (no local file required)
  • Machine-readable JSON output via --json for scripting and automation
  • Config file, environment variables, and .env support
  • TypeScript programmatic API

Quick start

Requirements: Bun v1.3+ (the CLI runtime) and Chromium for Playwright.

Install the screensmith CLI globally:

# npm
npm install -g screensmith

# pnpm
pnpm add -g screensmith

# bun
bun add -g screensmith

# yarn
yarn global add screensmith

Install Chromium (one-time setup):

bunx playwright install chromium

Capture your first screenshot:

screensmith capture https://example.com -o ./shot.png

On success, the absolute output path is printed to stdout. Use --json for a structured JSON response (see Output formats).

CLI

Global options (before any subcommand):

| Flag | Description | |------|-------------| | --config <path> | Path to screensmith.config.json | | --json | Emit machine-readable JSON to stdout (and stderr for errors) |

# Save to a file
screensmith capture https://example.com -o ./shot.png

# Save to a directory (generates screensmith-{ulid}.png)
screensmith capture https://example.com -o ./shots/

# Full-page JPEG at a custom viewport
screensmith capture https://example.com -o ./shot.jpeg \
  --format jpeg --quality 90 --width 1920 --height 1080 --full-page

# Retina / high-DPI capture
screensmith capture https://example.com -o ./shot.png --device-scale-factor 2

# Scroll lazy-loaded content on long pages
screensmith capture https://example.com -o ./shot.png --full-page --scroll

# Capture a specific element
screensmith capture https://example.com -o ./card.png --selector ".hero-card"

# Clip a region of the viewport
screensmith capture https://example.com -o ./region.png \
  --clip-x 100 --clip-y 200 --clip-width 800 --clip-height 600

# Wait for dynamic content
screensmith capture https://example.com -o ./shot.png \
  --wait-for "#content" --wait-ms 500 --wait-until networkidle

# Emulate print styles, timezone, and reduced motion
screensmith capture https://example.com -o ./print.png \
  --media-type print --timezone America/New_York --reduced-motion

# Remove cookie banners automatically (EasyList Cookie + uBlock annoyances-cookies)
screensmith capture https://example.com -o ./shot.png --remove-cookie-banners

# Hide specific overlays (chat widgets, etc.) alongside automatic cookie removal
screensmith capture https://example.com -o ./shot.png \
  --remove-cookie-banners --hide-selectors ".chat-widget"

# Hide elements manually when you know the selectors
screensmith capture https://example.com -o ./shot.png \
  --hide-selectors "#cookie-banner" --hide-selectors ".chat-widget"

# Transparent PNG
screensmith capture https://example.com -o ./logo.png --omit-background

# Upload directly to S3 (no -o flag)
screensmith --config ./screensmith.config.json capture https://example.com

# Machine-readable JSON output (for scripts and CI)
screensmith --json capture https://example.com -o ./shot.png

Output formats

By default, ScreenSmith uses text output:

  • Local capture: prints the absolute file path to stdout
  • Object storage capture (S3, R2, or GCS): prints a JSON object with capture metadata (id, path, url, etc.)
  • Errors: CODE: message on stderr
  • config show: pretty-printed JSON

With --json, all success and error responses use compact JSON:

# Success (local capture)
screensmith --json capture https://example.com -o ./shot.png
# {"path":"/abs/path/shot.png","format":"png","bytes":12345,"width":1512,"height":982,"durationMs":842}

# Error
screensmith --json capture https://example.com
# stderr: {"error":{"code":"INVALID_ARGS","message":"Missing required option: -o, --output <path>"}}

--json can appear anywhere in the argument list (e.g. screensmith capture https://example.com -o ./shot.png --json).

Options

| Flag | Default | Description | |------|---------|-------------| | -o, --output | required* | Output file path or directory | | --format | png | png or jpeg | | --quality | 80 | JPEG quality, 0–100 (JPEG only) | | --width | 1512 | Viewport width | | --height | 982 | Viewport height | | --device-scale-factor | 1 | Device pixel ratio for Retina/high-DPI captures | | --full-page | off | Capture the full scrollable page | | --clip-x | — | Clip region left offset in pixels | | --clip-y | — | Clip region top offset in pixels | | --clip-width | — | Clip region width in pixels | | --clip-height | — | Clip region height in pixels | | --scroll | off | Scroll to trigger lazy-loaded content (use with --full-page) | | --dark | off | Emulate prefers-color-scheme: dark | | --media-type | screen | CSS media type: screen or print | | --omit-background | off | Transparent background (PNG only) | | --reduced-motion | off | Emulate prefers-reduced-motion: reduce | | --wait-until | load | load, domcontentloaded, or networkidle | | --wait-for | — | CSS selector to wait for | | --selector | — | Capture a specific element instead of the viewport | | --wait-ms | 0 | Extra delay after the page is ready (ms, minimum 10) | | --timeout | 30000 | Global capture timeout (ms) | | --force | off | Overwrite an existing output file | | --timezone | — | IANA time zone (e.g. Europe/London, America/New_York) | | --user-agent | — | Override the browser user agent | | --hide-selectors | — | Hide matching elements before capture (repeatable) | | --remove-cookie-banners | off | Block cookie consent scripts and hide common cookie banners | | --quiet | off | Suppress non-error stderr | | --verbose | off | Debug output to stderr |

* -o is not required when persistence is configured for S3, R2, or GCS. In that case, text mode prints a JSON object with capture metadata (id, path, url, etc.) instead of a local file path. With --json, all captures return the full result object regardless of persistence backend.

Clip flags (--clip-x, --clip-y, --clip-width, --clip-height) must be used together.

--remove-cookie-banners uses bundled community filter lists (EasyList Cookie and uBlock annoyances-cookies) stored under assets/cookie-filters/ to block consent-management scripts and hide common banner elements. Update them with bun run fetch-cookie-filters. Coverage is not perfect: exotic consent flows or custom banners may still appear, and some sites dim the page behind a consent overlay. Use --hide-selectors for site-specific fixes.

Run screensmith capture --help for the full reference.

Exit codes

| Code | Meaning | |------|---------| | 0 | Success | | 1 | Internal error | | 2 | Invalid arguments, URL, or output conflict | | 3 | Timeout | | 4 | Navigation failed | | 5 | Selector not found | | 6 | Browser error |

Programmatic API

Use the capture engine directly from TypeScript:

import { capture } from "screensmith/core";

const result = await capture({
  url: "https://example.com",
  output: { kind: "file", path: "./shot.png" },
  format: "jpeg",
  quality: 90,
  viewport: { width: 1512, height: 982 },
  deviceScaleFactor: 2,
  fullPage: true,
  scrollToLoad: true,
  darkMode: true,
  mediaType: "screen",
  omitBackground: false,
  reducedMotion: false,
  hideSelectors: ["#cookie-banner"],
  removeCookieBanners: true,
  selector: ".hero-card",
  clip: { x: 0, y: 0, width: 800, height: 600 },
  timezoneId: "America/New_York",
  wait: { until: "networkidle", selector: "#content", delayMs: 500 },
  timeoutMs: 30_000,
});

console.log(result.path, result.bytes, result.durationMs);

For object storage persistence (S3, R2, or GCS), use in-memory output — the provider uploads the bytes and returns object metadata:

const result = await capture({
  url: "https://example.com",
  output: { kind: "memory" },
  format: "png",
  viewport: { width: 1280, height: 720 },
  fullPage: false,
  scrollToLoad: false,
  darkMode: false,
  deviceScaleFactor: 1,
  hideSelectors: [],
  removeCookieBanners: false,
  mediaType: "screen",
  omitBackground: false,
  reducedMotion: false,
  wait: { until: "load", delayMs: 0 },
  timeoutMs: 30_000,
});

console.log(result.id, result.url, result.path);

Exports are also available under screensmith/core/capture.

HTTP API

Start the API server:

screensmith serve
screensmith --config ./screensmith.config.json serve
bun run serve

The server listens on api.host and api.port from config (default 127.0.0.1:3100).

Endpoints

| Method | Path | Description | |--------|------|-------------| | POST | /capture | Capture a URL (JSON body) | | POST | /capture/async | Queue a capture job, returns 202 with a job id | | GET | /capture/:jobId | Poll an async capture job's status and result | | GET | /version | Server version | | GET | /metrics | Capture metrics snapshot (when enabled) | | GET | /files/:filename | Fetch a locally stored capture |

Capture request

Send a JSON body with the same fields as the programmatic API. Do not send an output field — the server chooses where to store the result.

curl -s -X POST http://127.0.0.1:3100/capture \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","fullPage":true}'

With local persistence (persistence.provider: "none"), the response includes a public URL:

{
  "path": "screensmith-01J....png",
  "url": "http://127.0.0.1:3100/files/screensmith-01J....png",
  "format": "png",
  "bytes": 12345,
  "width": 1512,
  "height": 982,
  "durationMs": 842
}

With object storage configured (S3, R2, or GCS), captures upload directly and the response includes object storage metadata (id, url, path, etc.).

Errors return JSON matching --json CLI output:

{"error":{"code":"VALIDATION_ERROR","message":"URL is required."}}

Async capture jobs

POST /capture/async accepts the same body as /capture but returns immediately with 202 Accepted, a Location header pointing at the job, and a body like {"jobId":"01J...","status":"pending"}. Poll GET /capture/:jobId until status is completed (with a result) or failed (with an error).

Jobs are held in memory only: a server restart loses all pending, running, and completed jobs, so callers must be prepared to resubmit. Finished jobs are kept for api.jobTtlMs (default 1 hour) and the store holds at most api.maxStoredJobs (default 1000); when full, the oldest finished job is evicted to make room, and new submissions get a 503 only if the store is full of unfinished jobs. Set api.asyncJobsEnabled: false to disable the endpoints.

Internal deployment

This API is designed as an internal capture worker, not a public-facing service.

  • Bind to a private address (default 127.0.0.1) and call it from your gateway or orchestrator
  • Authentication and rate limiting belong in the calling application, not in ScreenSmith
  • SSRF still applies: callers can pass URLs that reach internal networks, so validate URLs upstream if needed
  • Set api.publicUrl when a reverse proxy exposes /files/... on a different host than the bind address
  • api.maxConcurrentCaptures (default 5) limits parallel Playwright captures to protect the host from burst load
  • Requests beyond the concurrency limit wait in a bounded queue: api.maxQueueDepth (default 50, 0 disables queueing and restores fast 503s) caps its size and api.maxQueueWaitMs (default 10000) caps how long a request waits for a slot before a 503
  • api.rateLimit (maxRequests per windowMs, default 60 requests per 60s sliding window) sheds excess request volume before it reaches the capture queue

Configuration

ScreenSmith resolves config from (highest priority first):

  1. Environment variables (SCREENSMITH_*)
  2. Config file
  3. Built-in defaults

Config file search paths (first match wins):

  • Path from --config or SCREENSMITH_CONFIG
  • ./screensmith.config.json
  • ./screensmith.json
  • ./.screensmith/config.json
  • ~/.config/screensmith/config.json

Or pass an explicit path:

screensmith --config ./screensmith.config.json capture https://example.com -o ./shot.png

Example config (local files)

{
  "browser": {
    "provider": "playwright",
    "headless": true
  },
  "persistence": {
    "provider": "none"
  },
  "api": {
    "provider": "bun",
    "host": "127.0.0.1",
    "port": 3100,
    "storageDir": "./.screensmith/captures",
    "publicUrl": "http://127.0.0.1:3100"
  }
}

Example config (S3)

{
  "browser": {
    "provider": "playwright",
    "headless": true
  },
  "persistence": {
    "provider": "s3",
    "bucket": "my-screenshots",
    "region": "eu-west-1",
    "prefix": "captures",
    "url": "https://cdn.example.com",
    "accessKeyId": "AKIA...",
    "secretAccessKey": "..."
  },
  "api": {
    "provider": "bun",
    "host": "127.0.0.1",
    "port": 3100,
    "storageDir": "./.screensmith/captures",
    "publicUrl": "http://127.0.0.1:3100"
  }
}

Example config (Cloudflare R2)

{
  "browser": {
    "provider": "playwright",
    "headless": true
  },
  "persistence": {
    "provider": "r2",
    "bucket": "my-screenshots",
    "endpoint": "https://<account-id>.r2.cloudflarestorage.com",
    "prefix": "captures/",
    "url": "https://screenshots.example.com",
    "accessKeyId": "...",
    "secretAccessKey": "..."
  },
  "api": {
    "provider": "bun",
    "host": "127.0.0.1",
    "port": 3100,
    "storageDir": "./.screensmith/captures",
    "publicUrl": "http://127.0.0.1:3100"
  }
}

Example config (Google Cloud Storage)

GCS uses the S3-compatible interoperability API. Create HMAC keys in the GCS console and use them as accessKeyId / secretAccessKey.

{
  "browser": {
    "provider": "playwright",
    "headless": true
  },
  "persistence": {
    "provider": "gcs",
    "bucket": "my-screenshots",
    "prefix": "captures/",
    "url": "https://storage.googleapis.com/my-screenshots",
    "accessKeyId": "GOOG...",
    "secretAccessKey": "..."
  },
  "api": {
    "provider": "bun",
    "host": "127.0.0.1",
    "port": 3100,
    "storageDir": "./.screensmith/captures",
    "publicUrl": "http://127.0.0.1:3100"
  }
}

The GCS provider defaults to endpoint https://storage.googleapis.com. Override endpoint or set region if needed. When using url, objects must be publicly readable or configured with an appropriate ACL / bucket policy.

With object storage configured, captures are uploaded directly from memory — no local -o path is needed. Object keys are generated automatically under the configured prefix (default: captures/). Set url to a public CDN base URL to receive fully qualified URLs in the result.

See screensmith.config.example.json for a starting point. Bun loads .env automatically.

Environment variables

| Variable | Description | |----------|-------------| | SCREENSMITH_CONFIG | Path to a config file | | SCREENSMITH_BROWSER_PROVIDER | Browser provider (playwright) | | SCREENSMITH_BROWSER_HEADLESS | Run headless (true / false) | | SCREENSMITH_BROWSER_EXECUTABLE_PATH | Custom Chromium executable path | | SCREENSMITH_PERSISTENCE_PROVIDER | none, s3, r2, or gcs | | SCREENSMITH_PERSISTENCE_URL | Public base URL for stored captures | | SCREENSMITH_PERSISTENCE_BUCKET | Object storage bucket name | | SCREENSMITH_PERSISTENCE_REGION | AWS region (S3); optional for GCS | | SCREENSMITH_PERSISTENCE_ENDPOINT | Custom endpoint (required for R2; optional for GCS) | | SCREENSMITH_PERSISTENCE_PREFIX | Key prefix (default: captures/) | | SCREENSMITH_PERSISTENCE_ACCESS_KEY_ID | Access key ID | | SCREENSMITH_PERSISTENCE_SECRET_ACCESS_KEY | Secret access key | | SCREENSMITH_PERSISTENCE_SESSION_TOKEN | Optional session token | | SCREENSMITH_PERSISTENCE_ACL | S3 ACL for uploaded objects | | SCREENSMITH_API_PROVIDER | API server provider (bun) | | SCREENSMITH_API_HOST | API server host | | SCREENSMITH_API_PORT | API server port | | SCREENSMITH_API_STORAGE_DIR | Local directory for API captures (default: ./.screensmith/captures) | | SCREENSMITH_API_PUBLIC_URL | Public base URL for capture links (for reverse proxies) | | SCREENSMITH_API_MAX_CONCURRENT_CAPTURES | Max parallel capture requests (default: 5) | | SCREENSMITH_API_MAX_QUEUE_DEPTH | Max capture requests queued at the concurrency limit (default: 50; 0 disables queueing) | | SCREENSMITH_API_MAX_QUEUE_WAIT_MS | Max time a queued request waits for a capture slot (default: 10000) | | SCREENSMITH_API_RATE_LIMIT_MAX_REQUESTS | Sliding-window rate limit: max requests per window (default: 60) | | SCREENSMITH_API_RATE_LIMIT_WINDOW_MS | Sliding-window rate limit window in milliseconds (default: 60000) | | SCREENSMITH_API_METRICS_ENABLED | Enable GET /metrics endpoint (default: true) | | SCREENSMITH_API_ASYNC_JOBS_ENABLED | Enable POST /capture/async job endpoints (default: true) | | SCREENSMITH_API_JOB_TTL_MS | How long finished async jobs stay queryable (default: 3600000) | | SCREENSMITH_API_MAX_STORED_JOBS | Max async jobs held in memory (default: 1000) |

Inspect the resolved config (credentials are redacted):

screensmith config show
screensmith --json config show

Development

Clone and run from source:

git clone https://github.com/ahmedash95/screensmith.git
cd screensmith
bun install
bunx playwright install chromium
bun run dev -- capture https://example.com -o ./shot.png
bun run serve
bun test

Project layout:

src/
  cli/          # CLI entrypoint and commands
  core/         # Capture engine, config, providers, HTTP API
tests/          # Unit and integration tests

Contributing

Contributions are welcome. Open an issue to discuss larger changes, or send a pull request for bug fixes and improvements.

  1. Fork the repo and create a branch from main
  2. Make your changes and add tests where it makes sense
  3. Run bun test
  4. Open a pull request with a clear description

License

MIT © ScreenSmith contributors