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

@changeinteraction/screen-capture

v0.2.9

Published

Complete React workflow for browser screenshots, screen recordings, annotation, trimming, preview, and form uploads.

Readme

@changeinteraction/screen-capture

npm CI license

A typed React capture workflow that turns screenshots, screen recordings, and non-destructive edits into a standard browser File ready for FormData or a direct object-storage upload.

The library captures and edits locally. It does not upload media, create an account, add telemetry, or choose a backend for the host application.

Features

| Area | Included | | --- | --- | | Screenshot | Browser picker, frame capture, callout annotation, preview, PNG export | | Recording | Staged start, timer, microphone/system audio, stop/cancel, size auto-stop | | Video editor | Preview, split, trim, delete clips, timed callouts, mute, MP4/WebM export | | Attachment field | Picker, paste, drag/drop, MIME/size validation, preview, edit, remove | | Integration | High-level studio, headless hooks, native File, FormData, direct upload | | Distribution | React 18/19 types, scoped compiled CSS, ESM, tests, CI, no runtime npm dependencies |

Requirements

  • React 18.0 or newer.
  • A browser with navigator.mediaDevices.getDisplayMedia for capture.
  • MediaRecorder for recording and WebCodecs-capable media paths for edited video export.
  • HTTPS in production; localhost is accepted for local development.
  • A trusted user action such as a click. Browsers do not allow silent capture.

See browser support and capability detection.

Install

npm install @changeinteraction/screen-capture

Why use 0.2.7 or newer?

Version 0.2.7 includes the responsive screenshot annotator layout introduced in 0.2.6 and fixes a screenshot-preview blob URL race exposed by React Strict Mode. Earlier versions can show an oversized black annotation stage or log blob:... net::ERR_FILE_NOT_FOUND while the browser is still decoding a newly captured image.

For production applications that require reproducible installs, pin the exact version and commit the lockfile:

npm install --save-exact @changeinteraction/[email protected]

^0.2.7 accepts future 0.2.x releases. Those releases follow SemVer, but an exact pin ensures that dependency behavior changes only after your team reviews, tests, and deliberately upgrades it.

Import the stylesheet once near the application root:

import "@changeinteraction/screen-capture/styles.css";

The generated utilities and theme variables are scoped to .asc-root, so the package does not publish generic global .flex, .fixed, or color rules into the host application. CaptureStudio returns a React fragment and does not add that class for you. Put .asc-root on a real DOM ancestor that contains the entire studio output:

<div className="asc-root">
  <CaptureStudio onDone={setAttachment}>{/* product + field */}</CaptureStudio>
</div>

Putting .asc-root only on the form styles the field, but built-in toolbars and editor dialogs rendered as its siblings may remain unstyled. The common ancestor is the safe default.

Complete workflow

CaptureStudio owns permission and editor state. CaptureAttachmentField owns the controlled form value and attachment UI. The application still owns the form, upload, error reporting, and persistence.

Place the studio at the nearest component that owns both the capture target and the feedback panel. For a dashboard, that is usually Home or the dashboard shell—not the panel itself. Hoist the attachment state to the same component, then pass studio, attachment, and setAttachment into the panel. Otherwise captureRootRef can only retain the panel subtree and Element Capture may omit the dashboard the user intended to report.

"use client";

import { useState, type RefObject } from "react";
import {
  CaptureAttachmentField,
  CaptureExportError,
  CaptureStudio,
  finalizeCapturePayload,
  type CapturePayload,
} from "@changeinteraction/screen-capture";

const ALLOWED_TYPES = [
  "image/png",
  "image/jpeg",
  "image/webp",
  "video/webm",
  "video/mp4",
] as const;

export function DashboardWithFeedback() {
  const [attachment, setAttachment] = useState<CapturePayload | null>(null);
  const [progress, setProgress] = useState(0);
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);

  return (
    <div className="asc-root">
      <CaptureStudio
        onDone={setAttachment}
        initialAudioMode="none"
        maxRecordingBytes={7 * 1024 * 1024}
      >
        {(studio) => (
          <>
            <main
              ref={studio.captureRootRef as RefObject<HTMLElement>}
              data-screen-capture-root
            >
              Content retained during supported current-tab Element Capture
            </main>

            <form
              onSubmit={async (event) => {
                event.preventDefault();
                setSubmitting(true);
                setError(null);

                try {
                  const body = new FormData(event.currentTarget);
                  if (attachment) {
                    const file = await finalizeCapturePayload(
                      attachment,
                      setProgress
                    );
                    body.set("attachment", file);
                  }

                  const response = await fetch("/api/feedback", {
                    method: "POST",
                    body,
                  });
                  if (!response.ok) throw new Error("Upload failed");
                } catch (cause) {
                  setError(
                    cause instanceof CaptureExportError
                      ? cause.message
                      : "Could not submit feedback."
                  );
                } finally {
                  setSubmitting(false);
                }
              }}
            >
              <CaptureAttachmentField
                value={attachment}
                onChange={setAttachment}
                onEdit={studio.edit}
                onScreenshot={studio.startCapture}
                onRecord={() => studio.toggleRecording("none")}
                screenshotState={studio.captureState}
                recordingState={studio.recordState}
                captureError={studio.captureError}
                recordingError={studio.recordError}
                allowedTypes={ALLOWED_TYPES}
                maxSizeBytes={8 * 1024 * 1024}
                disabled={submitting}
                onValidationError={setError}
              />

              {progress > 0 && progress < 1 && (
                <p>Exporting {Math.round(progress * 100)}%</p>
              )}
              {error && <p role="alert">{error}</p>}
              <button type="submit" disabled={submitting}>
                {submitting ? "Sending..." : "Send"}
              </button>
            </form>
          </>
        )}
      </CaptureStudio>
    </div>
  );
}

