# How to add a waitlist form in plain HTML to any landing page

> A complete no-build waitlist page in plain HTML that works with JavaScript off, shows an inline thank-you when it loads, and falls back when it must.

Published 2026-10-25 · https://ikiform.com/guides/waitlist/html

Write a normal HTML form that posts the email to your Ikiform endpoint and redirects to a thank-you page, so it works on any host with no JavaScript at all. Then add a small script that posts the same form with `fetch` and shows the thank-you inline, and hands back to the plain post whenever the script can't finish, such as when the human check is on.

A waitlist page doesn't need a framework, a build step or a server. Four static files and a form backend cover it, and the page loads faster than anything a builder exports. This page builds the whole thing, layer by layer, so each layer works on its own.

## What matters without a build step

- **The form is the integration.** A plain `<form>` with an `action` is the whole connection to Ikiform. No package, no API key, nothing to compile.
- **JavaScript is an upgrade, not a requirement.** The page should collect signups when the script is blocked, slow or broken. Each layer below adds to the one before it and falls back to it.
- **The script must never trap someone.** If `fetch` fails or Ikiform asks for a human check that `fetch` can't run, the script hands the form back to the browser to post the normal way.
- **Hosting is anywhere.** GitHub Pages, Netlify, Cloudflare Pages, an S3 bucket or a shared host all serve these files as they are.

| Layer | What it adds | If it's missing |
| --- | --- | --- |
| HTML form | Posts the email, redirects to `thanks.html` | Nothing works, so it comes first |
| CSS | A readable page in light and dark mode | A plain but working form |
| Script | Inline thank-you, field errors, no page change | The plain post and redirect |

Diagram: The three paths a signup can take on a plain HTML waitlist page

1. With JavaScript off, the browser posts the form, Ikiform redirects with 303 to your _redirect URL, and the visitor sees thanks.html.
2. With the script loaded, it posts with fetch and asks for JSON. Ikiform answers 201 or an error, and the script shows the result on the same page.
3. If fetch fails, or Ikiform answers challenge_required, the script calls form.submit().
4. That plain post runs Ikiform's human check page if needed and ends on thanks.html.

Every path ends with a stored signup. The script only changes how the visitor sees it.

## The files

### The page

`index.html`:

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Acme is almost here</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main>
    <h1>Acme is almost here</h1>
    <p>Invoices that chase themselves. Leave your email and we'll invite you first.</p>

    <form id="waitlist" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
      <label for="email">Email</label>
      <div class="row">
        <input id="email" name="email" type="email" autocomplete="email" required placeholder="you@company.com">
        <button>Join the waitlist</button>
      </div>
      <input type="hidden" name="_redirect" value="https://example.com/thanks.html">
      <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden>
      <p class="message" role="status" aria-live="polite"></p>
    </form>
  </main>
  <script src="waitlist.js" defer></script>
</body>
</html>
```

- `_redirect` sends the browser to `thanks.html` after a plain post. It only works for an `https:` URL on one of the form's **Allowed domains**, so add your domain there. **Redirect after submit** in the form's settings does the same without the hidden input.
- The `_gotcha` input is the honeypot. People never see it; bots that fill every input are stored as spam.
- `aria-live="polite"` makes screen readers announce the script's messages without moving focus.
- The script tag uses `defer`, so it runs after the form exists and never blocks the page from drawing.

### The enhancement

`waitlist.js`:

```js
const form = document.getElementById("waitlist");
const message = form.querySelector(".message");
const button = form.querySelector("button");
const key = crypto.randomUUID();

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  button.disabled = true;
  message.textContent = "Joining…";
  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json", "Idempotency-Key": key },
      body: new FormData(form),
    });
    const result = await response.json();
    if (result.ok) {
      form.replaceChildren(Object.assign(document.createElement("p"), {
        textContent: "You're on the list. We'll email you on launch day.",
        role: "status",
      }));
      return;
    }
    if (result.error.code === "challenge_required") {
      form.submit();
      return;
    }
    const wait = response.headers.get("Retry-After");
    message.textContent = wait
      ? `Too many tries. Wait ${wait} seconds and try again.`
      : result.error.fields?.email?.[0] ?? result.error.message;
  } catch {
    form.submit();
    return;
  }
  button.disabled = false;
});
```

- `Accept: application/json` makes Ikiform answer with JSON instead of a redirect, so the script can show the result in place.
- The idempotency key is made once per page load. A double click, or a retry after a slow answer, stores one signup.
- `challenge_required` means the form's spam check is set to **Ikiform** and `fetch` sent no token. `form.submit()` posts the plain way, which shows Ikiform's check page and then the thank-you page. It doesn't fire the `submit` event again, so there's no loop.
- A network error takes the same fallback. If Ikiform was unreachable, the plain post shows the browser's own error, which is still more honest than a stuck button.
- A `429` reads `Retry-After` and says how long to wait.

### The thank-you page

`thanks.html`:

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>You're on the list</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main>
    <h1>You're on the list</h1>
    <p>We'll email you the day Acme opens. Nothing before that.</p>
    <p><a href="/">Back to the page</a></p>
  </main>
</body>
</html>
```

### The styles

`style.css`:

