StackBlitz HTML: a working page in one index.html

StackBlitz is an online code editor that runs in your browser. For plain HTML, the whole project can be one index.html file, and that one file is also what you copy out to share.

"StackBlitz" here means the online code editor at stackblitz.com, not a library you install. For plain HTML, a StackBlitz project is an index.html file with a live preview beside it. Everything the preview shows can sit in that one file.

Try the smallest working version. This is the whole page: markup, CSS and JavaScript in one file.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>One-file page</title>
<style>
  /* In a multi-file project this block would live in style.css */
  body {
    margin: 0; padding: 24px; font-family: system-ui, sans-serif;
    background: #f4f5f7; color: #1d2330;
  }
  .card {
    max-width: 340px; padding: 18px 20px; border-radius: 12px;
    background: #fff; box-shadow: 0 6px 20px rgba(0, 0, 0, .1);
  }
  h1 { margin: 0 0 6px; font-size: 20px; }
  p { margin: 0 0 14px; color: #4b5563; }
  button {
    font: inherit; padding: 10px 16px; border: 0; border-radius: 8px;
    background: #2563eb; color: #fff; cursor: pointer;
  }
  #count { font-weight: 700; }
</style>
</head>
<body>
  <div class="card">
    <h1>Hello from one file</h1>
    <p>Clicked <span id="count">0</span> times.</p>
    <button id="btn" type="button">Click me</button>
  </div>

  <!-- Placed after the elements it uses, so they exist when it runs -->
  <script>
    // In a multi-file project this block would live in script.js
    const btn = document.getElementById('btn');
    const count = document.getElementById('count');
    let n = 0;
    btn.addEventListener('click', () => {
      n += 1;
      count.textContent = n;
    });
  </script>
</body>
</html>
A complete page in one file. Click the button, then edit the code and the example reruns.

If it works here, it works pasted into the index.html of an HTML project. Nothing else is needed.

What StackBlitz is, in one paragraph

The StackBlitz docs say it is powered by WebContainers, which they describe as a WebAssembly-based operating system that boots a Node.js environment in milliseconds. They also say development, including Node.js and git, runs inside your browser tab.

You do not need any of that for an HTML page. The docs describe a second environment, EngineBlock, built for front-end projects only. A plain page is exactly that case.

Start a plain HTML project

  1. Open the StackBlitz start page and choose a starter without a framework. The docs group starters into sets such as popular, frontend and vanilla.
  2. Open index.html and replace its contents with your page.
  3. Put the CSS in a <style> tag in the head.
  4. Put the JavaScript in a <script> tag at the end of the body.
  5. Check the preview. Reload it once to be sure it still works from a cold start.

The StackBlitz SDK uses the same idea. Its example creates a project from an object of file names and contents, with a template field. The html template is one of the documented values.

One file instead of three

Editors encourage splitting a page into index.html, style.css and script.js. That is tidy, but it means the page is three files. Copy only the first and the other two are left behind.

Three project files become one file when the CSS and JavaScript move inside style and script tags.
Three project files become one file when the CSS and JavaScript move inside style and script tags.

The fix is to paste the CSS between <style> tags and the JavaScript between <script> tags. The page keeps working, and one file is all you have to send. The self-contained HTML checklist covers the rest of what a one-file page needs.

Add a library with one script tag

A library does not need an install step in a plain page. A script tag that points at a CDN address is enough. This example loads canvas-confetti from jsDelivr.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Library from a CDN</title>
<style>
  body {
    margin: 0; padding: 24px; font-family: system-ui, sans-serif;
    background: #f4f5f7; color: #1d2330;
  }
  .card {
    max-width: 340px; padding: 18px 20px; border-radius: 12px;
    background: #fff; box-shadow: 0 6px 20px rgba(0, 0, 0, .1);
  }
  h1 { margin: 0 0 6px; font-size: 20px; }
  p { margin: 0 0 14px; color: #4b5563; }
  button {
    font: inherit; padding: 10px 16px; border: 0; border-radius: 8px;
    background: #16a34a; color: #fff; cursor: pointer;
  }
  #status { margin-top: 12px; font-size: 14px; color: #6b7280; }
</style>
<!-- Pinned version: this URL always returns the same file. defer = run after the page is parsed -->
<script defer src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.4/dist/confetti.browser.min.js"></script>
</head>
<body>
  <div class="card">
    <h1>A library, one script tag</h1>
    <p>No install step. The tag is the whole setup.</p>
    <button id="go" type="button">Throw confetti</button>
    <div id="status" aria-live="polite"></div>
  </div>

  <script>
    const status = document.getElementById('status');
    document.getElementById('go').addEventListener('click', () => {
      // If the CDN script did not load, the global does not exist
      if (typeof confetti !== 'function') {
        status.textContent = 'The library did not load. Check the script URL and your connection.';
        return;
      }
      status.textContent = '';
      confetti({ particleCount: 90, spread: 70, origin: { y: 0.7 } });
    });
  </script>
</body>
</html>
One script tag loads the library. If it fails to load, the page says so instead of staying silent.

Three habits keep this reliable:

  • Pin the version. The address contains @1.9.4, which names one release. The library README shows its tag in that form.
  • Add defer. MDN says a deferred script runs after the document is parsed, in the order the tags appear.
  • Use the library inside a handler. The click runs after everything has loaded, so confetti exists by then.

Order matters for your own scripts too. A plain script runs the moment the parser reaches it.

A script placed before the button cannot find it. Placed after the button, it can.
A script placed before the button cannot find it. Placed after the button, it can.

When the page needs a build step

Some StackBlitz projects use the node template, where you decide how the app is built, for example with Vite or webpack. Those projects are source code, not a finished page. A typical layout looks like this:

my-app/
  index.html      <- loads /src/main.js
  package.json
  src/main.js     <- uses import

The page you can share comes from the build. The Vite guide names the command vite build, and says the output goes to the folder set by build.outDir.

If the result will live under a sub-path, set the base option so asset paths are rewritten. Hosting a built front end walks through those two problems.

One more trap: an import of a separate file from an HTML file opened locally fails. MDN says loading a module page through a file:// address gives CORS errors. Inline <script type="module"> code avoids it.

A finished page, still one file

Here is a small tool that uses what we covered: inline CSS, one inline script at the end, and a form handled inside the page.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tip splitter</title>
<style>
  body {
    margin: 0; padding: 20px; font-family: system-ui, sans-serif;
    background: #f4f5f7; color: #1d2330;
  }
  form {
    max-width: 360px; margin: 0 auto; padding: 20px; border-radius: 14px;
    background: #fff; box-shadow: 0 6px 20px rgba(0, 0, 0, .1);
  }
  h1 { margin: 0 0 14px; font-size: 20px; }
  label { display: block; margin: 12px 0 4px; font-size: 14px; font-weight: 600; }
  input {
    width: 100%; box-sizing: border-box; padding: 10px 12px; font: inherit;
    border: 1px solid #cbd2dc; border-radius: 8px;
  }
  button {
    width: 100%; margin-top: 16px; padding: 12px; font: inherit; font-weight: 600;
    border: 0; border-radius: 8px; background: #2563eb; color: #fff; cursor: pointer;
  }
  #out {
    margin-top: 16px; padding: 12px 14px; border-radius: 8px;
    background: #f0f6ff; font-size: 15px; line-height: 1.5; min-height: 22px;
  }
  #out b { font-size: 20px; }
  .err { background: #fff1ec; color: #9a3412; }
</style>
</head>
<body>
  <form id="f" novalidate>
    <h1>Split the bill</h1>
    <label for="bill">Bill</label>
    <input id="bill" name="bill" type="number" inputmode="decimal" min="0" step="0.01" value="80">
    <label for="tip">Tip (%)</label>
    <input id="tip" name="tip" type="number" inputmode="numeric" min="0" max="100" value="15">
    <label for="people">People</label>
    <input id="people" name="people" type="number" inputmode="numeric" min="1" step="1" value="3">
    <button type="submit">Calculate</button>
    <div id="out" aria-live="polite">Press Calculate.</div>
  </form>

  <script>
    const form = document.getElementById('f');
    const out = document.getElementById('out');

    form.addEventListener('submit', (e) => {
      e.preventDefault();  // handled in the page, nothing is sent anywhere
      const d = new FormData(form);
      const bill = parseFloat(d.get('bill'));
      const tip = parseFloat(d.get('tip'));
      const people = parseInt(d.get('people'), 10);

      if (!(bill >= 0) || !(tip >= 0) || !(people >= 1)) {
        out.className = 'err';
        out.textContent = 'Enter a bill, a tip and at least 1 person.';
        return;
      }
      const total = bill * (1 + tip / 100);
      out.className = '';
      out.innerHTML = 'Total ' + total.toFixed(2) +
        '<br>Each pays <b>' + (total / people).toFixed(2) + '</b>';
    });
  </script>
</body>
</html>
A bill splitter. The form never leaves the page, because the submit handler calls preventDefault.

The preventDefault() call stops the browser from reloading the page to submit the form. The result is calculated from FormData and written into the page.

Embedding and the editor address

StackBlitz documents an embed address: an iframe whose src is the project address with ?embed=1 added. The docs also list parameters such as view (editor or preview), file and hideNavigation.

<iframe src="https://stackblitz.com/edit/angular?embed=1"></iframe>

The SDK can do the same from JavaScript with embedProject and open a project in a new tab with openProject. The SDK page describes both as ways to put a project in docs, example pages or blog posts.

Compare what each kind of address gives the person who opens it:

An editor address opens a workspace with files and preview. A page address opens the page itself.
An editor address opens a workspace with files and preview. A page address opens the page itself.
Reader wants Send
To read or change the code The project address
To use the page A page address
The page inside another page An embed, where iframes are allowed

When it does not work

What you see Cause Fix
Blank preview, console says null The script runs before the element exists Move the script to the end of body
confetti is not defined The library tag is missing, wrong or later Check the URL, call it inside a handler
Styling disappears after copying style.css was left behind Paste the CSS into a style tag
Import fails from a local file Module pages need http, not file Inline the module or serve the folder
Reader lands in an editor The editor address was shared Share an address that opens the page
Form reloads the page Default submit is not cancelled Call preventDefault() in the handler
Works in the editor, not alone The project needs a build Run the build, share the output

An editor address is right for someone who wants to read the code. For someone who only wants to use the page, a screenshot cannot be clicked. A playground compared with a plain page covers that choice.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it. The page renders as written and its scripts run, so the people you send it to can click and try it themselves.

Scripts and styles from cdnjs.cloudflare.com, cdn.jsdelivr.net, cdn.tailwindcss.com, code.jquery.com and unpkg.com load, so the confetti example works as it is. Anyone with the link can open it without an account. If you change the code later, the same link shows the new version.

Inside a NOS page, iframes are blocked, so an embedded StackBlitz project will not show there. Put the page itself in the document and link to the project from a line of text instead.

Questions people ask

Can StackBlitz run plain HTML without Node.js?

Yes. StackBlitz documents two environments. EngineBlock is built for front-end projects and cannot run a full Node.js application. WebContainers adds a Node.js environment. In the StackBlitz SDK, the html and javascript templates are EngineBlock ones, and node is the WebContainers one.

Is StackBlitz free for HTML projects?

The StackBlitz FAQ says the Personal Plan is a free account that allows unlimited public projects, and that the Personal+ Plan adds unlimited private projects. Plans change, so check the StackBlitz membership page before relying on this.

Which browsers does StackBlitz work in?

The StackBlitz browser-support page covers WebContainers, the Node.js environment. It lists full support in Chrome and most Chromium-based browsers, and beta support in Firefox and in Safari from 16.4. That page shows a last-update date of February 2023, so check it for the current state.

Can I load a library from a CDN in my HTML?

A script tag with a CDN address is ordinary HTML and works in any page that can reach the CDN. The second example above loads a library from jsDelivr with a version number written into the address.

How do I get a single HTML file out of a StackBlitz project?

Copy the contents of index.html. If the project also has style.css and script.js, paste their contents into a style tag and a script tag inside index.html first. A project built with npm and a bundler needs a build step first, which the section on build steps covers.

Keep reading