HTML & CSS / Foundations / Lesson 4
Forms, labels, and native controls
Create controls whose purpose, grouping, and submission behavior are understandable before adding custom styling.
On this page
Lesson goals
- Start from the task and data
- Separate names, types, and submission
- Verify complete interactions
Start from the task and data
A form collects values for a purpose, so identify which fields are necessary and what each value means before choosing controls. A text input, checkbox, radio group, select, and textarea express different interaction models. Use native controls where they fit; recreating a select with styled div elements requires a substantial keyboard and accessibility implementation. The name attribute identifies submitted data, while id identifies an element within the document. They may share a value but perform different jobs. A visible label is information for every user, not merely a hidden accommodation for a screen reader.
Connect labels and instructions
An explicit label associates its for value with the control’s unique id. Clicking the label then provides the control’s native labeling behavior and increases the useful target area. Placeholder text is supplementary guidance; it disappears during entry and does not replace a persistent label. Use aria-describedby to connect additional instructions or an error message when appropriate, keeping those instructions visible when users need them. Do not add aria-label casually to a visibly labeled input because it can override the expected accessible name. Group related options with fieldset and legend when the relationship is necessary to understand the choices.
Separate names, types, and submission
The first example labels an email field, supplies autocomplete guidance, and uses a submit button. The form uses a fragment action and GET only for a synthetic local demonstration; a deployed endpoint would need an intentional method, privacy policy, and server processing. The email type enables suitable browser validation and input affordances but cannot prove an address exists or belongs to a person. required expresses a browser-side constraint, which a server must independently enforce. A button inside a form defaults to submission unless its type says otherwise, so mark ordinary local actions type="button" explicitly.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Labeled field</title>
</head>
<body>
<main><h1>Request workshop information</h1><form action="#confirmation" method="get"><p><label for="email">Email address</label><input id="email" name="email" type="email" autocomplete="email" required aria-describedby="email-help"><span id="email-help">Use an address you can access.</span></p><button type="submit">Review request</button></form><p id="confirmation">This local example has no processing service.</p></main>
</body>
</html>
A labeled email field with connected instructions and a submit button.
The explicit label names the field, and the helper text is connected separately. Only synthetic values should be entered into this local GET demonstration.
Group a single choice accessibly
The second example gives radio buttons the same name so the browser treats them as a single-choice group. Distinct ids keep each explicit label unambiguous, and legend supplies the shared question. A checked option can provide a documented default; do not preselect consent or infer sensitive preferences without justification. Native keyboard behavior differs between text fields and radio groups, so test the actual controls rather than imposing one universal Tab pattern. The selected value submitted is the control’s value attribute, not necessarily its visible label. Choose stable machine values while keeping user-facing descriptions clear.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Grouped options</title>
</head>
<body>
<main><h1>Session preference</h1><form><fieldset><legend>Choose a session</legend><p><input id="morning" type="radio" name="session" value="morning" checked><label for="morning">Morning</label></p><p><input id="afternoon" type="radio" name="session" value="afternoon"><label for="afternoon">Afternoon</label></p></fieldset><button type="button">Local demonstration only</button></form></main>
</body>
</html>
Exactly one session can be selected in the group.
The radio name creates one group, legend gives its question, and distinct ids connect each option label. The non-submit button avoids accidental navigation.
Verify complete interactions
Use the form by keyboard, with autofill, after zoom, and with errors present. Confirm that focus is visible, the label remains available, and instructions do not rely exclusively on color. Inspect submitted synthetic values to distinguish missing name attributes from missing ids. Do not disable the submit button as the only explanation of invalid input; users need actionable feedback. Browser validation messages vary by language and browser, so avoid claiming exact universal text. A production workflow also needs server validation, safe handling of submitted data, and a confirmation that accurately represents whether processing succeeded.
Common errors and fixes
A field looks labeled but has no useful accessible name
- Why it happens
- Text is merely positioned beside the input.
- How to fix it
- Connect label for to the unique input id.
- Prevent it next time
- Inspect names and click labels during review.
A helper button submits unexpectedly
- Why it happens
- The button omitted its type inside a form.
- How to fix it
- Set type="button" for non-submit actions.
- Prevent it next time
- Declare every form button’s purpose explicitly.
A control is missing from submitted values
- Why it happens
- It has an id but lacks the required submission name.
- How to fix it
- Give the control its intentional name and value.
- Prevent it next time
- Verify synthetic serialized form data.
Check your understanding
1. Why does a placeholder saying “Email” not replace a label?
Show worked solution
It disappears when a value is entered, may have weak contrast, and does not supply the same persistent visible instruction and native association. Add a real label connected to the input, then use a placeholder only for optional examples of expected content.
2. Design two radio choices for delivery speed, including their submitted values.
Show worked solution
Use one fieldset with legend “Delivery speed,” two inputs sharing name="speed", distinct ids, values "standard" and "express", and explicit labels. One group name enforces a single choice; ids associate labels and do not define the server’s field name.
Key takeaways
- Visible labels and programmatic names work together.
- Client constraints do not replace server validation.
- Choose native controls before custom behavior.