forms

Forms that work without a backend

Add one attribute to a form on your static site and every submission lands in your dashboard. No JavaScript, no embedded widget, no third-party service, and nothing for the person filling it in to sign up for.

Its markup, styles and deploy script are on GitHub — copy it, change the fields, redeploy.

index.html
<form harvis-form="contact">
  <input name="email" type="email" required>
  <textarea name="message"></textarea>
  <button>Send</button>
</form>
how it works

Three steps, and one of them is redeploying

There is no form builder to learn and no endpoint to copy. The attribute is the whole setup.

  1. 01

    Add the attribute

    Put harvis-form on any form in your HTML. Give it a name if the site has more than one. Leave action and method off — they get filled in when the page is served.

  2. 02

    Redeploy

    Drag the folder in again, or run npx harvis. Nothing to configure, no key to paste, no switch to flip in a dashboard first.

  3. 03

    Read what comes in

    Submissions appear under your site in the dashboard, newest first. Open a row to read the whole thing, or take the lot as a CSV.

the markup

The whole thing is two attributes

harvis rewrites the form on its way out of the server, so the page you wrote stays the page you wrote.

what you write
<form harvis-form="contact"
      data-harvis-redirect="/thanks.html">
  <input name="email" type="email" required>
  <button>Send</button>
</form>
what your visitors get
<form harvis-form="contact"
      data-harvis-redirect="/thanks.html"
      action="/__harvis/form/contact" method="post">
  <input type="hidden" name="_harvis_redirect" value="/thanks.html">
  <input type="text" name="_harvis_hp" tabindex="-1" aria-hidden="true" style="…">
  <input name="email" type="email" required>
  <button>Send</button>
</form>

The two attributes

harvis-form
Marks the form as one to collect. The value names it, so several forms on one site stay apart; leave the value off and it is collected under “default”.
data-harvis-redirect
Optional. The page on your site to send people to after they submit. Anything pointing off your site is ignored.
frameworks

When JavaScript builds the form, write the endpoint in

harvis fills the action in as the page leaves the server. A form that only exists once your bundle has run isn't in the page yet, so there is nothing to fill in — you write out what the rewrite would have written. It is still an ordinary HTML post, and everything else on this page still applies.

  • React
  • Vue
  • Svelte
  • Angular
ContactForm.jsx
<form action="/__harvis/form/contact" method="post">
  <input name="email" type="email" required />
  <textarea name="message" />

  {/* optional — where to land after sending */}
  <input type="hidden" name="_harvis_redirect" value="/thanks" />

  {/* optional — the decoy harvis would have added */}
  <input
    type="text"
    name="_harvis_hp"
    tabIndex={-1}
    autoComplete="off"
    aria-hidden="true"
    style={{ position: "absolute", left: "-9999px", opacity: 0 }}
  />

  <button>Send</button>
</form>
action + method
The endpoint harvis would have written, plus post. The last segment is the form's name — lowercase letters, numbers and dashes; anything else is collected under “default”.
_harvis_redirect
Optional, and the hand-written form of data-harvis-redirect. A path on your own site; anything pointing off it is ignored.
_harvis_hp
Optional, and the one thing you lose by writing the form yourself: the decoy that catches bots. Keep it off-screen rather than display:none, and leave it empty — anything that fills it in is dropped.
harvis-form
Leave it off. The attribute is a request for harvis to write the action, and you have just written it yourself.

Let the browser send it

No preventDefault, no fetch. The reply is a redirect the browser follows on its own, which is what keeps the form working when scripts don't.

Prerendered counts as JavaScript

If your build writes the form into the HTML and your app then takes it over in the browser, write the fields in anyway — the ones harvis inserted aren't in your component, so hydration can throw them away.

ai agents

Hand it to the agent that wrote your site