The maintained copy of this example is in examples/complete-workflow.tsx and is compiled in CI. The dashboard-owner/feedback-panel split is implemented in examples/feedback-form-study-case.tsx.

The 8 MB and 7 MB values above are an application policy chosen to match the UI copy. They are not additional package defaults. Keep the recording threshold below the attachment limit so the final encoded chunk has headroom.

Recording button behavior

Use studio.toggleRecording(mode) when one button should follow the full state machine:

idle/error -> open picker -> ready -> begin recording -> recording -> stop

Use startRecording, beginRecording, and stopRecording separately when the application provides dedicated controls for each stage.

Payload and upload contract

The controlled value is deliberately not always a File:

type CapturePayload = File | ImageEditDraft | VideoEditDraft;

An edit draft contains the original sourceFile plus live shapes, trim segments, and mute state. Keeping the draft avoids repeated encoding and allows the user to reopen the editor without losing information.

Only finalize at the upload boundary:

const file = await finalizeCapturePayload(payload, setProgress);
formData.set("attachment", file);

The result is a normal browser File with binary data, a name, MIME type, and size. Do not convert it to base64 for normal uploads. Use multipart for small files or a presigned direct upload for larger recordings.

See the complete form and upload guide.

Safe export failures

Edited video is never silently replaced by the unedited source. If the browser cannot decode or encode the draft, finalizeCapturePayload rejects:

try {
  const file = await finalizeCapturePayload(payload);
  await upload(file);
} catch (error) {
  if (error instanceof CaptureExportError) {
    // Keep `payload` in state. Its source and edits are still available.
    showMessage(error.message);
  }
}

burnDraft is a lower-level API and returns { file, fellBack }; callers using it directly must check fellBack themselves.

Audio

The public audio modes are:

type AudioMode = "none" | "mic" | "system" | "both";

Microphone and system/tab audio are mixed into a stable recording track. System audio is only available when the browser and selected capture surface offer it and the user enables it in the picker. Adding system audio requires a new share; it is intentionally refused after recording has started.

Examples:

onRecord={() => studio.toggleRecording("mic")}
onRecord={() => studio.toggleRecording("system")}
onRecord={() => studio.toggleRecording("both")}

Upload limits

The package defaults are 50 MiB for attachment validation and 48 MiB for recording auto-stop. They are product defaults, not a guarantee that a hosting platform accepts that request size.

For a serverless endpoint with a smaller body limit, configure both sides:

<CaptureStudio maxRecordingBytes={3.5 * 1024 * 1024} onDone={setAttachment}>
  {(studio) => (
    <CaptureAttachmentField
      value={attachment}
      onChange={setAttachment}
      maxSizeBytes={4 * 1024 * 1024}
      onRecord={() => studio.toggleRecording("none")}
    />
  )}
</CaptureStudio>

For production video, prefer direct object-storage upload so the media does not pass through the application function body.

Client limits are only UX. The maintained examples/server-route.ts validates subject, description, optional attachment shape, empty files, MIME allowlist, and the same 8 MB byte limit again at the trusted boundary. Adapt its authentication, signature inspection, storage, quota, and retention policy to your backend.

Public API

| Surface | Purpose | | --- | --- | | CaptureStudio | Complete screenshot/record/editor orchestration | | CaptureAttachmentField | Controlled capture/upload/preview/edit field | | CaptureActionButtons | Presentational capture buttons | | useScreenCapture | Headless screenshot capture | | useScreenRecorder | Headless staged recording and audio control | | finalizeCapturePayload | Convert a raw file or draft into an upload-ready File | | CaptureExportError | Safe edited-video export failure | | ScreenshotAnnotator, RecordingPreview | Standalone editors | | AnnotatedImagePreview, AnnotatedVideoPreview | Live draft previews | | VideoAnnotationLayer, VideoTimeline | Lower-level editor building blocks | | useVideoThumbnailState | Timeline thumbnails with loading/error status and native fallback | | @changeinteraction/screen-capture/editor | Optional editor-focused entry point |

See the complete API reference for every prop, return value, state, type guard, constant, editor type, and timeline helper.

Framework notes

  • Next.js App Router components using this package must be client components.
  • SSR is supported when capture APIs are called only from client interactions.
  • Import the stylesheet once; do not repeatedly import it inside each field.
  • The browser permission picker cannot be styled, bypassed, or pre-approved.
  • Attach data-screen-capture-root to the same element receiving captureRootRef when using current-tab Element Capture.

Production checklist

  • Pin an exact reviewed package version and commit the application lockfile.
  • Keep capture calls directly behind a trusted click.
  • Configure MIME, byte-size, duration, and resolution policies for the product.
  • Revalidate the final file on the trusted server/storage boundary.
  • Authenticate upload initialization and use short-lived presigned URLs.
  • Define retention, deletion, encryption, and access-control policies.
  • Show an explicit processing state while edited video is finalized.
  • Handle CaptureExportError without discarding the draft.
  • Test the supported browser/OS/capture-surface matrix with real devices.
  • Avoid base64 and in-process media storage on serverless functions.
  • Record user consent where required by the product or jurisdiction.

See security and privacy and troubleshooting. A copyable Web Request route is included in examples/server-route.ts.

Documentation

Development

npm ci
npm test
npm pack --dry-run

npm test builds the library, runs unit tests, compiles the maintained examples, and validates local documentation links.

License

MIT