renderstate
A deterministic UI-state renderer for React screens. Point it at your screen components; it
discovers them, reads their prop contracts from TypeScript, derives a small-but-strong set of prop
combinations, renders each in isolation (no router, no backend, no app boot), and screenshots them.
The output is a neutral manifest.json plus PNG frames. A static board is the first viewer of that
manifest; a publisher adapter can send the same output anywhere.
Install
npm install renderstate
Published on npm: npmjs.com/package/renderstate.
Pipeline
component discovery (convention)
→ typescript inspection (ts-morph, never guesses)
→ value resolution (generic + fixtures, recursive)
→ case generation (anchors + pairwise)
→ direct rendering (project's own Vite + Playwright)
→ frames + manifest.json
├── board viewer (renderstate serve)
└── publisher adapter (renderstate publish)
Integrating a project
Three things, nothing more:
renderstate.config.ts— where the screens are, viewport, labels, publishers.- Fixtures module — factories for domain types the engine can't synthesize, keyed by type name, with named variants. Node-safe (no CSS/JSX).
- Wrapper module — a
wrap(node)decorator giving screens your global CSS / fonts / frame.
// renderstate.config.ts
import { defineConfig } from 'renderstate';
export default defineConfig({
screens: ['src/screens/*-screen.tsx'], // convention: *-screen.tsx + exported *Screen
harness: 'src/renderstate.harness.tsx', // default-exports wrap(node)
fixtures: 'src/renderstate.fixtures.ts', // default-exports Record<TypeName, { variants }>
viewport: { width: 402, height: 874 },
});
Multiple sizes
Capture every screen at several device sizes and let the viewer toggle between them. The first entry
is the default (also written as each frame's image); the manifest carries a per-size images map.
export default defineConfig({
screens: ['src/screens/*-screen.tsx'],
harness: 'src/renderstate.harness.tsx',
fixtures: 'src/renderstate.fixtures.ts',
viewports: [
{ id: 'pro', label: 'iPhone 16 Pro', width: 402, height: 874 }, // default
{ id: 'se', label: 'iPhone SE', width: 375, height: 667 },
{ id: 'max', label: 'iPhone 16 Pro Max', width: 440, height: 956 },
],
});
renderstate generate # write .renderstate/{manifest.json,index.html,frames/}
renderstate serve # open the board on localhost
renderstate publish # run your publisher adapter(s)
What it derives automatically
| TypeScript | states |
|---|---|
boolean |
false, true |
| literal union / enum | each member |
T | null |
null + a representative T |
optional prop? |
absent + a representative value |
T[] |
empty + populated (element resolved the same way, recursively) |
| callbacks | a no-op recorder |
string / number |
deterministic representative values |
| named domain type | your fixture registry (fails loudly if unregistered) |
Core invariant: renderstate never guesses about an unresolved type. A type it cannot resolve confidently, with no fixture, stops the run with the screen and prop named — never a silent skip.
Determinism
Guaranteed for a fixed (renderstate version, browser environment, source, fixtures): frozen clock,
disabled animations, fixed viewport + DSF, loaded fonts, seeded generic values. Not "identical bytes
forever" — browser/font/platform drift is real.
Not in v1 (on purpose)
Not a testing framework, navigation/backend simulator, replay system, or publishing platform. The manifest is the product; those are separate consumers that can be built on top of it later.
See examples/mini for the whole integration in a second, non-Matra project.
Product direction
The living product thesis, roadmap, and research questions are grounded in the current implementation. GitHub issues track bounded work; speculative ideas stay in research until real usage earns them a build commitment.