Fylgja Form Field

A form field groups a label, a control, optional hint text, and an error message. Validation comes from the browser itself: attributes like required, type="email", minlength, and pattern decide when the value is invalid. Fylgja base turns the border red, and the field-error-message class from form extend reveals the message, all without JavaScript.

Preview

Type something that is not an email address, then move focus away.

Enter an email address, like me@example.com.
<div class="field">
	<label for="field-email">Email</label>
	<input
		type="email"
		id="field-email"
		name="email"
		autocomplete="email"
		required
		aria-errormessage="field-email-error"
	/>
	<small id="field-email-error" class="field-error-message block">
		Enter an email address, like me@example.com.
	</small>
</div>

Usage

  • Wrap the label, control, hint, and error in an element with the field class. The error is scoped to that wrapper, so it can sit anywhere inside it.
  • Add the native validation attributes the value needs. The browser does the checking.
  • Give the error message the field-error-message class. It stays hidden until the control matches :user-invalid or has aria-invalid="true".
  • Point aria-errormessage at the error and aria-describedby at the hint.

The message uses visibility, so its space stays reserved while hidden and the layout does not jump when it appears.

Anatomy

  • .field: the wrapper that ties the error message to its control.
  • <label>: names the control, linked with for and id.
  • Control: any <input>, <select>, or <textarea> with validation attributes.
  • Hint (optional): a <small> below the control with guidance that is always visible. Use it sparingly, for passwords and other inputs with rules the label cannot convey.
  • .field-error-message: the message shown once the control is invalid.

Accessibility

  • Every control needs a visible <label>. A placeholder is not a label, since it disappears as soon as the user types.
  • Link the hint with aria-describedby so it is read along with the label. Do not add the error to aria-describedby as well, since screen readers would then read the hidden message every time.
  • aria-errormessage links the error to the control. Screen reader support for it is still uneven, but on submit the browser also announces its own validation message and moves focus to the first invalid field.
  • Write the error as a fix, such as “Enter an email address”, not just “Invalid”.
  • Do not rely on the red border alone. The message text carries the meaning for users who cannot tell the colors apart.

Variants

Several rules

Combine required, minlength, maxlength, and pattern on one control. Since the message cannot tell which rule failed, describe all of them in it.

3 to 16 characters, lowercase letters, numbers or underscores. Use 3 to 16 lowercase letters, numbers or underscores.
<div class="field">
	<label for="field-username">Username</label>
	<input
		type="text"
		id="field-username"
		name="username"
		autocomplete="username"
		required
		minlength="3"
		maxlength="16"
		pattern="[a-z0-9_]+"
		aria-describedby="field-username-hint"
		aria-errormessage="field-username-error"
	/>
	<small id="field-username-hint" class="block">3 to 16 characters, lowercase letters, numbers or underscores.</small>
	<small id="field-username-error" class="field-error-message block">
		Use 3 to 16 lowercase letters, numbers or underscores.
	</small>
</div>

Server error

Some errors only the server knows about. Render the control with aria-invalid="true" and the message is visible on page load.

This coupon has expired.
<div class="field">
	<label for="field-coupon">Coupon code</label>
	<input
		type="text"
		id="field-coupon"
		name="coupon"
		value="SUMMER20"
		aria-invalid="true"
		aria-errormessage="field-coupon-error"
	/>
	<small id="field-coupon-error" class="field-error-message block">
		This coupon has expired.
	</small>
</div>

Checkbox

A required checkbox works the same way. Wrap it in its label and keep the error inside the .field wrapper.

You need to accept the terms to continue.
<div class="field">
	<label>
		<input type="checkbox" name="terms" required aria-errormessage="field-terms-error" />
		I agree to the terms of service
	</label>
	<small id="field-terms-error" class="field-error-message block">
		You need to accept the terms to continue.
	</small>
</div>

Examples

Sign-up form

Submit the empty form and every invalid field shows its error at once. The browser focuses the first one. Use flow on the form to space the fields.

Enter your name.
Enter an email address, like me@example.com.
At least 8 characters. Use at least 8 characters.
<form action="#" class="flow">
	<div class="field">
		<label for="signup-name">Name</label>
		<input type="text" id="signup-name" name="name" autocomplete="name" required aria-errormessage="signup-name-error" />
		<small id="signup-name-error" class="field-error-message block">Enter your name.</small>
	</div>
	<div class="field">
		<label for="signup-email">Email</label>
		<input type="email" id="signup-email" name="email" autocomplete="email" required aria-errormessage="signup-email-error" />
		<small id="signup-email-error" class="field-error-message block">Enter an email address, like me@example.com.</small>
	</div>
	<div class="field">
		<label for="signup-password">Password</label>
		<input
			type="password"
			id="signup-password"
			name="password"
			autocomplete="new-password"
			required
			minlength="8"
			aria-describedby="signup-password-hint"
			aria-errormessage="signup-password-error"
		/>
		<small id="signup-password-hint" class="block">At least 8 characters.</small>
		<small id="signup-password-error" class="field-error-message block">Use at least 8 characters.</small>
	</div>
	<button>Create account</button>
</form>

Related: Select, Search Box, Input Switch.

FAQ

Why does the error not show while I am still typing?

The error follows the :user-invalid pseudo-class, which only matches after the user has changed the field and moved on, or tried to submit the form. An empty required field is invalid from the start, and :user-invalid keeps that from being shown before the user has done anything.

Can I show a different message for each validation rule?

Not with CSS alone. The field-error-message class only knows the field is invalid, not why. Write one message that covers every rule, or read input.validity in a script and swap the message text.

How do I show an error from the server?

Render the field with aria-invalid="true" and the message shows straight away. Remove the attribute with a script once the user edits the value, or the error stays visible.

Can I hide the browser's own validation bubble?

Adding novalidate to the form removes the bubble, but also stops the browser from blocking an invalid submit, so you then need a script to do that. Keep the native behavior unless you have a reason to replace it.