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

@awhitty/snapper

v0.4.1

Published

Visual snapshot testing tool for React component libraries

Downloads

1,124

Readme

snapper

Visual snapshot testing for React component libraries.

Captures components as PNGs via Playwright, compares them against baselines, and gives you a workbench for reviewing and approving visual diffs. Integrates with Vite (or any Vite-compatible dev server — including Vite running alongside a Next.js app).

Quick start

1. Install

pnpm add -D @awhitty/snapper
# or
bun add -D @awhitty/snapper

2. vite.config.ts (in your host project)

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { snapperPlugin } from "@awhitty/snapper/plugin";

export default defineConfig({
  plugins: [
    react(),
    snapperPlugin({
      // Host CSS for workbench preview + /__snapper/frame (chrome uses Shadow DOM)
      styles: "./app/globals.css",
    }),
  ],
});

3. snapper.config.ts

import { defineConfig } from "@awhitty/snapper";

export default defineConfig({
  snapshots: "components/**/*.snaps.tsx",
  snapshotsDir: "./snapshots",
});

4. Add scripts to package.json

{
  "scripts": {
    "snapper": "snapper",
    "snapper:ci": "snapper --ci"
  }
}

5. Write a .snaps.tsx next to a component

// components/Button/Button.snaps.tsx
import { meta, snap } from "@awhitty/snapper/protocol";
import { Button } from "./Button";

export const Meta = meta("Components/Button");
export const Base = snap("Base", () => <Button>Click me</Button>);
export const Disabled = snap("Disabled", () => <Button disabled>—</Button>);

6. Run it

pnpm snapper          # watch mode + live workbench at http://localhost:5173/__snapper
pnpm snapper:ci       # single run, exits non-zero if anything changed
pnpm snapper approve  # approve pending diffs in snapshots/review/

Architecture

┌──────────────────────────────────────────────────────────────────┐
│  /  (one document: host CSS in <head> for real <html>/<body>)    │
│  ┌────────────────────┐  ┌────────────────────────────────────┐  │
│  │ sidebar (Shadow    │  │  preview panel (light DOM)         │  │
│  │  DOM): preflight + │  │  host CSS + <div data-snapshot-root>│  │
│  │  workbench.css     │  │  <Component />                       │  │
│  └────────────────────┘  └────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘
  • Chrome shell (/ HTML) — host snapshot CSS loads in <head> so the preview matches production. The sidebar lives in a Shadow DOM with snapper's preflight + workbench.css, so host globals don't restyle the tree/search UI.
  • Frame page (/__snapper/frame) — same snapshot renderer as the preview, but its own document with host CSS only (no snapper chrome). Used by Playwright captures and useful for debugging (__RENDER_SNAPSHOT__ on window).

Interactive workbench selection updates the inline preview via React props; Playwright still navigates to /__snapper/frame and calls window.__RENDER_SNAPSHOT__(id) there.

Authoring

meta(name, options?)

Declares a group. name is the sidebar label (supports / for hierarchy).

export const Meta = meta("Components/Form", {
  wrap: ({ children }) => <FormProvider>{children}</FormProvider>,
});
  • wrap — optional file-scoped wrapper applied around every snapshot in the file (after the global wrap).

snap(name, component, options?)

Declares a single snapshot.

export const Base = snap("Base", () => <Button>Base</Button>, {
  position: "center",      // "top-left" | "top-center" | "top-right" | "center" | "bottom-*"
  padding: 16,             // number, string, or true for default
  background: "#0b0b0b",   // color, className, true (subtle), or false
});

Global providers (snapper.config.ts wrap)

// snapper.config.ts
export default defineConfig({
  snapshots: "components/**/*.snaps.tsx",
  wrap: "./snapper.providers.tsx",
});
// snapper.providers.tsx
import { Theme } from "@radix-ui/themes";

export default function Wrap({ children }: { children: React.ReactNode }) {
  return <Theme>{children}</Theme>;
}

Composition order when a snap renders: global wrapfile wrap (from meta) → <Component />.

Keep .snaps.tsx files clean

Only export meta and snap calls. Non-component runtime exports force @vitejs/plugin-react to bail on Fast Refresh, which means every edit triggers a full page reload. Move helpers to a sibling file.

Next.js hosts

Next.js uses webpack/turbopack at runtime, but snapper needs Vite. Your Next.js project gets a vite.config.ts alongside next.config.js — snapper's Vite server runs in parallel with Next's and is used only by snapper.

Add the compat layer for common next/* imports:

import { snapperPlugin } from "@awhitty/snapper/plugin";
import { nextCompat } from "@awhitty/snapper/compat/next";
import { tsconfigPaths } from "@awhitty/snapper/compat/tsconfig-paths";

export default defineConfig({
  plugins: [
    tsconfigPaths(),      // workspace aliases, path mappings
    react(),
    nextCompat(),         // stubs next/image, next/link, next/font, etc.
    snapperPlugin({ styles: "./app/globals.css" }),
  ],
});

Covered out of the box:

  • next/image<img> (supports src, alt, fill, width, height)
  • next/link<a>
  • next/headnull
  • next/scriptnull
  • next/dynamicReact.lazy + Suspense
  • next/navigation → mock useRouter, usePathname, useSearchParams, etc.
  • next/font/local → honors variable, returns empty className
  • next/font/google → configurable via snapper.next.ts (see below)

Fonts

Real next/font/google downloads and injects fonts at build time. The compat layer needs to know which fonts your app uses so it can return the expected className for each call site.

// snapper.next.ts (auto-discovered at project root)
import type { NextCompatSetup } from "@awhitty/snapper/compat/next";

export default {
  fonts: {
    Inter: { className: "font-inter", variable: "--font-inter" },
    GeistMono: { className: "font-geist-mono", variable: "--font-geist-mono" },
  },
} satisfies NextCompatSetup;

Load the actual @font-face rules through your host CSS (via the styles option). The compat layer returns the className; your CSS delivers the font.

CLI

snapper                                  # single run (one-shot)
snapper dev                              # watch mode with Ink TUI + workbench
snapper --log                            # single run, append-only plain log lines
snapper dev --log                        # watch mode, append-only plain log lines
snapper --ci                             # single run, text output, exits 0/1/2
snapper --ci --format json               # single run, JSON output for CI
snapper approve                          # usage / list pending
snapper approve --all                    # approve every PNG in review/
snapper approve <id>                     # approve a specific id
snapper skill install [--global|--project]     # Claude Code skill file

Exit codes (in --ci mode):

  • 0 — all captures match baselines
  • 1 — changes detected (new or diff-from-baseline)
  • 2 — workbench failed to boot (real failure — check the emitted error)

Environment

  • SNAPPER_LOG=1 — append-only plain log mode (one snapper … line per event: ready, syncing, captured, summary, watching, changed, approved). Same mode as snapper --log / --plain.
  • SNAPPER_NO_TUI=1 — force the simple line-based logger (with ANSI accents) even on a TTY. Prefer --log or SNAPPER_LOG=1 for agents.
  • Plain log mode also turns on automatically when any of these is set: CLAUDE_CODE, CURSOR_AGENT, AI_AGENT, or NO_COLOR. Non-TTY shells still use the simple logger unless plain mode applies as above.

Claude Code skill

snapper ships a skill file for agents working in projects that use it. Install once globally:

snapper skill install             # ~/.claude/skills/snapper/SKILL.md
snapper skill install --project   # .claude/skills/snapper/SKILL.md
snapper skill status
snapper skill uninstall

The skill tells Claude what snapper does, when to reach for it, and how to diagnose a failing capture.

License

MIT