Styling Required Field Indicators

Should required fields get an asterisk, the word “required”, or nothing at all — with optional fields marked instead? And whichever you choose, how do you make sure the indicator is understood by sighted users who do not know what an asterisk means, announced once (not twice) by screen readers, translated by page translation tools, and kept in sync with the actual required attribute that drives the Constraint Validation API? This recipe compares the conventions, implements the recommended one — mark the minority, in words, in the markup — and shows how :has(:required) styling keeps the visual indicator honest without duplicating information for assistive technology.

When Each Convention Fits

The right indicator depends on which fields are the minority. Users read forms faster when only the exceptions are marked.

  • Most fields required (sign-up, checkout): mark the optional ones with “(optional)” and say so at the top: “All fields are required unless marked optional.” This is the convention used by the UK government design system and many large services.
  • Most fields optional (profile, preferences): mark the required ones with “(required)”.
  • Mixed, long forms (applications): mark required fields and explain the marker once at the top.

An asterisk alone is the least clear choice: many users do not know what it means, it is small and easy to miss, and screen readers announce it as “star” — or skip it. If brand guidelines require an asterisk, pair it with a legend explaining it and with the real required attribute, which is what assistive technology actually announces. The broader styling approach is in CSS validation state styling.

Asterisks versus words for required fields Two columns comparing an asterisk-only required indicator with a word-based indicator in the label. Asterisk only ✗ meaning must be learned or explained ✗ small target, easy to miss ✗ read as "star" or skipped ✗ not translated with the page "(optional)" or "(required)" in the label ✓ understood without explanation ✓ read naturally by screen readers ✓ translated with the page ✓ mark the minority to reduce noise
Words are understood without a legend, translate with the page and read naturally aloud; an asterisk needs all three fixed separately.

Minimal Working Indicators

<form id="apply" novalidate>
  <p class="form-note">All fields are required unless marked optional.</p>

  <div class="field">
    <label for="fullname">Full name</label>
    <input id="fullname" name="fullname" autocomplete="name" required>
  </div>

  <div class="field">
    <label for="company">Company <span class="optional">(optional)</span></label>
    <input id="company" name="company" autocomplete="organization">
  </div>

  <div class="field">
    <label for="phone">Phone number <span class="optional">(optional)</span></label>
    <input id="phone" name="phone" type="tel" autocomplete="tel">
  </div>

  <button type="submit">Send application</button>
