HTML Form Generator

Form markup with real labels, the autocomplete tokens browsers actually read, the right inputmode per field, and an error pattern that announces itself.

Enable JavaScript to customise; default output below.

Method

post for anything that changes something. get puts the values in the URL, which is right for a search and wrong for a sign-up.

A legend names the group for a screen reader. It is the one piece of form markup that has no visual substitute.

Say what happens. "Submit" is the default nobody chose.

Fields
  1. Used for the name attribute and the id.

    Associated with aria-describedby, so it is announced after the label rather than instead of it.

The kind sets the input type, the autocomplete token and the mobile keyboard, which is most of what makes a form pleasant to fill in on a phone.

Live preview form.html
<!--
	Form markup.

	Four things carry most of the value here:
	  - a real <label for>, which is clickable, visible and translated
	  - autocomplete tokens from the HTML spec, which are what let a browser or a
	    password manager fill the form correctly
	  - inputmode and type, which decide the keyboard on a phone
	  - novalidate off: the browser's own validation is better than most of what
	    gets written to replace it
-->
<form class="form" action="/subscribe" method="post">
	<!--
		Error summary. role="alert" announces it when it appears, which is why it is
		in the markup from the start and empty rather than inserted on submit.
	-->
	<div class="form-errors" role="alert" aria-live="assertive" hidden>
		<h2>Please check the form</h2>
		<ul></ul>
	</div>

	<fieldset>
		<legend>Sign up for the newsletter</legend>

		<div class="field">
			<label for="email">Email address <span aria-hidden="true">*</span></label>
			<input
				type="email"
				autocomplete="email"
				inputmode="email"
				spellcheck="false"
				id="email"
				name="email"
				required
			/>
			<p class="error" id="email-error" hidden></p>
		</div>

		<!--
			Honeypot. Hidden with CSS rather than type="hidden", because a bot filling
			every field will fill a hidden input too. The label tells a screen reader
			user to leave it, and tabindex="-1" keeps it out of the tab order.

			Reject the submission server side when it has a value. This stops the
			simple bots and nothing else; it is not a substitute for a rate limit.
		-->
		<div class="field honeypot" aria-hidden="true">
			<label for="website-url">Leave this field empty</label>
			<input type="text" id="website-url" name="website_url" tabindex="-1" autocomplete="off" />
		</div>

		<button type="submit">Subscribe</button>
	</fieldset>
</form>

<style>
.field {
	margin-block-end: 1rem;
}

.field label {
	display: block;
	margin-block-end: 0.25rem;
	font-weight: 600;
}

.field input,
.field select,
.field textarea {
	/* 16px or more: iOS zooms the page for anything smaller, and the layout goes
		with it. */
	font: inherit;
	font-size: max(1rem, 16px);
	width: 100%;
	padding: 0.5rem 0.75rem;
	border: 1px solid #767676;
	border-radius: 4px;
}

.field-checkbox {
	display: flex;
	align-items: center;
	gap: 0.5rem;
}

.field-checkbox input {
	width: auto;
	/* A tap target of at least 24 by 24 CSS pixels, which is WCAG 2.5.8. */
	min-width: 24px;
	min-height: 24px;
}

.hint {
	margin-block: 0.25rem 0;
	font-size: 0.875rem;
	color: #545454;
}

.error {
	margin-block: 0.25rem 0;
	font-weight: 600;
	color: #b3261e;
}

.form-errors {
	margin-block-end: 1.5rem;
	padding: 1rem;
	border-inline-start: 4px solid #b3261e;
	background: #fdecea;
}

/* Never colour alone: the border and the message carry it too, for anyone who
	cannot see the red. */
[aria-invalid='true'] {
	border-color: #b3261e;
	border-width: 2px;
}

/* Off screen rather than display:none, so a bot that reads the CSS is less
	likely to notice. */
.honeypot {
	position: absolute;
	left: -9999px;
	width: 1px;
	height: 1px;
	overflow: hidden;
}
</style>

Output is valid and updates as you type.

Most of what makes a form pleasant to fill in is four attributes, and they are the four most often left out.

A real <label for>, which is clickable, visible and translated. The autocomplete token from the HTML spec, which is what lets a browser or a password manager fill the field correctly. inputmode and type, which decide the keyboard on a phone. And aria-describedby for the hint, so it is announced after the label rather than instead of it.

This generates the markup with all of them, per field kind, plus an error pattern that announces itself and a honeypot that does not trap screen reader users.

How to use

  1. Set the action, the method and the legend.
  2. Add a field per row, choosing the kind: it sets the type, the autocomplete token and the keyboard.
  3. Keep the error pattern unless you have one already.
  4. Copy the markup and the CSS.

