Form Fields covered the controls one by one. This page puts them together. It works through one form, a trail report for the Trail Notes site, and builds it up from bare controls to a finished pattern: labels that name each control, groups that name each set of related controls, messages tied to the controls they describe, and a small custom element, <form-field>, that holds a label, a control and its message together without a single <div>. The pattern defined here is the one every later page in the section reuses.
1. Four readers of every form
A sighted person looking at a form reads its structure from the layout: this text sits above that box, so it must be the box's name; these three round buttons are boxed together under a heading, so they answer one question. Nobody else gets the layout. Everyone else reads the markup.
- Assistive technology reads the accessibility tree the browser builds from the HTML. A screen reader landing on a text box announces its accessible name, its role, its state (required, invalid) and any description. If the name is missing, the person hears "edit text, blank" and has to guess.
- Voice control users say what they see: "click Email." That works only if the visible text and the accessible name match.
- Autofill and password managers read
name,id,autocompleteand the label text to decide what goes where. A well-labeled form fills itself in; a vague one gets the phone number in the name box. - The server reads only the submitted name and value pairs. It never sees a label, a heading or a layout, which is why every control that matters needs a
name, as The form and its request showed.
Organizing a form is the work of making the markup say what the layout shows. The tools are few and old: <label>, <fieldset> with <legend>, <output>, and the order of elements in the source. The diagram shows which reader each one serves.
2. Labels: the contract
Every control a person fills in needs a <label>. The label is the control's visible name and, in almost every case, its accessible name too. It also does a job sighted mouse and touch users notice: clicking or tapping the label focuses the text box, or ticks the checkbox, so the target is bigger than the control alone.
Explicit labels
An explicit label points at its control. The label's for attribute holds the control's id:
Keep the two attributes apart in your head. The id is for the page: labels, aria-describedby, CSS and scripts find the control by it, and it must be unique in the document. The name is for the server: it becomes the key in the submitted data, and several controls can share it, as every radio button in a group does. They often hold the same text for convenience, but they answer to different readers.
Explicit labels have the widest support across assistive technologies and leave the label and the control free to sit anywhere in the layout. They are this course's default.
Implicit labels
A label can also wrap its control, with no for and no id needed:
This is valid HTML and works well for a checkbox or a radio button, where the control and its text read as one line. It is less convenient for text boxes: the label text and the control are now one element, so styling the text alone needs an extra <span>, and placing the text above the box and a message below it gets awkward. A label may contain only one labelable control, and it must not contain another label. Using for while also wrapping is allowed, and harmless, as long as both point at the same control.
What is not a label
A placeholder is not a label. Placeholder text disappears the moment the person starts typing, so a form labeled only by placeholders asks people to remember what each box was for, and to delete what they typed to check. It is usually drawn in low-contrast gray, it is often mistaken for a value that is already filled in, and it is not reliably used as an accessible name. Use it, if at all, for an example of the expected format, never for the only description of the field.
Text that merely sits nearby is not a label either. A paragraph or a table cell next to a box looks like a label to a sighted person and means nothing to the accessibility tree. When a design truly has no room for visible text, as in a single search box beside a button reading "Search", an aria-label on the control supplies a name. It is the last resort, not the first: it is invisible, so sighted voice-control users cannot see what to say, and translation tools often skip it.
Where the accessible name comes from
When the browser computes a control's accessible name it takes the first source it finds, in this order: aria-labelledby, then aria-label, then an associated <label>, then the title attribute, then the placeholder. The ARIA attributes win over the label, so adding one to a labeled control replaces the visible name instead of adding to it. That is rarely what anyone wanted. A control can have more than one <label>; their text is joined. A script can list them through the control's labels property.
3. Groups: fieldset and legend
Some questions take more than one control to answer. "What were the trail conditions?" is answered by one of several radio buttons; "Which hazards did you see?" by any number of checkboxes; "Where do you live?" by a street, a city, a postal code and a country. Each control has its own label, "Muddy" or "Fallen tree", but the label alone does not say what question it answers. That is the job of <fieldset> and its <legend>.
The legend must be the fieldset's first child. Most screen readers announce it when focus enters the group, so the person hears "Trail conditions, group" and then "Dry, radio button, 1 of 3." The name is shared, so only one value can be chosen and only one is sent; the id values are unique, so each label finds its own button. One required on any button in the group makes the whole group required.
A fieldset is for a real group, a set of controls that answer one question or belong to one part of the form. It is not a wrapper for every field. An older habit put each single text box in its own fieldset to get a box drawn around it; that gives every box a "group" with one member, and makes a screen reader announce groups that are not there. Draw boxes with CSS; use fieldsets for meaning.
One switch for the whole group
A fieldset carries one useful behavior of its own. Its disabled attribute disables every control inside it, except those in its legend. Disabled controls cannot be focused or edited and are not submitted. That makes a fieldset the natural unit for a part of the form that applies only sometimes, such as the shipping address when "same as billing" is ticked: one attribute turns the whole section off, and a small script, or a server-rendered page, turns it back on.
Fieldsets can nest, and an address inside a "Contact" group is a fair use, but nesting more than one level deep makes for long announcements. If a form needs deep nesting, it probably needs to be split into sections or steps instead, which section 7 takes up.
4. Messages: the output element
Most fields need a little more than a name. Some need a hint before anyone types: "As it appears on the trailhead sign," "Up to 500 characters." Most can go wrong and need an error message after: "Choose the trail conditions." A form needs a place for each of these, tied to the control it describes. This course puts them in <output>.
Why output
The HTML standard describes <output> as the result of a calculation performed by the application, or the result of a user action. A message about what someone typed is exactly that: the form's response to their input. The element brings three things a <div> or <span> does not:
- A
forattribute. It holds theidof the control, or a space-separated list of controls, whose values the output is about. The relationship is written in the markup, where a script, a style sheet or a reader of the source can see it. - A role. The HTML accessibility mappings map
<output>to the ARIAstatusrole, a polite live region: when its text changes, screen readers announce the new text at the next pause, without interrupting. This course still writesaria-live="polite"on message outputs, because not every browser has always followed the mapping, and saying it twice costs nothing. - It is not submitted. An output belongs to the form, has a
nameand avaluea script can read, and is listed in the form'selements, but its contents are never sent. A message never ends up in the request by accident.
Connecting the message to the control
The for attribute records the relationship but does not, by itself, make the message part of what assistive technology says about the control. That takes aria-describedby on the control, listing the id of each message in the order they should be read. The two attributes point in opposite directions and do different jobs: for says "this output is about that control"; aria-describedby says "read this text as that control's description."
The two classes are the only classes this section's markup uses, and they mark the two kinds of message. A hint is there from the start and does not change, so it needs no live announcement; it is read as part of the description when the person reaches the field. An error starts empty and is filled when something is wrong, so its live region announces the problem the moment it appears and the description repeats it when the person returns to the field. Keeping them apart also lets CSS style them differently, and lets an empty error take no space at all.
What HTML and CSS can do alone, and what needs a script
With no script, the browser still checks constraints such as required when the form is submitted, stops the submission, focuses the first problem and shows its own message. CSS can do more on top: the :user-invalid pseudo-class matches a control only after the person has interacted with it and left it invalid, so a style sheet can color the field and reveal a static error message that was already in the markup. That reveal is a visual cue only. A live region announces text that is added or changed, and showing an element that was hidden is not reliably either, so a CSS-only error is seen but may not be heard.
Putting the right words into the error output when they are needed, and so getting them announced, is a script's job: a few lines that listen for the field losing focus or for the form's invalid event and set the output's text. Form Accessibility and Usability covers when to show a message and what it should say, and Events: forms covers the events. The markup on this page is ready for both: the error has its place, its for, its live region and its description link before any script runs.
Output for results
The element's original use still applies. A trip-cost estimate that multiplies nights by a nightly fee, or a "characters remaining" count, is a calculation the page shows and does not send. Put it in an output with for listing the controls it depends on. The server must calculate anything that matters again, from the submitted values, because what the page shows is only a preview. Form Security explains why.
5. The form-field pattern
A label, a control, a hint and an error belong together, and something has to hold them together for layout and styling. The usual answer is a <div> with a class, repeated for every field, until the form is mostly divs. This course uses a custom element instead.
How the pattern got here
The pattern is the last step in a progression that many developers walk through, each step fixing a problem with the one before:
The pattern
Everything inside is standard HTML doing its standard job: the label names the control, the control carries its constraints and its name, and the outputs carry its messages. <form-field> adds no behavior at all. It is a container with a name that says what it contains.
Why a custom element and not a div
- It says what it is. A page full of
<form-field>reads as a form made of fields. A page full of<div class="form-group">reads as boxes, and every project spells the class differently. - CSS can select it by element. The style sheet styles
form-fieldand what is inside it, with no class names to keep in step between the markup and the CSS. That is the house style from Selectors: style the structure first. - It is valid HTML that needs no script. Any element name with a hyphen in it is a valid custom element name. A browser that has no definition for it treats it as a plain element, like a
<span>, and renders its contents. Nothing breaks, nothing waits for JavaScript. - It is ready for later. If the course's script defines the element, it can add behavior, such as checking the field when the person leaves it, to every field at once, without touching the markup. That is progressive enhancement: the markup works first, and script improves it.
The element adds nothing to the accessibility tree; it has no role, so assistive technology sees through it to the label, control and outputs inside. That is what we want. It is not a form control either: it does not submit anything. An element that should take part in the form like an <input> does, with its own value and validity, is a different and much bigger idea, a form-associated custom element, which Components: form-associated elements covers.
The br, and the page with no CSS
A browser with no definition for <form-field> and no style sheet displays it inline, the default for any unknown element, and the label, the input and the outputs are all inline too. Without help they would run together on one line: "Email [______] We send one confirmation… Trail [______]", one field running into the next. The <br> elements are that help. With no CSS at all, they put the label above the box, each message on a line of its own below it, and the next field on a new line, which is the layout a form should have anyway. An empty error output followed by its <br> leaves a blank line, which is just enough space between fields.
When the style sheet loads, it makes form-field a grid and hides the <br>, which then does nothing:
Three details are worth noticing. The error output is hidden only while it is empty, so the moment a script puts words in it, it appears. The invalid style uses :user-invalid, not :invalid, so a required field is not painted red before anyone has touched it. And the invalid border gets a second line, drawn with a shadow, as well as changing color, so the state does not depend on color alone; the message text in the error output is the real signal.
A checkbox field puts the control first and the label after it, the order people expect for a checkbox. Its <br> goes after the label, not between them:
A radio group keeps its fieldset and legend, and one <form-field> can hold the whole group with one message for the question as a whole. The for on that message lists every button in the group:
6. Order and layout
A form is read in order: top to bottom by eye, first to last by keyboard, start to end by screen reader. Those three orders should be the same, and they are, as long as the source order is the order the form should be filled in.
One column, labels above
Most forms work best as a single column, one field after another, with each label directly above its control. The eye moves straight down; nobody has to decide whether to read across or down; the layout is the same on a phone and a desktop, so it needs no breakpoints. Labels above their controls stay close to them however long the label text grows, and translated labels, often much longer than English, do not push the box off the screen. Short fields that people think of as one thing, such as a city, a state and a postal code, can share a row on wide screens, but the default is one column.
Put related things together
People read closeness as belonging. A label equally far from two boxes belongs to neither. A checkbox that sits directly under the email field, "Send me trail news," reads as part of the email question even if it is about something else; given its own space and its own group, it reads as a separate choice. The rule of proximity from design applies directly: keep a label close to its own control, keep a group's controls closer to each other than to the next group, and leave clear space between groups. Fieldsets and the margins in the house CSS do most of this.
Source order is tab order
CSS can draw elements in a different order from the source, with grid placement, order, or flex-direction: row-reverse. In a form that is a trap. Keyboard focus and screen readers follow the source, not the picture, so a field drawn first but written last is reached last, and focus jumps around the screen. Positive tabindex values, which try to set the order by number, make it worse, because every other control on the page then comes after them.
Write the HTML in the order the form should be filled in and let the layout follow it. A newer CSS property, reading-flow, lets a grid or flex layout tell the browser to follow the visual order instead; it shipped in Chrome in 2025 but is not yet in Firefox or Safari, so it is not something to rely on.
Buttons where the eye ends
The submit button goes after the last field, aligned with the fields, where the eye and the keyboard arrive when the form is done. Say what it does: "Send report" is clearer than "Submit," and far clearer than "OK." Put one primary action in a form. A secondary action, such as "Save draft," can sit beside it if it is styled as secondary; a "Clear form" button that throws everything away sits too easily under a careless click, which is why Form Fields calls type="reset" an anti-pattern.
Controls outside the form
Sometimes the layout puts a control outside the <form> element, for instance a "Send" button in a sticky footer bar. The form attribute connects it anyway: give the form an id and give the control form="that-id", and the control belongs to the form, submitting it or being submitted with it, wherever it sits in the document. It is a useful escape hatch, but it splits the form across the page for anyone reading the source, so use it only when the layout truly requires it.
Marking required and optional
People need to know before they start which fields they can skip. The required attribute tells assistive technology, which announces "required," and tells the browser to check it; the person looking at the screen needs to be told too. Two conventions work. When most fields are required, as in a short form that asks only for what it needs, mark the exceptions: "Phone (optional)". When most are optional, mark the required ones, with the word or with an asterisk explained once at the top of the form. Whichever you choose, use it consistently, and put the words in the label, where everyone reads them. The house CSS can add an asterisk to every label whose field has required, with form-field:has(:required) label::after, as a visual cue, but the explanation of what the asterisk means still has to be in the page.
7. Long forms: sections and steps
Every field costs the person filling in the form some time and some effort, and every extra field is another chance to give up. The first fix for a long form is a shorter one, and Forms for People asks hard questions about what to collect at all. When a form really does need many fields, organization makes the length bearable.
Sections with headings
Break a long form into named parts: "About you," "The hike," "What you saw." A heading at the start of each part lets people skim the whole form before they start, lets screen reader users jump between parts with their heading keys, and turns one long column into a few short ones. When a part is a real group of related controls, the part can be a fieldset and the heading its legend; when it is just a stretch of the form, a heading is enough.
Steps, with progress you can see
Some forms are better split across several screens: a checkout with shipping, then payment, then a review; an application with eligibility questions that decide which later questions apply. Steps keep each screen short and let the questions on one screen depend on the answers to the last. They work when three things hold:
- The person can see where they are. "Step 2 of 4: The hike," with the steps listed in an ordered list and the current one marked with
aria-current="step", says how far they have come and how far is left. A<progress>bar can show the same thing, but the words matter more than the bar. - The person can go back without losing anything. Every step keeps its answers when the person returns to it.
- The steps are honest. A four-step form that turns out to have seven, or a "quick question" that opens into a twenty-field page, costs more trust than the extra fields.
In plain HTML, each step can be its own page with its own form, posting to the server, which keeps the answers so far and sends back the next step. That works with no script at all and survives a closed tab, because the answers live on the server. Scripted versions show one section at a time on a single page; they feel faster, but they must keep the same promises, and they must still work when the script does not run.
Saving progress
A long form should not be lost to a dropped connection, a dead battery or a closed tab. Two HTML features help. A second submit button with formnovalidate and its own name and value, such as "Save draft," sends the form without the browser's checks, so a half-finished form can be saved on the server. And the browser itself often restores typed values when the person goes back to a page, if the page did not stop it. Script can go further and keep a copy of the answers in the browser as the person types, which Forms for People introduces, along with what should never be kept that way.
8. Summary
| Concept | Key points |
|---|---|
| Four readers | People, assistive technology, autofill and the server all read the markup, not the layout. Organizing a form makes the markup say what the layout shows. |
| Labels | Every control gets a <label>, explicit with for and id by default. id is for the page, name for the server. A placeholder is not a label; aria-label is a last resort and overrides a real label. |
| Groups | <fieldset> and a first-child <legend> name a real group: radio sets, checkbox sets, an address. Not one per field. disabled on a fieldset switches off a whole section. |
| Messages | <output> holds hints and errors: for names the control, aria-describedby on the control reads it as the description, the status role announces changes politely, and it is never submitted. class="hint" is static; class="error" starts empty. |
| form-field | A custom element holding label, control and messages: named, valid without script, selectable by element in CSS, invisible to assistive technology, ready for enhancement. A <br> keeps it readable with no CSS; the house CSS hides it. |
| Order and layout | One column, labels above, related things together. Source order is focus order, so write the form in filling order and never reorder it with CSS or positive tabindex. One clearly named primary button at the end. |
| Long forms | Shorten first. Then sections with headings, or honest steps with visible progress that keep answers when going back. Let people save a draft with formnovalidate. |