```text
body { margin: 0; font: 16px/1.5 system-ui, sans-serif; color: #111; background: #fff; }
main { max-width: 34rem; margin: 0 auto; padding: 18vh 1rem 4rem; }
h1 { font-size: 2.25rem; line-height: 1.1; margin: 0 0 0.75rem; }
label { display: block; font-weight: 600; margin: 1.5rem 0 0.375rem; }
.row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
input[type="email"] { flex: 1 1 14rem; font: inherit; padding: 0.625rem 0.75rem; border: 1px solid #ccc; border-radius: 0.5rem; }
button { font: inherit; padding: 0.625rem 1rem; border: 0; border-radius: 0.5rem; background: #111; color: #fff; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: progress; }
.message { min-height: 1.5em; margin: 0.5rem 0 0; }
@media (prefers-color-scheme: dark) {
  body { color: #eee; background: #111; }
  input[type="email"] { background: #1b1b1b; color: inherit; border-color: #444; }
  button { background: #eee; color: #111; }
}
```

> If the page is on a different domain from `example.com`, update `_redirect` and the link back. Ikiform ignores a `_redirect` it can't follow and shows its own hosted thank-you page instead, so a mismatch never loses a signup.

## Deploy and test

Put the four files in one folder and upload it to any static host. Then add the host's domain under **Allowed domains**: for example `username.github.io`, `your-site.netlify.app` or your own domain. While you work locally, add `localhost` as well; serve the folder with any static server, since a page opened as a `file://` URL sends no origin.

## Check it works

1. Under the form's **Allowed domains**, add your site's domain, plus `localhost` while you develop. Leave it empty to accept posts from anywhere.
2. Turn JavaScript off in the browser and sign up. You land on `thanks.html`.
3. Turn it back on and sign up again. The form swaps for the thank-you line without leaving the page.
4. Set **Spam check** to **Ikiform** and sign up once more. The page moves to the check, then to `thanks.html`.
5. Open **Submissions** in the dashboard. The new submission is there with every field you sent.
6. Post with the honeypot filled in, `_gotcha=bot`. It still gets a success answer, but the submission is stored as spam and sends no email or webhook.

## Gotchas

| Problem | What to do |
| --- | --- |
| The browser lands on Ikiform's thank-you page instead of yours | `_redirect` is ignored unless its domain is in **Allowed domains** and it is an `https:` URL. Add the domain, or set **Redirect after submit** in the form's settings instead. |
| `403 origin_not_allowed`, or every post lands in spam | The page's domain isn't in the form's **Allowed domains**. Add it, plus `localhost` for local tests. Subdomains of a listed domain are allowed. |
| Posts from a page opened as a file are refused or marked spam | A `file://` page sends no origin. Serve the folder with a local static server and add `localhost` to **Allowed domains**. |
| The thank-you line never shows, but signups arrive | The script didn't load: check the path in the `src` attribute, or that the host serves `.js` files. The plain post still works, which is why signups arrive. |
| `402 test_limit_reached` | The workspace has no plan and used its 50 free `localhost` tests this month. Pick a plan, or use **Send test** in the dashboard. |

The [HTML forms docs](https://ikiform.com/docs/html.md) list every reserved field, and the [waitlist template](https://ikiform.com/forms/waitlist) generates a version with name and role. Exporting the list and the launch email are in the [main guide](https://ikiform.com/guides/waitlist.md#launch-day).

## Questions

### Can a waitlist form work without JavaScript?

Yes. A plain HTML form posts the email to your Ikiform endpoint and Ikiform redirects the browser to your thank-you page. JavaScript only adds the inline message.

### Where can I host a plain HTML waitlist page?

Anywhere that serves static files: GitHub Pages, Netlify, Cloudflare Pages, S3 or a shared host. The form posts to Ikiform, so the host never runs any code.

### What happens if the human check is on and the script posts with fetch?

Ikiform answers 403 challenge_required. The script in this guide catches that and submits the form the plain way, which shows the check page and then your thank-you page.

### Does Ikiform host the form page for me?

No. Ikiform has no hosted form pages. You put the form on your own site, in any builder or framework, and point it at your Ikiform endpoint.

### Is Ikiform free?

No, there is no free plan. Testing is free: you can send 50 test submissions a month from localhost before you pay. To go live, Pro starts at $6/mo for 500 submissions, or you can pay once for 10,000 submissions for $49 or 50,000 submissions for $149, with credits that never expire.

## More guides

- [Waitlist for a new SaaS](https://ikiform.com/guides/waitlist.md): Build a pre-launch waitlist on your own landing page: the form, spam protection, duplicates, confirmation emails and a clean export on launch day.
- [Next.js waitlist](https://ikiform.com/guides/waitlist/nextjs.md): A Next.js App Router waitlist: a server action that forwards signups with a secret key, useActionState, an optimistic thank-you and a per-visitor limit.
- [Waitlist on Carrd](https://ikiform.com/guides/waitlist/carrd.md): Collect waitlist signups on a Carrd page: a Send to URL form on Pro Plus with UTM tracking, or an embedded HTML form with a honeypot on Pro Standard.
- [Product Hunt launch list](https://ikiform.com/guides/waitlist/product-hunt.md): Collect a launch list weeks before Product Hunt, tag each signup by channel, and send the launch-day email in two waves timed to the Pacific Time reset.
- [Beta signup with screening](https://ikiform.com/guides/waitlist/beta-signup.md): A beta application form with screening questions the server enforces, and a script that scores applicants and picks each cohort from the list.
- [Mobile app waitlist](https://ikiform.com/guides/waitlist/mobile-app.md): A pre-launch waitlist for an iOS or Android app that asks which phone people use, sends iPhone testers to TestFlight and fills a Play closed test.
