Form States — Styling Focus, Hover, Disabled, and Validation
A well-styled form communicates with the user. It shows which field is active, which fields are unavailable, and which fields contain errors — all without a word of JavaScript. CSS pseudo-classes make this possible.
You already styled :focus in Lesson 02. This lesson builds the complete state system.
The state hierarchy
Section titled “The state hierarchy”Think of form field states as a progression:
- Default — the field is available but not interacted with
- Hover — the user’s cursor is over the field
- Focus — the field is active (clicked or tabbed to)
- Filled — the user has entered content
- Disabled — the field is not available
- Invalid — the content does not pass validation
Each state gets its own visual treatment. Together, they create a form that guides users through completion without requiring any explanatory text.
:hover — signaling interactivity
Section titled “:hover — signaling interactivity”A subtle border color change on hover confirms the field is interactive before the user commits to clicking:
.form-group input:hover,.form-group textarea:hover,.form-group select:hover { border-color: #5a5a5a;}#5a5a5a (mid-gray) is a slight darkening of the default warm gray border. The change is noticeable on close inspection but not dramatic — hover is a hint, not a statement.
The order in CSS matters here: :hover should be defined before :focus in the stylesheet so that :focus wins when both are active simultaneously (the user is hovering over a focused field).
:focus — already in place
Section titled “:focus — already in place”From Lesson 02, the focus state uses a border color shift and a custom shadow ring:
.form-group input:focus,.form-group textarea:focus,.form-group select:focus { outline: none; border-color: #2c4a1e; box-shadow: 0 0 0 3px rgba(44, 74, 30, 0.2);}This is the most important state — it must be clearly visible. The green ring against the warm off-white background of the STO site is high-contrast and immediately recognizable.
:disabled — communicating unavailability
Section titled “:disabled — communicating unavailability”When a field is not available (because of form logic, user permissions, or a submitted state), its visual appearance should communicate “this cannot be used”:
.form-group input:disabled,.form-group textarea:disabled,.form-group select:disabled { opacity: 0.5; cursor: not-allowed; background-color: #eae4da;}opacity: 0.5— fades the entire field including its label and textcursor: not-allowed— the prohibited cursor iconbackground-color: #eae4da— the STO alternate background color; a slight gray to reinforce that the field is inactive
The disabled attribute in HTML automatically prevents user interaction — this CSS only handles the visual signal.
HTML validation and CSS :valid / :invalid
Section titled “HTML validation and CSS :valid / :invalid”HTML provides built-in validation through attributes:
required— the field must not be emptytype="email"— the value must look like an email addressminlength="N"— the value must be at least N characterspattern="..."— the value must match a regex pattern
CSS can read the validation state:
input:valid { border-color: #2c4a1e; /* green border for valid input */}
input:invalid { border-color: #c0392b; /* red border for invalid input */}There is a problem with this approach: :invalid fires immediately when the page loads, before the user has typed anything — required fields are invalid by default when they are empty. This creates a jarring experience where users see red borders the moment they arrive at the form.
:placeholder-shown — the timing fix
Section titled “:placeholder-shown — the timing fix”The :placeholder-shown pseudo-class fires when the field’s placeholder text is visible — meaning the field is empty. Combining it with :not() and :invalid creates a validation style that only appears after the user has started (and then cleared or left invalid content):
.form-group input:invalid:not(:placeholder-shown),.form-group textarea:invalid:not(:placeholder-shown) { border-color: #c0392b; box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.15);}Reading the selector: “An invalid input that is NOT currently showing its placeholder” — meaning the user has typed something, and what they typed does not pass validation.
The practical effect: the red border only appears after the user has interacted with the field and left it in an invalid state. Empty fields that the user has not yet touched show no error styling.
Note: This approach relies on the placeholder attribute being present on the input. Fields without a placeholder will show the error state immediately. Always include placeholder on fields that use this validation pattern.
:required — marking required fields
Section titled “:required — marking required fields”You can style required fields to give users an upfront signal:
.form-group input:required,.form-group textarea:required { border-left: 3px solid #2c4a1e;}This adds a green left border accent to required fields. The approach is optional — many forms use an asterisk (*) in the label text instead, which is equally clear and requires no CSS:
<label for="name">Name <span aria-hidden="true">*</span></label>Both approaches are valid. The asterisk approach is more universally understood and does not require additional CSS.
A note on error messages
Section titled “A note on error messages”CSS can style the visual appearance of an invalid field, but it cannot show or hide error message text. Displaying a specific error message (“Please enter a valid email address”) requires a small amount of JavaScript.
The CSS preparation: write the error message in your HTML with a class and set it to display: none by default. JavaScript adds a class to show it when validation fails. Module 07 onwards is where JavaScript enters the picture — for now, the visual state of the field itself communicates the error.
The complete state ruleset
Section titled “The complete state ruleset”/* Hover */.form-group input:hover,.form-group textarea:hover,.form-group select:hover { border-color: #5a5a5a;}
/* Focus */.form-group input:focus,.form-group textarea:focus,.form-group select:focus { outline: none; border-color: #2c4a1e; box-shadow: 0 0 0 3px rgba(44, 74, 30, 0.2);}
/* Invalid after interaction */.form-group input:invalid:not(:placeholder-shown),.form-group textarea:invalid:not(:placeholder-shown) { border-color: #c0392b; box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.15);}
/* Disabled */.form-group input:disabled,.form-group textarea:disabled,.form-group select:disabled { opacity: 0.5; cursor: not-allowed; background-color: #eae4da;}Exercise
Section titled “Exercise”Apply state styling to the STO contact form:
-
Add the complete state ruleset to
style.css. -
Open
contact.html. Hover over the Name field — the border should darken slightly. Click into it — the green focus ring should appear. Click elsewhere — the ring should fade out. -
Type an invalid email address in the Email field (e.g. “notanemail”). Click away. The field should show a red border and shadow. Now type a valid email (e.g. “hello@example.com”). The red border should disappear — the
:invalid:not(:placeholder-shown)selector no longer matches a valid email. -
In DevTools, select the Name input and add the
disabledattribute. The field should fade to 50% opacity with the alternate background color. -
Clear the Name field entirely and click elsewhere. Because it has the
requiredattribute, it becomes:invalid— but because the placeholder is now showing again, the red border should NOT appear. This confirms the:not(:placeholder-shown)timing fix is working.
- Form state styling is a system: default → hover → focus → invalid → disabled. Each state should be visually distinct.
:hoveron inputs signals interactivity with a subtle border darkening — define it before:focusso:focuswins when both are active.:focusmust always be visible for keyboard and assistive technology users — the custombox-shadowring replaces the browser outline.:invalid:not(:placeholder-shown)shows validation errors only after the user has typed and left invalid content, avoiding the “red on arrival” problem.:disabledusesopacity: 0.5,cursor: not-allowed, and a muted background to communicate unavailability.- CSS handles the visual state of invalid fields; showing or hiding specific error messages requires JavaScript.
Lesson 06 assembles everything into the complete STO contact form stylesheet.