Guide

A single HTML file app, from a builder that defaults to React

Most pages on this topic teach you how to write a single HTML file by hand. The thing none of them mention: when you ask an AI app builder to make one for you, you are fighting the scaffold the builder boots from. Here is the exact line of code that decides that on mk0r, why the agent drifts back to multi-file React, and the prompt formula that produces an actual standalone .html.

M
Matthew Diakonov
8 min read
4.7from single-file builders on mk0r
Anchor: e2b.Dockerfile line 65 hard-codes the React-TS Vite scaffold
Why agents drift: src/core/e2b.ts line 148 locks the assumed stack
Override formula: four sentences, none of them optional
Verifiable by reading the source, not by trusting marketing copy

The premise everyone gets wrong

The promise of a single HTML file app is the same as it was in 2003: one file, no build, the browser is the runtime, you ship by emailing the .html. The promise of an AI app builder is that you describe what you want and a working app appears. Both promises are real. People assume those two promises stack: type a sentence, get one self-contained .html.

They almost never do. Every AI app builder I have looked at, including the one I work on, scaffolds a multi-file project at the moment a sandbox boots, before you have typed anything. The first prompt lands inside that scaffold, not in an empty directory. So the agent does what its environment is shaped to encourage: it adds a component to the existing project. You type "single HTML file app", you get src/components/Thing.tsx imported in src/App.tsx and a dev server on port 5173.

That is not the agent being unhelpful. It is the agent reading the room. The room was decorated for a multi-file React project an hour before you opened the door. If you want a single .html file, you have to redecorate the room first.

What actually happens when you type the prompt

Three components, in order. The Dockerfile picks the scaffold at template build time. The agent inherits the scaffold at sandbox boot. The agent reads the per-project spec and acts inside that frame.

Single HTML file prompt → multi-file React drift

YouSandboxAgentDiskopen mk0r.comscaffold from Dockerfile line 65 (react-ts)'build me a single HTML file app'read /app/CLAUDE.md (line 14: import in App.tsx)write src/components/Thing.tsxpatch src/App.tsx with importpreview at localhost:5173'no, single .html file. one. file.'

The line of code that decides this

Inside the mk0r repo there is a Dockerfile that builds the E2B sandbox image. Line 65 of docker/e2b/e2b.Dockerfile reads:

RUN npm create vite@latest . -- --template react-ts \
 && npm install \
 && npm install -D tailwindcss @tailwindcss/vite

That command is what bakes a Vite + React + TypeScript project into the template image at /app. Every fresh sandbox you boot inherits this scaffold. The dev server on port 5173 is running before you have typed anything. So is the source tree at /app/src/: main.tsx, App.tsx, index.css, the HMR bridge file. The agent does not pick the scaffold; the Dockerfile picks the scaffold for the agent.

On top of that, the per-project spec at /app/CLAUDE.md line 14 explicitly says: "all new components must be imported here," pointing at src/App.tsx. That is not a suggestion the agent can ignore; it is the loud-and-bold rule for the project. So the agent reads "build me an app", opens the spec, sees the rule, adds a component to App.tsx. End of story.

Add the system prompt at src/core/e2b.ts line 148, which describes the agent as "an expert app builder inside an E2B sandbox with a Vite + React + TypeScript + Tailwind CSS v4 project at /app." That paragraph is the agent's prior. By the time your prompt arrives, the agent has been told three times in three different files that the answer is React.

What the scaffold actually looks like, fresh out of the oven

Run the same commands the Dockerfile runs. The sandbox you get when you open mk0r is the contents of this directory plus a couple of overlay files. Nothing about it is a single HTML file.

Inside a fresh sandbox: ls /app

The drift, made concrete

If you type "build a tip splitter as a single HTML file" with no other instructions, the agent will almost certainly do one of three things, in roughly this order of likelihood. It writes src/components/TipSplitter.tsx, adds an import to App.tsx, and reports success based on the dev server preview. Or it writes that React component and a separate .html that calls the same React via a CDN, which is two artifacts pretending to be one. Or it writes a real standalone .html, but also writes the React version, because its verification step is hard-coded to the dev server.

None of those is the agent failing at understanding. They are the agent succeeding at following its environment. The dev server is running, App.tsx is open, the per-project spec says new components go in src/. The path of least resistance is the React path. The single .html lives outside that path.

The override is to make the single-file path the path of least resistance instead. Below is the prompt formula that does that.

The prompt formula that overrides the scaffold