</form>
.optional {
  font-weight: 400;
  color: var(--muted-text, #475569);     /* 4.5:1 on white — still readable */
}

/* Consistency guard: a field marked optional must not be required, and vice versa.
   In development, make mismatches loud so they are fixed before release. */
.dev .field:has(.optional):has(:required) label { outline: 3px dashed #b91c1c; }
.dev .field:not(:has(.optional)):has(:is(input, select, textarea):not(:required)) label { outline: 3px dashed #d97706; }
// Messages still come from validity; the indicator is only a promise about it.
const form = document.querySelector<HTMLFormElement>("#apply")!;
form.addEventListener("submit", (event) => {
  for (const el of form.querySelectorAll<HTMLInputElement>("[required]")) {
    el.setCustomValidity("");
    if (el.validity.valueMissing) {
      const label = form.querySelector(`label[for="${el.id}"]`)?.firstChild?.textContent?.trim() ?? "this field";
      el.setCustomValidity(`Enter your ${label.toLowerCase()}.`);
    }
  }
  if (!form.reportValidity()) event.preventDefault();
});

The word lives in the markup, inside the label, so it is part of the accessible name (“Company (optional)”), it translates with the page, and it cannot be dropped by a CSS change. The development-only :has() rules catch the most common drift — a field marked optional that someone later made required — which would otherwise produce the confusing “this optional field is required” error.

Which fields should carry the marker? A decision tree choosing whether to mark optional fields, required fields, or both, based on the proportion of required fields in the form. Are most fields required? yes Mark optional fields in words + note at top no Are most fields optional? yes Mark required fields in words no Mark required fields + explain once
Mark the minority, explain the convention once at the top, and let the required attribute handle what assistive technology announces.

Indicator Option Reference

Option Where Example Notes
Word in the label markup Company <span class="optional">(optional)</span> Part of the accessible name; translatable
Form note top of form “All fields are required unless marked optional.” Explains the convention once
required attribute input required Announced as “required” by screen readers; drives validation
aria-required="true" custom widgets only on a custom combobox Native inputs should use required instead
Asterisk (if mandated) markup + legend Name <abbr title="required">*</abbr> Hide from AT with aria-hidden if required is set, to avoid double announcements
Generated content CSS ::after content: " (required)" Avoid for meaning: not translated, inconsistent in screen readers

Verification Steps

import { test, expect } from "@playwright/test";

test("optional markers agree with the required attribute", async ({ page }) => {
  await page.goto("/apply");
  const mismatches = await page.evaluate(() =>
    [...document.querySelectorAll(".field")].filter((f) => {
      const optional = !!f.querySelector(".optional");
      const required = !!f.querySelector(":required");
      return optional === required;          // optional must mean not required, and vice versa
    }).length,
  );
  expect(mismatches).toBe(0);
});

Edge Cases and Failure Modes

Double announcements. A label containing a visible asterisk and an input with required is often announced “Name star, required”. If an asterisk is mandated, wrap it in aria-hidden="true" and rely on required for the announcement.

Conditionally required fields. A field that becomes required when another option is chosen must update both its required attribute and its marker. Toggle a class or re-render the label text in the same function that sets required, so they cannot disagree — the pattern in conditional field validation on selection.

Groups. For radio groups and checkbox sets, put the marker in the <legend>, not on each option, and put required on the radios (one is enough for a group, but set it on all for clarity).

Colour-only markers. A red label for required fields fails WCAG 1.4.1. Colour may reinforce a text marker, never replace it.

How the Indicator Is Announced

Screen readers build a field’s announcement from several sources, and the indicator should appear exactly once in it. The accessible name comes from the label, so “(optional)” inside the label is spoken as part of the name. The required state comes from the required attribute, spoken as “required” after the name. Hints and errors come from aria-describedby, spoken last. Putting the word “required” in the label and setting the attribute produces “Full name required, required” in some screen readers — one reason marking optional fields is the cleaner convention when most fields are required.

Label, state and description in one announcement A form field showing how its label with an optional marker, its required state and its hint combine into what a screen reader announces. Your application Full name Ada Lovelace 1 Company (optional) 2 Phone number (optional) 3 We'll only call about this application Send application 1 Announced "Full name, edit text, required" — the attribute supplies "required" 2 Announced "Company (optional), edit text" — the marker is part of the name 3 The hint follows as the description, read after name and state
The marker belongs to the name, the requirement to the state, and the hint to the description; each is spoken once.

Styling the Indicator Across Themes and States

Once the marker is text, styling it is ordinary typography: a lighter weight and a muted colour that still meets 4.5:1 contrast in both light and dark themes. Resist making it so faint that it becomes decorative; “(optional)” is information. When a field enters an error state, the label typically changes colour through .field:has(:user-invalid) label; make sure the marker inside the label inherits or overrides deliberately, so an erroring optional field does not look as though “(optional)” itself is the error. For required-field markers in dense forms, a small pill style can help scanning — but keep the text inside it, as described in styling validation states with the :has() selector.

.field:has(:user-invalid) label { color: var(--error-text, #b91c1c); }
.field:has(:user-invalid) label .optional { color: var(--muted-text, #475569); }   /* marker stays neutral */
:root[data-theme="dark"] .optional { color: #cbd5e1; }                           /* 4.5:1 on dark surfaces */

Reducing the Number of Required Fields

The most effective required-field indicator is a shorter list of required fields. Every field you mark optional, or remove, is one fewer chance for a validation error and one less reason to abandon the form. Before styling markers, audit each required field against a simple question — what breaks if this is empty? — and make anything with a weak answer optional or remove it. The fields that remain required then deserve the clear, consistent marking described above.

Why Marking Required Fields Is an Accessibility Requirement

WCAG 3.3.2 Labels or Instructions asks for labels or instructions when content requires user input, and “this field must be filled in” is exactly such an instruction. Relying on users discovering requirements through errors fails that criterion in spirit and makes forms slower for everyone. The required attribute satisfies the programmatic side — screen readers announce it and the constraint model enforces it — while the visible text satisfies the visual side. Having both, in agreement, is what makes the indicator work for every user. The checklist in WCAG 3.3.3 error suggestion patterns covers the message that follows when a required field is left empty anyway.

Frequently Asked Questions

Should I mark required fields or optional fields?

Mark whichever is the minority. If most fields are required, mark the optional ones with "(optional)" and say at the top that all other fields are required; if most are optional, mark the required ones.

Is an asterisk enough to indicate a required field?

Not on its own. Many users do not know what it means and screen readers may read it as "star". Use a word, or explain the asterisk at the top of the form and rely on the required attribute for screen readers.

Should the required indicator be added with CSS ::after?

Avoid it for meaning. Generated content is not consistently announced and is not translated with the page. Put the word in the markup and use CSS only to style it.

Do I need aria-required on native inputs?

No. The required attribute already exposes the requirement to assistive technology. Use aria-required only on custom widgets that cannot take the native attribute.

← Back to CSS Validation State Styling