Publishing a design as a real website is the easy part. Making the contact form deliver messages is the part nobody mentions — here is the embed-friendly HTML that works, plus the iframe gotcha that catches everyone.
TL;DR
A published Figma site is static frontend output, so a designed contact form has no way to deliver messages. Embed a real HTML form that posts to a FormsList endpoint — action="https://formslist.com/f/YOUR_HASH" method="POST". Because embedded HTML usually runs in an iframe, use fetch() with Accept: application/json for an in-place success message, or add target="_top" if you want the plain form to navigate the whole page. Free plan: 5 forms, 500 submissions/month, email notifications included.
A contact form drawn in a design tool is a picture of a form: rectangles, labels, a button, maybe an interactive prototype state that advances to a thank-you frame. When that design is published as a website, you get real HTML, CSS, and JavaScript — but publishing does not conjure the half of a contact form that lives on a server, because that half was never part of the design. Delivering a message requires a server listening at a public URL to receive the POST, durable storage so the payload survives the request, and an authenticated mail relay with a verified sending domain. A published site is static output served from a CDN. There is no server-side process to run, nowhere to put an SMTP password that visitors could not read, and no database. The practical consequence is a form that either does nothing when clicked, or advances to a designed "Thanks!" state that is purely visual. Prototype interactions are the sharpest version of this trap, because the thank-you screen genuinely appears — it just has no relationship to whether any data was transmitted. Visitors leave believing they contacted you.
The missing piece is a URL that is always up, accepts POST requests, keeps every submission, filters spam, and emails you. FormsList provides one, and you can create it from the dashboard after a normal signup — or skip the signup entirely. A single POST to the instant provisioning endpoint, giving the email address that should receive submissions and a name for the form, returns a live endpoint straight away: no account, no OAuth, no credit card. The response contains the endpoint URL, a paste-ready HTML snippet, and a claim link, and that email address gets a claim message so the site owner can take over the dashboard and read submission history later. Unclaimed forms are deleted after 7 days, so claim it once the site is real. Copy the endpoint URL somewhere handy — it is the only piece of configuration in this entire guide, and everything below is the same markup with your hash substituted in.
curl -X POST https://formslist.com/api/v1/instant \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","form_name":"Contact"}'
# Response:
# endpoint -> https://formslist.com/f/YOUR_HASH
# html_snippet -> paste-ready markup
# claim_url -> claim the dashboard (also emailed)
# expires_at -> unclaimed forms are deleted after 7 daysFigma Sites can publish embedded custom code alongside your designed layout, which is where a functioning form goes. Rather than trying to make the drawn rectangles submit anything, place an embed where the form should appear and put real markup inside it. Because embedded code is styled independently of your design system, use inline styles so the result matches your page instead of inheriting browser defaults. Set the font family, colours, border radius, and spacing to the values from your design. It is worth being fussy here: the embedded form is the one part of the page that will not automatically look like the rest of it, and a form with default browser styling in the middle of a considered layout is immediately noticeable. Every input needs a name attribute — that is what becomes the field name on the submission. Fields without a name are simply not sent, which is the single most common reason a form appears to work but arrives half empty. Include a hidden input named _gotcha as a honeypot, and give the form a max-width so it does not stretch awkwardly on wide screens.
<form action="https://formslist.com/f/YOUR_HASH" method="POST"
style="max-width:520px;font-family:Inter,system-ui,sans-serif;">
<label style="display:block;margin-bottom:6px;font-size:14px;font-weight:600;color:#111;">Name</label>
<input type="text" name="name" required
style="width:100%;padding:12px;margin-bottom:16px;border:1px solid #ddd;border-radius:8px;font-size:15px;box-sizing:border-box;" />
<label style="display:block;margin-bottom:6px;font-size:14px;font-weight:600;color:#111;">Email</label>
<input type="email" name="email" required
style="width:100%;padding:12px;margin-bottom:16px;border:1px solid #ddd;border-radius:8px;font-size:15px;box-sizing:border-box;" />
<label style="display:block;margin-bottom:6px;font-size:14px;font-weight:600;color:#111;">Message</label>
<textarea name="message" rows="5" required
style="width:100%;padding:12px;margin-bottom:16px;border:1px solid #ddd;border-radius:8px;font-size:15px;box-sizing:border-box;"></textarea>
<!-- Honeypot: hidden from humans, filled in by bots -->
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none" />
<button type="submit"
style="width:100%;padding:14px;background:#111;color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;">
Send message
</button>
</form>Embedded custom code on a published site normally runs inside an iframe. That is a security boundary, and it changes what a plain form submission does: instead of navigating the page your visitor is looking at, the browser navigates the iframe. FormsList responds to a browser-style POST with a 303 redirect to a thank-you page, so what the visitor sees is a small rectangle in the middle of your beautiful layout suddenly showing a different page. It technically worked. It looks broken. Option one, and the better default, is to submit with JavaScript and never navigate at all. Send the request with fetch and an Accept: application/json header — that header is what makes FormsList reply with JSON containing ok: true and a submission id rather than a redirect — then replace the form's contents with your own thank-you message. The iframe stays exactly where it is and the visitor sees a clean inline confirmation. Option two, if you prefer no JavaScript, is to add target="_top" to the form element. That tells the browser to apply the navigation to the top-level page rather than the frame, so the visitor is taken to a proper full-page thank-you. Pair it with a hidden input named _next pointing at a thank-you page on your own site so they land somewhere that looks like yours. Pick one of the two — a plain form inside an iframe with neither is the combination that produces the odd-looking result.
<form id="contact" action="https://formslist.com/f/YOUR_HASH" method="POST"
style="max-width:520px;font-family:Inter,system-ui,sans-serif;">
<input type="text" name="name" placeholder="Name" required
style="width:100%;padding:12px;margin-bottom:16px;border:1px solid #ddd;border-radius:8px;box-sizing:border-box;" />
<input type="email" name="email" placeholder="Email" required
style="width:100%;padding:12px;margin-bottom:16px;border:1px solid #ddd;border-radius:8px;box-sizing:border-box;" />
<textarea name="message" rows="5" placeholder="Message" required
style="width:100%;padding:12px;margin-bottom:16px;border:1px solid #ddd;border-radius:8px;box-sizing:border-box;"></textarea>
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none" />
<button type="submit"
style="width:100%;padding:14px;background:#111;color:#fff;border:none;border-radius:8px;font-weight:600;cursor:pointer;">
Send message
</button>
</form>
<script>
var form = document.getElementById("contact");
form.addEventListener("submit", async function (e) {
e.preventDefault();
var button = form.querySelector("button");
button.disabled = true;
button.textContent = "Sending...";
try {
var res = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" }, // JSON back, no navigation
body: new FormData(form)
});
if (!res.ok) throw new Error("Request failed");
form.innerHTML =
'<p style="font-family:Inter,system-ui,sans-serif;font-size:16px;color:#0a7d3f;">' +
"Thanks — we got your message and will reply soon.</p>";
} catch (err) {
button.disabled = false;
button.textContent = "Send message";
alert("Something went wrong. Please try again.");
}
});
</script>
<!-- No-JavaScript alternative: navigate the whole page instead of the frame
<form action="https://formslist.com/f/YOUR_HASH" method="POST" target="_top">
...
<input type="hidden" name="_next" value="https://your-domain.example.com/thanks" />
</form>
-->Your site exists at more than one address: an in-editor preview while you work, a published URL on a provided subdomain, and your own domain once you connect one. Embedded code can behave differently in the editor preview than on the published site, so the published site is the one to trust. The usual reason a form works in one place and not another is cross-origin blocking. Browsers refuse a cross-origin POST unless the receiving server explicitly permits the requesting origin, and an embedded iframe frequently has a different origin from the page containing it. Self-hosted backends need every one of those origins allowlisted, which is tedious and easy to get wrong. FormsList endpoints accept cross-origin POSTs from any origin, so the same endpoint works from the editor preview, the published subdomain, your custom domain, and from inside the embed's iframe with nothing to configure. If you later enable the allowed-domains restriction in form settings, do it after your custom domain is live and list every origin you actually submit from — including the embed origin — or you will block your own form.
Spam protection: the hidden _gotcha input is a honeypot. Bots fill in every field they can find, so a submission arriving with it populated gets flagged; humans never see it because it is display:none. There is no third-party script to load, no checkbox and no image puzzle, so it costs your visitors nothing in speed or friction — which matters more on a design-led site than almost anywhere else. Automatic filtering runs alongside it on every plan, free included. Endpoints also accept reCAPTCHA, Turnstile, and hCaptcha tokens if you want a visible challenge. Email notifications: included on every plan, free included. Messages are never held in a dashboard pending an upgrade. Every submission is stored as well as emailed, so a filtered notification does not mean a lost enquiry. File uploads: add enctype="multipart/form-data" to the form and include a file input; a fetch call using FormData sets the encoding automatically. Free accepts 1 file up to 2MB per submission, paid plans up to 5 files at 4MB each. Useful for a portfolio-style site where people send a brief or a reference image with their enquiry.
<!-- Uploads: multipart encoding + a file input -->
<form action="https://formslist.com/f/YOUR_HASH" method="POST"
enctype="multipart/form-data" target="_top" style="max-width:520px;">
<input type="text" name="name" placeholder="Name" required style="width:100%;padding:12px;margin-bottom:16px;box-sizing:border-box;" />
<input type="email" name="email" placeholder="Email" required style="width:100%;padding:12px;margin-bottom:16px;box-sizing:border-box;" />
<textarea name="message" rows="5" placeholder="Tell us about the project" required style="width:100%;padding:12px;margin-bottom:16px;box-sizing:border-box;"></textarea>
<input type="file" name="brief" style="margin-bottom:16px;" />
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none" />
<button type="submit" style="width:100%;padding:14px;background:#111;color:#fff;border:none;border-radius:8px;font-weight:600;cursor:pointer;">Send</button>
</form>Publish, then submit a real test message from the published site rather than the editor preview — the embed environment is what you are actually testing, and it is only fully itself once published. Check the FormsList dashboard and your inbox. Present in the dashboard but missing from email means the notification address is wrong or the message was filtered; check spam before assuming a bug. If nothing arrives, the most common causes are an input missing its name attribute (that field is silently dropped), a typo in the endpoint hash producing a 404, or a form provisioned instantly and never claimed within the 7-day window. Error responses include an error code plus a fix field describing the corrective action, so opening the network tab and reading the response body is usually faster than guessing. A GET request to your endpoint returns JSON usage documentation for that specific form — accepted content types, expected fields, an example request — which is the quickest way to confirm the endpoint is alive before you go hunting through embed settings. Broader reference lives at formslist.com/docs, the programmatic API at formslist.com/form-api, and formslist.com/static-site-forms covers the same pattern for any site published as static files.
# Confirm the endpoint is live
curl https://formslist.com/f/YOUR_HASH
# Send a test submission
curl -X POST https://formslist.com/f/YOUR_HASH \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"name":"Test","email":"test@example.com","message":"Verifying the embed"}'Ask Claude for a landing page and you get one, contact form included — but that form has no backend, so submissions go nowhere. Here is the fix, including the version you can paste straight into your prompt.
Learn morev0 generates a beautiful contact form — and then nothing happens when someone submits it. Here is why the generated form has no backend, and how to make it actually deliver messages to your inbox in about two minutes.
Learn moreBolt.new scaffolds a whole project in the browser and the contact form looks done — until you deploy and discover nothing is being delivered. Here is why, and the fix that needs no server, no SMTP account, and no extra dependency.
Learn moreAdd a fully functional contact form to any static site generator — Jekyll, Hugo, Eleventy, Astro, or plain HTML. No server-side code required.
Learn moreSet up your form backend in under a minute. No server required, no complex configuration — just a simple endpoint for your forms.