Four sentences. None of them are optional. Drop any one and the drift comes back.

  1. 1

    Name the output path

    Specify /app/dist/<name>.html or wherever you want the file. If you do not name a path, the agent will write it next to App.tsx and you will spend ten minutes trying to find it.

  2. 2

    Forbid the scaffold

    Add: 'no React, no Vite build, no imports from src/, do not edit src/App.tsx, do not run the dev server.' All four sentences are doing work. The first three keep the file standalone. The last one prevents the agent from claiming success based on the React preview.

  3. 3

    Inline everything

    Add: 'inline CSS in a <style> tag, JS in a <script> tag at the bottom of <body>, no external dependencies, no fetch calls unless the user explicitly asks.' This collapses the file into one shippable artifact and forces the agent to skip the npm install reflex.

  4. 4

    Force file:// verification

    Add: 'after writing the file, open file:///app/dist/<name>.html in the in-VM browser, screenshot, confirm it renders.' This is the part that prevents drift. The agent's default verification path is localhost:5173, which only shows the React project. file:// makes it actually open your one file.

Glued together, the prompt looks like: "Build a standalone HTML file at /app/dist/tip-split.html for splitting a restaurant bill across N people. No React, no Vite build, no imports from src/, do not edit src/App.tsx, do not run the dev server. Inline CSS in a <style> tag, JS in a <script> tag at the bottom of <body>, no external dependencies. After writing, open file:///app/dist/tip-split.html in the in-VM browser, screenshot, confirm it renders." That is verbose. It is also the difference between a one-file artifact you can email and a 12-file React project you cannot.

What you get with each prompt shape

Same five-word task on the left, same task on the right with the override applied. The artifact on the right is one file you can drag onto Netlify Drop. The artifact on the left is something else.

Open AI builder. Type: 'build a tip splitter as a single HTML file'. Watch the dev server light up the right pane. Notice the file tree fills with src/components/TipSplitter.tsx, modifications to App.tsx, an updated index.css. Ask: where is the .html file I asked for. Get a fresh App.tsx that imports the new component. Ask again, more firmly. Agent writes /app/standalone.html as a copy of the same React component, plus the multi-file project. Download the project, realize there is no working .html.

  • Output is a multi-file React project, not a .html
  • Live preview is the dev server, not the file
  • No actual single-file artifact at the end

When the React default is the right answer anyway

I am not arguing the scaffold is wrong. For 80% of what people actually build on an AI app maker, the multi-file React project is the right shape. Once an idea has more than one screen, more than one piece of state that survives reload, more than one user, or any need for a real backend, you outgrow a single .html file fast. The scaffold is shaped for that future, on purpose.

The single-file path is a deliberate choice for a narrower set of projects: things you want to email to one person, throwaway prototypes, weekend ideas, embeds, the modern equivalent of a Geocities page that does one thing. For those, the React scaffold is friction with no payoff. You will never run npm install on the receiving end. You will never spin up a dev server. You want the file.

Both shapes are valid. The trap is asking for one and getting the other because the default fought you. The four-sentence override is what closes the gap.

One more honest caveat

If your single HTML file app needs to call an API that requires a secret key, you cannot do that from a one-file app on the public web. The key would sit in the <script> tag, anyone with view-source can grab it. The single-file path forces you into one of three things: APIs with no key (public weather endpoints, CORS-enabled JSON feeds), keys you are okay shipping (Mapbox public tokens, short-lived demo keys), or a tiny serverless function elsewhere that wraps the key and you call from the .html. The third option means you have given up the "one file" promise.

Same applies to anything multi-user, anything with persistent storage past localStorage, anything with auth. The single .html is a Saturday-afternoon sweet spot, not a production target. Go in knowing that and the format pays off.

Want a teardown of your single-file prompt?

Fifteen minutes. Bring an idea, leave with a .html that works and a clear sense of where it stops being one file.

Frequently asked questions

What actually counts as a single HTML file app?

One .html file you can double-click, email, drop on Netlify Drop, or attach to a Reddit comment. The CSS lives in a <style> tag in the same file, the behavior lives in a <script> tag in the same file, the data is either embedded as a JSON literal or fetched from a public no-key API at runtime. There is no build step, no node_modules, no package.json, no bundler. The file is the artifact. Anything that needs a build, a server, or a folder of assets is something else, even if it ships as one route in production.

Why does mk0r default to a multi-file React project instead of a single HTML file?

