A <Screenshot> directive for Mintlify MDX, backed by Playwright. Captures a light + dark PNG pair for every directive and rewrites the <Frame> block that follows it. Idempotent. Stays in sync as your app's UI evolves.
<!-- you write this -->
<Screenshot
name="api/playground"
caption="API Playground — testing a Search Listing"
alt="API Playground dialog with request config on the left and JSON response on the right"
width="640px"
/>
<!-- pnpm shots fills in this -->
<Frame caption="API Playground — testing a Search Listing">
<img src="/images/api/playground-light.png" className="dark:hidden block max-w-... mx-auto" style={{ maxWidth: "640px" }} alt="..." />
<img src="/images/api/playground-dark.png" className="hidden dark:block max-w-... mx-auto" style={{ maxWidth: "640px" }} alt="..." />
</Frame>Maintaining light + dark screenshots by hand is tedious enough that most docs skip dark mode entirely. This kit makes them a one-liner and keeps them honest: when the UI changes, you re-run pnpm shots --force and the PNGs catch up — no manual editing of <Frame> blocks, no drifting alt text, no out-of-sync captions.
<Screenshot>directive — plain MDX, no React component to installpnpm shotsCLI — finds directives, runs the matching Playwright tests, writes the Frame blocks- Playwright fixture —
shot(),theme,waitForReady(), plus anappInithook for app-specific setup - Two-theme runner — every test runs once per
colorScheme, producing the light/dark pair - Auth flow —
pnpm shots:loginopens a headful browser, capturesstorageState, reuses it on every later run
-
Copy the kit into your Mintlify docs repo — these files transfer as-is:
scripts/ shots/fixtures.ts screenshots.config.ts shots/helpers.ts playwright.config.ts tsconfig.json shots/example.spec.ts .env.example -
Install dev dependencies in your docs repo:
pnpm add -D @playwright/test dotenv fast-glob tsx typescript @types/node pnpm exec playwright install chromium -
Add the scripts to your
package.json:"scripts": { "shots": "tsx scripts/screenshots.ts", "shots:force": "tsx scripts/screenshots.ts --force", "shots:login": "tsx scripts/screenshots.ts --login" }
-
Configure your app's URL — copy
.env.exampleto.envand edit:APP_BASE_URL=https://app.example.com APP_LOGIN_URL=/login # only if your app is gated -
Write your first directive and test:
{/* in any .mdx file */} <Screenshot name="getting-started/dashboard" caption="The dashboard" alt="..." />
// shots/getting-started.spec.ts import { test, waitForReady } from "./fixtures"; test("getting-started/dashboard", async ({ page, shot }) => { await page.goto("/dashboard"); await waitForReady(page); await shot(); });
-
Run it:
pnpm shots:login # one-time, only if your app needs auth pnpm shots # captures missing pairs and rewrites <Frame> blocks
That's it. Deploy your docs and you've got synced light/dark screenshots.
Apps usually need a tiny amount of pre-test setup — pin a tenant cookie, hide a transient banner, seed localStorage. Use the fixture's appInit hook for this. See shots/README.md for the recipe.
Read docs/how-it-works.md for the architecture and lifecycle of a shot. Read shots/README.md for authoring conventions and CLI options. If you use Claude Code, .claude/skills/docs-screenshots/SKILL.md is a copy of the skill our team uses to add and audit shots — drop it into your repo's .claude/skills/ and Claude will pick it up.
pnpm shots # only tests with missing PNG pairs
pnpm shots --force # recapture everything
pnpm shots --force --name section/stem # recapture one shot
pnpm shots --file path/to/page.mdx # scope to one MDX file
pnpm shots --no-sync # capture without rewriting <Frame> blocks
pnpm shots --dry-run # show what would run, don't run it
pnpm shots --login # capture storageState for a gated app- Playwright-only. No Cypress, no Puppeteer.
- No NPM package. This is a starter kit you copy into your repo. Easy to customize, no upgrade churn — but no
npm updateeither. - No image optimization. PNGs are saved at 2× DPR (1440 × 900 → 2880 × 1800). Add a build-time optimizer (Mintlify's image pipeline,
sharp, etc.) if you need it. - No CI helpers. Wire it into your own pipeline as needed; the CLI exits non-zero on failure.
MIT — copyright Frontic. This is a snapshot of the tool we use internally; PRs welcome but no SLA. See CONTRIBUTING.md. Originally built for the Frontic docs.