The detail that stopped me wasn’t in the README. It was a code comment inside agent/engine.py: some model, observed in production, will call its own asset-fetching tools and then simply stop, never writing the file it was asked for. Someone had to write a dedicated exception, EmptyOutputError, just so that a run which produces nothing doesn’t get scored as a success.
That’s not the kind of thing you put in marketing copy, but it’s the kind of thing that tells you a project has actually been run against real screenshots, by real models, enough times to hit the weird edge. You’ve probably felt the smaller version of this problem yourself: you paste a mockup into an AI tool, get back a page of Tailwind that’s almost right, and then spend twenty minutes hunting for the one div with the wrong padding. Fast, plausible, and only sort of yours.
screenshot-to-code is an MIT-licensed tool by Abi Raja that takes a screenshot, a Figma export, or a screen recording, and turns it into HTML, React, Vue, Bootstrap, or Ionic markup. It ships a FastAPI backend and a React/Vite frontend, runs locally or via Docker, and has picked up around 78k stars and 9.5k forks since it first appeared in late 2023. The pitch, stripped of adjectives, is: point it at an image, get a working page back, in whichever stack you already use.
The honest objection here is that UI-from-screenshot has been “basically solved” by hosted tools for a couple of years now, and self-hosting one more Python service to get the same result is a hard sell when v0 or Bolt will do it in a browser tab with no setup. That objection holds if you only care about the first thirty seconds. It stops holding once you factor in what a hosted tool won’t give you: which model touched your screenshot, whether it’s an internal product screen you don’t want leaving your infrastructure, and control over the cost per generation. Those are exactly the three things running your own copy buys you.
The part that’s actually interesting
Most screenshot-to-code tools are a single vision prompt: image in, HTML out, hope for the best. This one is built as an agent with tools, and the tool that matters most is screenshot_preview. Once the model has written HTML with create_file, it can call screenshot_preview to render that exact file in a headless Chromium instance, at both desktop and mobile viewports, and get the resulting PNGs back as image input in the same conversation. It’s genuinely looking at its own output before deciding whether to fix something with edit_file. The images are never saved as assets; they exist purely so the model can grade its own homework mid-run.
The loop is worth spelling out, because the ordering is the whole trick:
- The model writes HTML with
create_file. - It calls
screenshot_preview, which renders that file in headless Chromium at a desktop and a mobile viewport. - Both PNGs come back into the same conversation as image input.
- The model compares what it sees against the original screenshot and patches the gap with
edit_file.
Nothing here is novel on its own. What’s unusual is that step 2 is wired in as a tool the model can reach for mid-run, rather than a verification pass bolted on at the end where it can only produce a score nobody acts on.
I assumed, going in, that “multiple variants” meant the same prompt sent four times for redundancy. It doesn’t. NUM_VARIANTS = 4 drives a genuine model bake-off: with every provider key configured, one generation fans out across several models at once — the current lineup spans Claude Opus, two Gemini 3 configurations, and GPT-5.5 — and you pick the winner yourself in the UI. The model lineup isn’t static either — comments in model_choice_sets.py cite specific judged eval scores from internal test sessions, down to things like a 48-second median and $0.05 per brief for the fast slot. That’s an unusually candid level of “here’s why we picked this model” to leave sitting in the source.
The knobs that matter
If you’re running it locally, backend/config.py is worth reading before you touch the .env file. GENERATION_MAX_COST_USD = 3.0 is a hard spend ceiling per generation; go over it mid-run and the agent aborts. NUM_VARIANTS_VIDEO = 2 caps video-to-prototype mode to two Gemini variants, since Gemini is the only provider that takes video input here. Getting the self-checking loop at all requires Chromium:
poetry run playwright install chromium
Skip that step and the app doesn’t complain — the settings dialog just quietly shows preview as unavailable, and the model writes code blind.
Caveats, honestly
The budget ceiling tells you nothing useful when it fires. BudgetExceededError returns “this variant exceeded its resource limit” to the frontend, by design, with no dollar figure. That’s a reasonable choice for a hosted product; it’s a frustrating one if you’re self-hosting and trying to work out whether $3 was actually too low for your use case.
A missing Chromium install degrades output silently. There’s no error, no warning banner pushed to you mid-generation, just a model working without the one tool that lets it check its layout against a real render.
Keys land in plaintext. The documented setup path is echo "OPENAI_API_KEY=sk-..." > .env. That’s fine on a personal machine, less fine if this ends up on anything shared, since there’s no keyring integration or encryption at rest mentioned anywhere in the setup docs.
This isn’t for someone who wants zero setup. If you don’t already have at least one paid model API key you’re willing to spend against, the hosted screenshottocode.com is the better starting point; the self-hosted path assumes you’re comfortable with Poetry, pnpm, and Playwright browser binaries.
Where it sits next to v0
| screenshot-to-code | v0 (Vercel) | |
|---|---|---|
| Best at | Self-hosting, BYO model choice across OpenAI/Anthropic/Gemini, output in six different stacks, video-to-prototype mode | Zero setup, one-click deploy straight to Vercel, tight Next.js and shadcn/ui integration |
| Falls down on | You supply and pay for API keys directly; no managed hosting or one-click deploy | Locked to a credit-metered subscription, and output is React/Next.js only |
What I’d steal for my own setup isn’t the multi-model bake-off, it’s the smaller idea underneath it: don’t trust a code-generation model’s claim that it’s finished. Make it look at a screenshot of what it actually produced before it gets to say so.