# Add a waitlist form to any HTML website > Three ways to add a LaunchList form to a hand-written HTML site: the inline embed, enhancing your own form, or a native form that needs no JavaScript at all. Source: https://getlaunchlist.com/help/docs/integrations/html --- A hand-written site gives you the most choice, because nothing is stripping your markup. Pick by how much control you want. ## Option A: the inline embed Least work. The form takes the design you pick in the form editor, under **Design**, and updates when you publish a change to it. ```html
``` The script goes in the `` **once**. The `
` can appear as many times as you like. ## Option B: enhance your own form Keep your markup and your CSS. Add one attribute and we take over the behavior: submission, live format validation, a one-tap fix for email domain typos, inline errors, referral attribution and success routing. The one thing this path does not do is the on-blur deliverability check, which is only in our own rendered form. Everything the server enforces at submit is identical either way. ```html
``` Match each input's `name` to the field key from your form editor so its value lands in that field and is validated. For a simple waitlist the identity input must be `name="email"`, or the signup cannot be attributed. The loader still goes in the `` once. A survey on your own site asks for an email too, so its answers belong to someone: give your survey form an `email` input, required. The status page never asks for it, because the person answering there has already signed up. If the loader script doesn't load, the form still posts to LaunchList and the signup is saved, like Option C. With **Message** chosen under **After signup**, that fallback returns the visitor to your page without the message. This is the right choice when the form belongs to a designed page and an isolated embed would look like a transplant. > [!NOTE] Extra fields are kept, within limits > Inputs your form does not define are captured too, up to 20 per submission: they appear as > **Input data** columns in your audience rather than being discarded, with values truncated at > 2,000 characters. Give each a plain `name` (letters, digits, spaces, `_` and `-`); dots and > brackets are dropped. Declared fields still need matching `name`s to fill and validate, and the > identity field's name must match for the signup to attribute. ## Option C: no JavaScript at all A native form posting straight to the capture endpoint. The browser submits it, our server redirects to your Status page or a URL you choose. ```html
``` Nothing to load, nothing to break. This works with JavaScript disabled, behind strict content security policies, and inside templates that strip scripts. Copy the `action` from **Embed & API โ†’ Your website** rather than typing it. It points at the embed host, the same origin the loader script comes from, and the two matching is what makes the next tip work. > [!TIP] Add the loader anyway if you can > A native form cannot read `?ref=` from the URL, so referrals do not credit. Drop the loader in your > `` and it finds the form by its `action` and injects the referral code and UTM parameters as > hidden fields. It never intercepts the submission: the browser still posts the form. > > It only claims forms whose `action` is on **its own origin**. Point the form at a different host of > ours and the script ignores it, so referrals quietly stop crediting with nothing in the console to > tell you. > [!WARNING] Option C needs somewhere to land > The server answers a no-JavaScript post with a redirect, to your Status page or to your own URL. A > form whose **After signup** is **Message** has neither, so the visitor is sent back to the page > they came from with nothing confirming the signup worked. Choose **Status page** or **Link** (your > own page, on the Collect plan and above) for this method, or use Option A or B, which show the > message in place. ## Reading the answer yourself If your own script posts to the capture endpoint with `Accept: application/json`, a signup shown in place answers with `success.heading` and `success.message`. Both are already filled in for that person and in their language, so a `{{first_name|there}}` in your message arrives as their name, or as "there". Show them as they are. Someone who was already on the list gets one neutral line instead, and nothing about their place in line. ## CAPTCHA If your project has CAPTCHA set to Test or On, every option that runs the loader shows it. Options A and B draw the challenge for you, and so does Option C when the loader is on the page. In your own form (B, or C with the loader) it goes just above your submit button, or under the row that holds it. To place it yourself, add an empty element with `data-launchlist-captcha` inside the form: ```html
``` Option C without the loader cannot show a challenge. Those signups still go through; they just arrive unverified, which counts as a small risk signal rather than a refusal. Your CAPTCHA key only works on hostnames you list in your provider account, so add the domain this form lives on there too. ## Content Security Policy If your site sets a CSP, Options A and B need the embed origin allowed. The runtime injects its own stylesheet into each shadow root and loads the product typeface from that origin: ``` script-src 'self' https://embed.getlaunchlist.com; connect-src 'self' https://embed.getlaunchlist.com; style-src 'self' 'unsafe-inline'; font-src 'self' https://embed.getlaunchlist.com; ``` `style-src 'unsafe-inline'` is what lets the embed style itself. With Option A those styles live inside the shadow root and cannot reach the rest of your page. Option B adds one small stylesheet to your page head, defining `.launchlist-error` and `.launchlist-suggestion` so its inline error lines are legible; override either class from your own CSS. With CAPTCHA on, also allow your provider's script and frame host: `challenges.cloudflare.com` for Turnstile, `www.google.com` and `www.gstatic.com` for reCAPTCHA, or `js.hcaptcha.com` and `hcaptcha.com` for hCaptcha, in both `script-src` and `frame-src`. If your form's success message shows an image, add an `img-src` entry for wherever that image is hosted, or the message renders without it. If your form uses a font pairing picked under **Design** (or, on a form made before designs, a brand typeface other than the default DM Sans), the embed loads it from Google Fonts, so add those two hosts as well: ``` style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://embed.getlaunchlist.com https://fonts.gstatic.com; ``` Option C needs no CSP change at all, since it loads nothing. ## Adding social proof A live counter beside the form usually lifts conversion more than any copy change: ```html
``` Counter, leaderboard, milestone and countdown are all available. They show aggregate numbers only, never anybody's details. ## If something is not working | What you see | What to do | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Nothing renders | Check the script tag loaded, and that the form's public ID is right. Errors are logged to the browser console. | | A field lands in the wrong place | To fill a specific form field, its input `name` must match that field's key, not its label. Inputs the form does not define are kept as **Input data** columns, not lost. | | Referrals never credit | On a native form, load the script so it can inject the referral code, or send `ref` yourself. If the script is already loaded, check the form's `action` is on the same host the script came from: it only claims forms on its own origin. | | A no-JS submit goes nowhere useful | The form's success behavior is **Message**, which gives the server no URL to redirect to. In the form editor, under **After signup**, switch it to **Status page** or **Link** (on the Collect plan and above), then publish. | | Styles look wrong | The embed is in a shadow root and ignores your CSS by design. Restyle it in the form editor, under **Design**. | ## Using the classic LaunchList dashboard A waitlist made in the classic LaunchList dashboard has its own snippet: a `
` with `widget.js` in the ``, a form posting to `getlaunchlist.com/s/` with its form key (with `widget-diy.js`), or the popup form with `popup.js`. That snippet keeps working for now and sends signups to the classic dashboard's waitlist, not to a project here. A classic form key does not work with the snippet on this page. To bring that waitlist here, [import it into a project](/help/docs/getting-started/import-from-v1), then replace the classic snippet on your HTML site with the one from **Embed & API โ†’ Your website**. Your signups are copied, and the classic waitlist is not changed. --- Next: [every way to integrate](/help/docs/integrations/overview) ยท [the REST API](/help/docs/api/contacts-create) ## Common questions ### How do I add a waitlist form to a static HTML site? Add one script tag to the head and one div where the form should appear. The form renders itself, isolated from your CSS, and updates when you change its fields in LaunchList. ### Can I use a waitlist form without any JavaScript? Yes. Write a normal HTML form posting to the capture endpoint. The browser submits it and our server redirects to your result page, so it works with JavaScript disabled entirely. ### How do I keep my own form styling? Use the enhance method. Add data-launchlist-enhance to your existing form and we take over submission, validation, errors and referrals while your markup and CSS stay untouched.