HTML & CSS / Adaptable interfaces / Lesson 12
Form layout, focus, and error states
Style forms without losing labels, native behavior, keyboard visibility, or actionable feedback.
On this page
Lesson goals
- Arrange relationships rather than coordinates
- Provide focus that is actually visible
- Verify recovery, not only rejection
Arrange relationships rather than coordinates
A form layout should keep each label, control, instruction, and error visibly related. A simple stacked field often adapts more reliably than absolute positioning. Use consistent spacing within a field and a larger separation between fields so grouping survives without borders. Grid can align a label column on wide layouts, but the source should still read label then control then help. Do not use placeholder text as a compact-layout substitute for labels. Long translations, zoom, autofill, and validation messages can all change height, so avoid fixed rows that assume every field stays on one line.
Preserve native affordances
Inputs and buttons have platform-specific appearance that communicates interaction. Resetting all appearance can remove useful affordances and creates a responsibility to restore every necessary state. Inherit font settings where suitable, provide adequate padding, and keep borders distinguishable from adjacent backgrounds. Disabled and readonly are different: a disabled control is generally unavailable for interaction and submission, while a readonly text control can remain focusable and submitted. Do not disable a control simply to make it look muted if users still need its value or explanation. Styling should reinforce the underlying HTML contract rather than contradict it.
Provide focus that is actually visible
The first example uses focus-visible to add a strong outline to keyboard-focused controls while keeping the browser’s default behavior available elsewhere. It does not start by removing every outline. Outline-offset separates the indicator from the border, and nearby spacing prevents it being clipped by another box. A focus style needs contrast against its surroundings in each state and theme. Test real Tab movement, not just a manually toggled pseudo-class in developer tools. A parent’s overflow clipping or a sticky header can hide an otherwise correct indicator, so focus visibility is a layout concern as well as a color choice.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Form focus</title>
<style>
.field { display: grid; gap: .4rem; margin-block-end: 1rem; } input, button { font: inherit; padding: .6rem; } input:focus-visible, button:focus-visible { outline: 3px solid #174e9a; outline-offset: 3px; }
</style>
</head>
<body>
<main><h1>Contact details</h1><form><div class="field"><label for="contact">Contact name</label><input id="contact" name="contact" autocomplete="name"></div><button type="button">Local preview</button></form></main>
</body>
</html>
A stacked field and a visible keyboard-focus treatment.
The label and control remain grouped in source and layout. Keyboard focus adds an explicit indicator without changing the input’s role.
Make feedback specific and associated
The second example is a static error-state demonstration. The control has aria-invalid="true", its error text is visible, and aria-describedby links that text to the field. The message says how to correct the problem rather than displaying only “invalid.” This markup does not implement a live validation system or announce dynamically inserted messages automatically. A real application must update state at an appropriate time, manage any summary and focus behavior deliberately, and retain entered values when possible. Client validity pseudo-classes can help presentation, but showing errors before a person has interacted can be confusing and must be designed intentionally.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Associated error</title>
<style>
.error { color: #8b0000; } [aria-invalid="true"] { border: 2px solid #8b0000; } input { font: inherit; padding: .5rem; }
</style>
</head>
<body>
<main><h1>Quantity correction</h1><form><label for="quantity">Quantity</label><input id="quantity" name="quantity" type="number" min="1" value="0" aria-invalid="true" aria-describedby="quantity-error"><p id="quantity-error" class="error">Enter a quantity of at least 1.</p><button type="button">Static preview only</button></form></main>
</body>
</html>
Quantity 0 is marked invalid with a specific correction message.
A visible correction message supplies meaning beyond the red border. The static aria-invalid state must be updated by real application behavior after correction.
Verify recovery, not only rejection
A usable form helps someone succeed after an error. Test submitting synthetic invalid values, finding the first problem, understanding its message, correcting it, and reaching a truthful confirmation. Check that errors remain associated after responsive rearrangement and that focus is not trapped or obscured. Color and icons can reinforce text but cannot replace it. Server-side failures need feedback too, especially when an operation may have partially succeeded or its result is uncertain. HTML and CSS provide structure and presentation; robust request handling, authorization, and persistence belong to the application layer and cannot be supplied by a green success border.
Common errors and fixes
Keyboard focus disappears
- Why it happens
- A reset removed outlines without a replacement.
- How to fix it
- Restore a visible indicator and inspect clipping.
- Prevent it next time
- Navigate every control with a keyboard.
An error cannot be connected to its field
- Why it happens
- The text is only visually adjacent.
- How to fix it
- Associate it with a stable id and appropriate description.
- Prevent it next time
- Review the accessible description with errors present.
A disabled value vanishes from submission
- Why it happens
- Disabled was used only to obtain a muted appearance.
- How to fix it
- Choose readonly or ordinary presentation according to the contract.
- Prevent it next time
- Test serialized data for every control state.
Check your understanding
1. Design a message for a date field that accepts future workshop dates only.
Show worked solution
Keep the visible label “Workshop date,” add a specific message such as “Choose a date after today,” associate it with the field, and mark the current invalid state. A real validator must use the application’s date and timezone policy rather than merely coloring the input red.
2. What should be tested after replacing default form styles with custom ones?
Show worked solution
Check keyboard focus, disabled and readonly behavior, checked states, error messages, autofill, zoom, high-contrast settings, and actual submitted values. The new appearance owns the responsibility for every affordance removed by the reset; a successful default screenshot covers only one of those states.
Key takeaways
- Focus visibility depends on layout and contrast.
- Associate errors and explain correction.
- Form states have behavior as well as appearance.