If an AI built the site, it can wire the forms up too. Copy the brief below into whatever has your project open — Claude Code, Cursor, Copilot, the chat you built the page in — and it will find the forms you already have and convert them. It covers both cases on this page, so you don't have to know which one you're in.

paste this into your ai agent
Wire the forms in this project up to harvis.dev (https://harvis.dev), which collects form submissions for static sites that have no backend. Read all of this before editing anything.

HOW IT WORKS
harvis serves every page of a site it hosts, so it can take a form post on the way past. Marking a <form> with the harvis-form attribute is the whole integration: as the page is served, harvis rewrites that tag to action="/__harvis/form/<name>" method="post", inserts a hidden honeypot field, and turns data-harvis-redirect into a hidden field. The form then posts natively, same-origin, with no JavaScript, no API key and no third-party service. Submissions appear in the site owner's dashboard (https://harvis.dev/dashboard) and are emailed to them.

STEP 1 — FIND THE FORMS
Look for every <form> in the project: .html files, and templates or components if a framework is in use. Skip search boxes and any form that posts to an API this project owns and expects a reply from. For each remaining form, decide which case it is:
- CASE A — the form is in the HTML that gets deployed: plain HTML, or a generator/framework that renders it at build time. This is the common case.
- CASE B — the form only exists once JavaScript has run: a React/Vue/Svelte/Angular component mounted in the browser, or markup a script writes. harvis rewrites the served HTML, so a form that is not in it yet is never rewritten. A form that is prerendered and then hydrated is CASE B too, because the fields harvis inserts are not in the component tree and hydration can discard them.

STEP 2A — CASE A: ADD THE ATTRIBUTE
- Add harvis-form="<name>" to the opening <form> tag. Name it for what it is — contact, signup, feedback. The name must match ^[a-z0-9][a-z0-9_-]{0,39}$ or it is collected under "default". Two forms on one site should not share a name unless they should share one list of submissions.
- Delete that form's existing action and method attributes. harvis overwrites both, so leaving them there only misleads whoever reads the file next.
- Optional: add data-harvis-redirect="/thanks.html" to choose where the visitor lands after sending. It must be a path on this same site and the file must exist in the deploy; anything pointing off-site is ignored. Without it, visitors get a plain harvis confirmation page.
- Do not add a honeypot, _harvis_hp or _harvis_redirect by hand — harvis inserts them, and a second copy is a bug.

STEP 2B — CASE B: WRITE THE ENDPOINT YOURSELF
- Set action="/__harvis/form/<name>" and method="post" on the form, and do NOT add harvis-form: that attribute is a request to rewrite, and here you have written the rewrite yourself.
- Optional redirect: a hidden input named _harvis_redirect whose value is a path on this site.
- Optional honeypot, since nothing will add one for you: an empty text input named _harvis_hp with tabindex -1, autocomplete off, aria-hidden true, positioned off-screen with position:absolute;left:-9999px rather than display:none.
- Let the browser submit it: no onSubmit handler, no preventDefault, no fetch. The reply is a 303 that the browser follows on its own, and intercepting it is what breaks the form when scripts fail.

STEP 3 — IN BOTH CASES
- Every field to be collected needs a name attribute; an input without one is never submitted. Those names become the column headings the owner reads, so prefer name, email and message over field1.
- Remove what is left of any other form service: a Formspree, Getform, Basin or FormSubmit action URL, Netlify's data-netlify attribute and its hidden form-name input, a Web3Forms access_key input, and any handler that POSTed the form somewhere else.
- Keep the client-side validation as it is. required, type="email", minlength and the rest all still work.
- File inputs are dropped: submissions are stored as text and files are not kept. If a form has one, say so rather than leaving it in silently.
- Add no script, SDK, key or config file. There is nothing to install.
- Limits, worth mentioning if a form is likely to meet one: 30 fields per submission, 5,000 characters per field, 64 KB per submission, 60 submissions per site an hour, 20 per visitor an hour, and the newest 1,000 per site are kept.

STEP 4 — DEPLOY AND REPORT
- The attribute only does anything on a served page, so deploy the site again: run npx harvis from the site folder, or tell the user to drag the folder onto https://harvis.dev/drop.
- Then tell the user which files changed, the name you gave each form, and that submissions arrive at https://harvis.dev/dashboard and by email — noting that a site deployed anonymously has no owner to email until it is claimed with the claim link.
- Suggest they send one test submission through the live site.

What it tells the agent to do

  • Find every form in the project and leave the search boxes and API calls alone.
  • Add harvis-form with a sensible name, and strip the action and method that harvis replaces anyway.
  • Write the endpoint by hand instead when the form only exists after JavaScript runs.
  • Clear out whatever the last form service left behind, and flag a file upload as something that won't be kept.
  • Redeploy, then tell you what changed and where the submissions land.

Or leave it in the repo

The same text works as a file — save it as AGENTS.md, CLAUDE.md or a project rule and an agent reads it on its own, so the next form someone adds is collected without anyone asking.

included

What you get

Everything below is on by default. There is no settings page for any of it.

  • No JavaScript

    The form posts the way HTML has always posted. It still works with scripts blocked, on a slow phone, and in a browser that renders text and nothing else.

  • Spam gets filtered

    Every form is served with a decoy field no person can see, and anything that fills it in is dropped without being told. Rate limits cap what one visitor, and one site, can send in an hour.

  • As many forms as you like

    Name them — a contact form and a signup form on the same site each keep their own list, and you can filter and export them separately.

  • Your own thank-you page

    Point the form at a page you wrote and that is where people land after sending. Leave it off and they get a plain confirmation page instead.

  • Export whenever you want

    One button gives you a CSV with a column for every field anyone has ever submitted, so a form that gained a question mid-life still opens as one table.

  • Nothing follows anyone around

    No tracking script, no cookie, no third party in the page. The visitor's address is hashed before it is stored, and only so the rate limit can tell two submissions apart.

limits

The limits

Generous for a contact form, tight enough that a script pointed at your site can't fill your inbox.

Fields per submission
30
Length of one field
5,000 characters
Size of one submission
64 KB
Submissions per site
60 an hour
Submissions per visitor
20 an hour
Kept per site
1,000, newest first
faq

Questions

What people ask before they put a form on a static site.

What is a static form?

A form on a site that has no server of its own. Normally that means the form has nowhere to send anything, which is why static sites usually borrow a third-party form service. harvis already serves every page of your site, so it can take the submission on the way past instead.

Do I need to know how to code?

You need to be able to add one word to a line of HTML. If an AI wrote your site, ask it to add the harvis-form attribute to the form — it knows how, because the instructions harvis publishes for AI assistants say so.

Does it work with JavaScript turned off?

Yes. That is the reason it is built this way. The form does an ordinary HTML post and the browser follows a redirect afterwards, exactly as forms worked before JavaScript existed.

Can I keep using my own endpoint or another form service?

Yes — just don't add the attribute. Forms without harvis-form are served exactly as you wrote them, action and all. On a form that does have it, harvis replaces the action, because adding the attribute is how you say where the submission should go.

Does it work with React, Vue or Svelte?

Yes, with one extra step. If the form is in the HTML your build produces, the attribute is all you need. If it only appears once JavaScript has run, write action="/__harvis/form/contact" and method="post" into the component instead — same endpoint, same dashboard, and the browser still does the posting.

Can people attach files?

Not yet. A file input on a collected form still works for the visitor, but the file is not stored — only the text fields are kept.

What happens to the submissions if I delete the site?

They go with it. Submissions belong to the site rather than to a deploy, so redeploying keeps them and deleting the site removes them for good. Export a CSV first if you want to keep them.

Put a site online and try it

Publishing is free and takes about two seconds. Add the attribute to a form, redeploy, and send yourself a test message.

Put a site online