Skip to content
Open with AI

Configuration

stillsmith.config.tsx
import { defineConfig } from "@stillsmith/capture/react";
import { QueryClientProvider } from "@tanstack/react-query";
import "@/theme.css";
export default defineConfig({
scenes: ["src/**/*.scene.tsx"],
vite: "./vite.config.ts", // auto-detected if omitted
presets: {
docs: { width: 1280, height: 800, dpr: 2, colorScheme: "dark" },
thumb: { width: 1280, height: 800, dpr: 1, colorScheme: "light" },
hero: { width: 2560, height: 1440, dpr: 2, colorScheme: "dark" },
},
targets: {
docs: { outDir: "docs/public/images", flat: true, presets: ["docs"], tags: ["docs"] },
marketing: { outDir: "screenshots", presets: ["hero", "thumb"] },
},
// Wraps every scene. Only ever called in the browser.
wrapper: ({ children }) => (
<QueryClientProvider client={seededClient}>{children}</QueryClientProvider>
),
applyColorScheme: (scheme) =>
document.documentElement.classList.toggle("dark", scheme === "dark"),
});

A preset is a browser configuration: size, pixel density, colour scheme.

A target is an output profile: where images go, at which presets, for which shots. flat: true writes <stem>.jpg directly into outDir; otherwise images are grouped into per-preset subdirectories.

Targets filter by tag, which is what stops you from duplicating scene declarations. Tag a shot docs and the docs target picks it up while the marketing target ignores it. One scene set, several output profiles:

Terminal window
stillsmith capture --target docs

Images are written as jpeg by default. format — set on a target, or at the top level as the default for every target — switches the encoding:

format: "webp", // config-level default: jpeg | png | webp
targets: {
docs: { outDir: "docs/images", format: "webp" },
marketing: { outDir: "screenshots", format: "png" }, // per-target override
},
  • jpeg (default) — smallest, no transparency. quality defaults to 90.
  • png — lossless, always. The right choice when every pixel must survive, e.g. visual-regression baselines.
  • webp — lossless unless you set a quality, which opts into lossy. Lossless webp is pixel-identical to png at a fraction of the size, which makes it the right format for screenshots committed to a repo — it’s what stillsmith’s own docs use. Needs sharp installed (pnpm add -D sharp); png and jpeg come straight from the browser and don’t.

quality (1–100) sits at either level too, and applies to jpeg and webp; png ignores it. The extension follows the format: .jpg, .png, .webp.

It doesn’t bring its own. Your aliases, plugins and CSS pipeline all apply to scenes, which is what lets a scene import a real component and have it work with no duplicated build setup.

If your app’s config carries plugins that can’t run in a headless browser (Tauri, route codegen), strip them:

viteOverrides: {
plugins: [], // merged over your app's config
},

No Vite config at all? stillsmith synthesizes one from your tsconfig paths, PostCSS setup, and env files, and ships shims for meta-framework modules like next/image — see Next.js & other hosts. vite: false forces synthesis even when a vite.config exists; appUrl points tour authoring at a separately-running app.

wrapper wraps every scene: query clients, theme providers, stores, a seeded cache. It’s only ever called in the browser.

applyColorScheme is how a preset’s colorScheme becomes your app’s idea of dark mode. stillsmith owns the browser-level setting; you decide what it means:

applyColorScheme: (scheme) =>
document.documentElement.classList.toggle("dark", scheme === "dark"),

The config is read in Node, because the CLI needs presets, targets and globs before a browser exists, and again in the browser, which needs wrapper and applyColorScheme.

That normally forces two files; it’s why Storybook has main.ts and preview.ts.

stillsmith avoids the split by bundling the config for Node itself, with stylesheets and assets resolved to empty stubs and your Vite aliases resolved the way your app resolves them. So import "@/theme.css" and a JSX wrapper are both safe here: Node can import them, and only the browser ever calls them.

stabilize: {
fonts: true, // await document.fonts.ready before measuring
animations: "disable", // freeze CSS transitions and animations
delay: 0, // global settle time, ms
},

These are the defaults, and worth leaving alone unless something forces your hand. They’re what make a re-capture byte-identical, which keeps a committed screenshot directory from dirtying your git diff on every run.

@stillsmith/capture is the framework-neutral core; @stillsmith/capture/react is the React binding. It types wrapper and Scene against ReactNode and selects the React renderer.

React is an optional peer dependency and the core’s published types mention no framework, so a non-React project can depend on stillsmith without installing React or @types/react. React is the only renderer that ships today.