Because the E2B template image at docker/e2b/e2b.Dockerfile line 65 runs `RUN npm create vite@latest . -- --template react-ts` at build time. Every fresh sandbox starts from that scaffold, so /app already contains package.json, vite.config.ts, src/main.tsx, src/App.tsx, src/index.css, and the bridge file. The agent then reads /app/CLAUDE.md, which on line 14 says 'all new components must be imported here' pointing at src/App.tsx. By the time you type your first prompt, the path of least resistance is to add a component to the existing project, not to abandon it for one .html file.

Can mk0r still produce a real single HTML file app if I ask for it?

Yes. The agent has shell access at /app and can write any file it wants. If you say 'put everything in one self-contained .html file at /app/dist/standalone.html, do not import from src/, no Vite build step, inline CSS in a <style> tag and JS in a <script> tag', the agent will write that file. The thing to watch is that the dev server on port 5173 still serves the React project, so the live preview in the right pane is the React app. To preview the .html, open it directly: navigate the in-VM Chromium to file:///app/dist/standalone.html instead.

What kinds of apps fit cleanly in a single HTML file?

Anything where state lives entirely in the browser and there are no secrets to hide. Calculators, timers, breathing exercises, recipe scalers, tip splitters, single-screen games, kid-safe word drills, a reading-progress bar over an essay, a beat ticker, a conference badge generator, a Pomodoro tracker, an RSVP confirmation page, an offline-first journaling thing that writes to localStorage. The trade is you get instant share-by-link and no friction, you give up multi-user state, real auth, paid APIs that need a server-side key, and any database larger than a JSON literal you embed at the top.

Why does the agent drift back to React even after I say 'single HTML file'?

Two reasons, both visible in the source. First, the system prompt at src/core/e2b.ts line 148 hard-codes the assumed stack: 'a Vite + React + TypeScript + Tailwind CSS v4 project at /app. The dev server is running on port 5173 with HMR.' That paragraph is the agent's prior. Second, the dev-loop instructions at /app/CLAUDE.md line 5 say to navigate to http://localhost:5173 to verify, which only renders the React project. So even if the agent writes the .html, it tends to also bolt the same component into App.tsx so the live preview shows something. Telling it explicitly 'do not edit src/App.tsx, do not run the dev server, only write to /app/dist/standalone.html' shuts both behaviors down.

If I want a single HTML file, why use an AI builder at all instead of just writing it by hand?

You probably should write it by hand if the goal is the file, full stop, and you already know the shape of the JS you want. The honest case for delegating to an AI builder is when you do not know the shape yet. You describe the behavior, the agent writes a working first draft, you read it, you keep what works, you rewrite the parts that did not. The output is still a single .html, the file is the artifact. The builder is a faster way to a draft, not a replacement for understanding the file you ship.

Can I take a multi-file Vite project and squash it back into one HTML file at the end?

Yes, with `vite-plugin-singlefile`. It tells Vite to inline every JS chunk and CSS bundle into the single dist/index.html. The output works for client-only apps with no code-splitting requirements; it is a bigger file than the multi-file build because nothing can be cached separately. mk0r does not run that plugin by default. If you want it, ask the agent to `npm install -D vite-plugin-singlefile`, register it in vite.config.ts, then `npm run build` and grab dist/index.html. That is a different path from 'write a single .html by hand inside the sandbox' and produces a different artifact (a built React app inlined, not a hand-written .html).

Where do I host the single HTML file once I have it?

Netlify Drop for instant URL with no account. Cloudflare R2 with a public bucket alias if you own a domain. GitHub Gist plus the raw URL via githack.com works for sharing with a couple of friends. If you generated the file inside mk0r, the in-VM preview is already a working URL on a *.mk0r.com subdomain, but that URL is tied to the live sandbox; for a permanent home you still want it on storage you control. None of these need a build step because the file is the build.

What is the smallest viable mk0r prompt that produces a single HTML file app?

Something like: 'Build a standalone HTML file at /app/dist/tip-split.html for splitting a restaurant bill across N people with a tip slider. Inline CSS in a <style> tag, JS in a <script> tag, no external dependencies, no fetch calls, no React, no Vite. Do not edit src/. After writing the file, open file:///app/dist/tip-split.html in the in-VM browser and confirm it renders.' Three things matter: name the output path, forbid React and src/ edits explicitly, and tell the agent to verify by opening the file directly instead of the dev server.

mk0r.AI app builder
© 2026 mk0r. All rights reserved.