App Store screenshots, as code.
Turn the screens you've captured into your listing's story. Lay them out in HTML and CSS, and one command renders every one at Apple's exact pixels, for every device and language.
Start from the overlay starter:
npm install -D recadro
npx recadro init store/screenshots \
--starter overlay \
--captures path/to/captures
npx recadro devFree for any app. Node 20.11 or later.
Each starter is a set of panels in a different look. Pick one — or .
Drop your screenshots
Your screenshots stay in this browser. Nothing is uploaded.
The real lineup, running the overlay starter on sample screens. Switch its store to light, or click a panel.Tap a panel to see it alone.
Your layout is your HTML.
A panel is a plain HTML page that you or your coding agent edit freely, and npx recadro dev shows each change as soon as it's saved.
Its words sit apart, in strings/: one file per language, so another locale is one more file. recadro picks it up by its name and renders every panel in that language.
The layout and its strings live in your repo like any other code: reviewed, diffed, reverted.
panels/01-promise.html in the overlay starter<!doctype html>
<!-- The opening panel: the promise, and the one with a subline. -->
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="../panel.css" />
</head>
<body>
<img class="capture" data-capture="01-home.webp" alt="" />
<header class="band">
<h1 data-string="headline"></h1>
<p data-string="subline"></p>
</header>
<script type="module" src="../panel.js"></script>
</body>
</html>panel.css, where the look is set:root {
--ground: #16181d;
--ink: #ffffff;
--ink-soft: rgb(255 255 255 / 0.88);
--accent: #c8372d;
--highlight: #ffd84d;
--font-display: system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
--font-text: system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
/* How steeply the band's edge and the words on it rise to the right. */
--slant: 5deg;
/* How much the magnifier enlarges what is under it. */
--zoom: 1.5;
}strings/en-US.json, this starter's choice of format. Your panels read it, not recadro, so the format is yours.{
"01-promise": {
"headline": "Say what it *does*.",
"subline": "One line on who it is for."
},
"02-moment": { "headline": "Show it working." },
"03-difference": { "headline": "Name what only it does." },
"04-everyday": { "headline": "Make the daily part *easy*." },
"05-start": { "headline": "End on how to start." }
}panels/01-promise.html in the caption starter<!doctype html>
<!-- The opening panel: the promise, with a highlighted word and a stroke drawn under it. -->
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="../panel.css" />
</head>
<body data-decor="underline">
<header class="copy">
<h1 data-string="headline"></h1>
<p data-string="subline"></p>
</header>
<div class="device">
<div class="screen"><img class="capture" data-capture="01-home.webp" alt="" /></div>
</div>
<script type="module" src="../panel.js"></script>
</body>
</html>panel.css, where the look is set:root {
--ground: #f3efe8;
--ink: #1c2733;
--ink-soft: #5b6571;
--accent: #e8912d;
--font-display: Georgia, "Times New Roman", serif;
--font-text: system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
--ground-image: url("assets/ground.svg");
}strings/en-US.json, this starter's choice of format. Your panels read it, not recadro, so the format is yours.{
"01-promise": {
"headline": "Say what it *does*.",
"subline": "One line on who it is for and why it matters."
},
"02-moment": {
"headline": "Show the moment it works.",
"subline": "The screen where the promise pays off."
},
"03-difference": {
"headline": "Name what only it does.",
"subline": "Then zoom in on the detail that proves it."
},
"04-review": {
"headline": "Let a real review speak.",
"quote": "I open it every morning. It does the one thing I needed, and it never gets in my way.",
"name": "Alex Morgan",
"role": "App Store review"
},
"05-start": {
"headline": "End on how to start.",
"subline": "No account, nothing to set up — whatever is true.",
"badge": "Today"
}
}panels/01-promise.html in the panorama starter<!doctype html>
<!-- The opening panel: the promise. -->
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="../panel.css" />
</head>
<body>
<div class="world"></div>
<header class="copy">
<h1 data-string="headline"></h1>
<p data-string="subline"></p>
</header>
<script type="module" src="../panel.js"></script>
</body>
</html>panel.css, where the look is set:root {
--ground: #14173a;
--ink: #ffffff;
--ink-soft: rgb(255 255 255 / 0.78);
--accent: #ff8a5c;
--font-display: system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
--font-text: system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
/* A picture for the whole world instead, as wide as all the panels together: */
/* --panorama-image: url("assets/panorama.jpg"); */
--panorama-image: none;
}strings/en-US.json, this starter's choice of format. Your panels read it, not recadro, so the format is yours.{
"01-promise": {
"headline": "Say what it *does*.",
"subline": "One line on who it is for."
},
"02-moment": {
"headline": "Show it working."
},
"03-difference": {
"headline": "Name what only it does."
},
"04-everyday": {
"headline": "Make the daily part easy."
},
"05-start": {
"headline": "End on how to start.",
"points": [
"What it takes to *begin*.",
"What it costs, plainly.",
"Nothing to set up, if true."
]
}
}panels/01-promise.html in the exploded starter<!doctype html>
<!--
The opening panel: the promise, its screen taken apart the most. Each .layer
is a region of the capture lifted off the screen, placed below as --x --y --w
--h in percent of the capture: x and y its top left corner, w and h its size.
--z lifts a layer more steps and --zoom grows it more. A screen lays out
differently on iPad, so the regions fork on [data-device="iPad"]; fork a
language the same way with :lang(de), and drop a layer there with display: none.
-->
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="../panel.css" />
<style>
.lift-1 { --x: 4; --y: 23; --w: 92; --h: 10.5; }
.lift-2 { --x: 4; --y: 46; --w: 92; --h: 11; --z: 2; }
[data-device="iPad"] .lift-1 { --x: 1.5; --y: 14; --w: 97; --h: 6.5; }
[data-device="iPad"] .lift-2 { --x: 1.5; --y: 30; --w: 97; --h: 7.5; }
</style>
</head>
<body>
<header class="copy">
<h1 data-string="headline"></h1>
<p data-string="subline"></p>
</header>
<div class="stage">
<div class="phone">
<div class="screen"><img class="capture" data-capture="01-home.webp" alt="" /></div>
<div class="lifted">
<div class="layer lift-1"></div>
<div class="layer lift-2"></div>
</div>
</div>
</div>
<script type="module" src="../panel.js"></script>
</body>
</html>panel.css, where the look is set:root {
--ground: #e8ebf0;
--ink: #161a21;
--ink-soft: #596170;
--accent: #2d4bf0;
--font-display: system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
--font-text: system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
/* How far the phone tips back and turns, in degrees; a panel can set either on its .stage. */
--tilt: 20;
--turn: 4;
/* How far a layer rises off the screen, per step of its z, and how much larger it grows. */
--lift: 10vh;
--zoom: 1.1;
}strings/en-US.json, this starter's choice of format. Your panels read it, not recadro, so the format is yours.{
"01-promise": {
"headline": "Say what it *does*.",
"subline": "One line on who it is for."
},
"02-moment": { "headline": "Show it working." },
"03-difference": { "headline": "Name what only it does." },
"04-everyday": { "headline": "Make the daily part easy." },
"05-start": { "headline": "End on how to start." }
}panels/01-promise.html in the callouts starter<!doctype html>
<!--
The opening panel: the promise, with two notes on the screen. Each .note lands
on --x --y below, in percent of the capture, with its label past the left or
right edge of the phone as its class says; the label is the string named like
the note. A screen lays out differently on iPad, so the points fork on
[data-device="iPad"]; fork a language the same way with :lang(de), where a
longer label may want the other side.
-->
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="../panel.css" />
<style>
.first { --x: 50; --y: 27.3; }
.second { --x: 40; --y: 54; }
[data-device="iPad"] .first { --x: 23; --y: 16.3; }
[data-device="iPad"] .second { --x: 76; --y: 24.5; }
</style>
</head>
<body>
<header class="copy">
<h1 data-string="headline"></h1>
<p data-string="subline"></p>
</header>
<div class="device">
<div class="screen">
<img class="capture" data-capture="01-home.webp" alt="" />
</div>
<div class="marks">
<div class="note left first"><span class="label" data-string="first"></span></div>
<div class="note right second"><span class="label" data-string="second"></span></div>
</div>
</div>
<script type="module" src="../panel.js"></script>
</body>
</html>panel.css, where the look is set:root {
--ground: #123b33;
--ink: #f5efe3;
--ink-soft: rgb(245 239 227 / 0.74);
--accent: #f7c548;
--frame: #0a211c;
--font-display: system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
--font-text: system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
}strings/en-US.json, this starter's choice of format. Your panels read it, not recadro, so the format is yours.{
"01-promise": {
"headline": "Say what it *does*.",
"subline": "One line on who it is for.",
"first": "Point at the part that *matters*.",
"second": "A few words each."
},
"02-moment": {
"headline": "Show it working.",
"first": "Name what happens *here*."
},
"03-difference": {
"headline": "Name what only it does.",
"first": "The first thing.",
"second": "The second, *on* the screen.",
"third": "A third, if it earns it."
},
"04-everyday": {
"headline": "Make the daily part easy.",
"first": "Light the one part people come back for."
},
"05-start": {
"headline": "End on how to start.",
"first": "Show where to *begin*."
}
}panels/01-promise.html in the poster starter<!doctype html>
<!-- The opening panel: the promise, with a sticker under it. -->
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="../panel.css" />
</head>
<body data-swatch="1">
<header class="copy">
<h1 data-string="headline"></h1>
<p data-string="subline"></p>
</header>
<div class="phone">
<div class="screen"><img class="capture" data-capture="01-home.webp" alt="" /></div>
</div>
<script type="module" src="../panel.js"></script>
</body>
</html>panel.css, where the look is set:root {
--ink: #141414;
--paper: #fffdf6;
--swatch-1: #ff8f3f;
--swatch-2: #7cc6fe;
--swatch-3: #ff9ecd;
--swatch-4: #c2f261;
--swatch-5: #b8a6ff;
/* A condensed face macOS ships; elsewhere the fallback is wider and wraps sooner. */
--font-display: "Avenir Next Condensed", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif;
--font-text: system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
}strings/en-US.json, this starter's choice of format. Your panels read it, not recadro, so the format is yours.{
"01-promise": {
"headline": "Say what it *does*.",
"subline": "One line on who it is for."
},
"02-moment": { "subline": "No headline: let the screen talk." },
"03-difference": {
"headline": "Name what *only* it does.",
"subline": "Two screens, when one can't say it."
},
"04-everyday": { "headline": "Two words." },
"05-start": {
"headline": "End on how to start.",
"subline": "Nothing to set up — whatever is true."
}
}Edit the layout — on your own or with your agent.
npx recadro dev opens the lineup: every panel side by side, reloading as you save. Edit the files yourself, or ask your coding agent. The lineup has a pointer to help you tell it where: click, and it copies a reference to the exact spot you mean.
npx recadro dev --live opens a connection between the lineup and your agent: click an element, type your note right there in the browser, and it goes straight to your agent.
npx recadro skill sets this up in your repo as a skill for Claude Code. Underneath are two plain commands, recadro wait and recadro reply, so another agent can run the same loop.
One command renders every screenshot.
npx recadro render opens each panel at every device's viewport, waits until it has fully loaded, and writes PNGs at Apple's sizes: 1320×2868 for the 6.9″ iPhone, 2048×2732 for the 13″ iPad.
It renders the devices you've captured screens for and skips a panel whose screen isn't captured yet, so out/ only ever holds what can ship.
It runs headless in Playwright's Chromium, on your machine or in CI. Upload the PNGs in App Store Connect by hand, or from your pipeline: fastlane's deliver reads out/ as it is, and asc or the App Store Connect API take the same files.
npx recadro render
recadro 5 panels in store/screenshots
devices iPhone, iPad (captures)
locales en-US (strings/)
out store/screenshots/out/{locale}/{device}-{slug}.png
wrote en-US/iPhone-01-promise.png
wrote en-US/iPhone-02-moment.png
wrote en-US/iPhone-03-difference.png
wrote en-US/iPhone-04-everyday.png
wrote en-US/iPhone-05-start.png
wrote en-US/iPad-01-promise.png
wrote en-US/iPad-02-moment.png
wrote en-US/iPad-03-difference.png
wrote en-US/iPad-04-everyday.png
wrote en-US/iPad-05-start.png
10 written, 0 skipped → store/screenshots/out1320×2868 for iPhone and 2048×2732 for iPad, read back from the files.npx recadro render
recadro 5 panels in store/screenshots
devices iPhone, iPad (captures)
locales en-US (strings/)
out store/screenshots/out/{locale}/{device}-{slug}.png
wrote en-US/iPhone-01-promise.png
wrote en-US/iPhone-02-moment.png
wrote en-US/iPhone-03-difference.png
wrote en-US/iPhone-04-review.png
wrote en-US/iPhone-05-start.png
wrote en-US/iPad-01-promise.png
wrote en-US/iPad-02-moment.png
wrote en-US/iPad-03-difference.png
wrote en-US/iPad-04-review.png
wrote en-US/iPad-05-start.png
10 written, 0 skipped → store/screenshots/out1320×2868 for iPhone and 2048×2732 for iPad, read back from the files.npx recadro render
recadro 5 panels in store/screenshots
devices iPhone, iPad (captures)
locales en-US (strings/)
out store/screenshots/out/{locale}/{device}-{slug}.png
wrote en-US/iPhone-01-promise.png
wrote en-US/iPhone-02-moment.png
wrote en-US/iPhone-03-difference.png
wrote en-US/iPhone-04-everyday.png
wrote en-US/iPhone-05-start.png
wrote en-US/iPad-01-promise.png
wrote en-US/iPad-02-moment.png
wrote en-US/iPad-03-difference.png
wrote en-US/iPad-04-everyday.png
wrote en-US/iPad-05-start.png
10 written, 0 skipped → store/screenshots/out1320×2868 for iPhone and 2048×2732 for iPad, read back from the files.npx recadro render
recadro 5 panels in store/screenshots
devices iPhone, iPad (captures)
locales en-US (strings/)
out store/screenshots/out/{locale}/{device}-{slug}.png
wrote en-US/iPhone-01-promise.png
wrote en-US/iPhone-02-moment.png
wrote en-US/iPhone-03-difference.png
wrote en-US/iPhone-04-everyday.png
wrote en-US/iPhone-05-start.png
wrote en-US/iPad-01-promise.png
wrote en-US/iPad-02-moment.png
wrote en-US/iPad-03-difference.png
wrote en-US/iPad-04-everyday.png
wrote en-US/iPad-05-start.png
10 written, 0 skipped → store/screenshots/out1320×2868 for iPhone and 2048×2732 for iPad, read back from the files.npx recadro render
recadro 5 panels in store/screenshots
devices iPhone, iPad (captures)
locales en-US (strings/)
out store/screenshots/out/{locale}/{device}-{slug}.png
wrote en-US/iPhone-01-promise.png
wrote en-US/iPhone-02-moment.png
wrote en-US/iPhone-03-difference.png
wrote en-US/iPhone-04-everyday.png
wrote en-US/iPhone-05-start.png
wrote en-US/iPad-01-promise.png
wrote en-US/iPad-02-moment.png
wrote en-US/iPad-03-difference.png
wrote en-US/iPad-04-everyday.png
wrote en-US/iPad-05-start.png
10 written, 0 skipped → store/screenshots/out1320×2868 for iPhone and 2048×2732 for iPad, read back from the files.npx recadro render
recadro 5 panels in store/screenshots
devices iPhone, iPad (captures)
locales en-US (strings/)
out store/screenshots/out/{locale}/{device}-{slug}.png
wrote en-US/iPhone-01-promise.png
wrote en-US/iPhone-02-moment.png
wrote en-US/iPhone-03-difference.png
wrote en-US/iPhone-04-everyday.png
wrote en-US/iPhone-05-start.png
wrote en-US/iPad-01-promise.png
wrote en-US/iPad-02-moment.png
wrote en-US/iPad-03-difference.png
wrote en-US/iPad-04-everyday.png
wrote en-US/iPad-05-start.png
10 written, 0 skipped → store/screenshots/out1320×2868 for iPhone and 2048×2732 for iPad, read back from the files.Start in your repo.
Free for any app, paid or free. Your screenshots, and the sets you start from a starter, are yours.
Start from the overlay starter:
npm install -D recadro
npx recadro init store/screenshots \
--starter overlay \
--captures path/to/captures
npx recadro dev