Example

<form class="form" action="/subscribe" method="post">
	<fieldset>
		<legend>Sign up for the newsletter</legend>

		<div class="field">
			<label for="email">Email address <span aria-hidden="true">*</span></label>
			<input
				type="email"
				autocomplete="email"
				inputmode="email"
				spellcheck="false"
				id="email"
				name="email"
				required
				aria-describedby="email-hint"
			/>
			<p class="hint" id="email-hint">We send one email a month and nothing else.</p>
			<p class="error" id="email-error" hidden></p>
		</div>

		<button type="submit">Subscribe</button>
	</fieldset>
</form>

autocomplete="email" is the line that matters most. It is why the browser offers the address it already knows, and a form without it makes everybody type what their phone could have filled in.

Pitfalls

A placeholder is not a label. It disappears when the user types, it fails contrast requirements in most implementations, and a form full of placeholders is unfillable the moment somebody is interrupted. Use both if you like; never the placeholder alone.

autocomplete tokens are a fixed list. email, name, given-name, tel, postal-code, cc-number, current-password, new-password, one-time-code. Making one up, or writing autocomplete="on", gets you nothing. The full list is in the HTML specification and it is shorter than you think.

autocomplete="off" is mostly ignored, and it is usually wrong anyway. Browsers deliberately disregard it on login forms because password managers make people safer. The one place it earns its keep is a field that genuinely should never be remembered, such as a one-time code, and there autocomplete="one-time-code" is better still.

current-password and new-password are different tokens. Getting them the right way round is what makes a password manager offer to save the new password rather than fill in the old one.

16px minimum on inputs. iOS zooms the page in for anything smaller, and the layout goes with it. font-size: max(1rem, 16px) keeps it responsive and immune.

type="number" is for quantities, not for numbers. A card number, a phone number and a postcode are not quantities: they can have spaces, leading zeros and letters, and the spinner is nonsense on all three. Use type="text" with inputmode="numeric".

A <legend> has no visual substitute. It names the group for a screen reader. A heading above the fieldset looks the same and is not associated with anything.

Colour alone is not an error state. The generated CSS uses a thicker border, a message and aria-invalid together. Red on its own fails for the eight percent of men with a colour vision deficiency.

A honeypot hidden with type="hidden" catches nothing. Bots skip hidden inputs and fill visible ones, so the field has to be visible to the parser and invisible to people. It is also why the generated one carries a label and tabindex="-1": a screen reader user should be told to leave it alone rather than find an unlabelled field.

Validate on the server too. Everything here is a convenience for the person filling the form in. required and pattern are trivially bypassed, and a form that trusts them is a form with a vulnerability.

Compatibility

Everything runs in the browser: nothing is uploaded and nothing is stored.

The autocomplete tokens are from the HTML specification’s autofill section and are implemented by Chrome, Safari, Firefox and Edge, plus the major password managers. The inputmode attribute has been in every mobile browser since around 2019 and is ignored harmlessly on a desktop.

The error pattern uses role="alert" with aria-live="assertive" on an element that is in the markup from the start and empty. That matters: a live region inserted at the same moment as its content is often not announced, because the screen reader never saw the empty container to start watching it.

aria-invalid="true" on the field plus the message inside the element aria-describedby points at is the combination that gets announced when focus lands on a bad field. Setting one without the other gets you half of it.

The generated CSS uses logical properties, margin-block-end and border-inline-start, which work in every current browser and behave correctly in a right-to-left layout.

Frequently asked questions

Should I use the browser’s own validation?
Yes, as the first layer. It is free, it is localised, and it works before any JavaScript loads. Add your own messages where the built-in one is unhelpful, and keep the server-side check regardless.
Where do the error messages come from?
Your own script or your server. The markup is the shape: the message element, the aria-invalid flag and the summary. Filling them in is application code, and the comments in the output say what each part expects.
Is a honeypot enough against spam?
It stops the unsophisticated bots, which is most of the volume, and nothing else. Pair it with a rate limit and a timing check, which are both server-side and neither of which annoys a real person. Reach for a CAPTCHA last: it costs real users more than it costs spammers.
How do I lay this out in two columns?
CSS grid on the form and grid-column: span 2 on the fields that should be full width. Keep the DOM order the reading order, because that is the tab order.
Do I need a fieldset for a single field?
No. One field does not make a group. A <legend> earns its place when several fields belong together, such as an address or a set of radio buttons.
Weekly drops

New tools, when there are new tools

One email when something worth using ships. No schedule to fill, so no filler.

Your address goes nowhere else, and one click unsubscribes.