Design System

Design System

Form

Forms

Open in Storybook: Form (opens in a new tab)

On this page

Overview

A form is native controls, each with a visible label and one message above it. Focus goes where the problem is, and what arrives without focus is said with ariaNotify() on the field: no live region, no toast.

The rule. Pick the control that makes the mistake impossible, then validate what is left. Every control has a visible label, “(required)” or “(optional)” in its text, and one message above it. Judging never moves focus; only a submit does: one error → the field, two or more → the error list heading. Results that arrive without focus are said by one ariaNotify() call on the field; the same words are already in its visible message.

Anatomy

  1. Form

    form novalidate aria-labelledby

    Named by its heading, so it is a landmark: name only the page’s main form. required, min, max and type stay: they still drive the checks and autofill.

  2. Heading

    h2 · tabindex="-1"

    Names the form. Its level comes from the outline of the page. Focusable by script, so Back can return to it.

  3. Convention

    p

    “(required)” or “(optional)”, said once at the top. Then only the optional fields say what they are.

  4. Error list

    section > h3 + ul

    After a failed submit with two or more errors, at the top and never sticky. The heading takes focus; each link focuses its field; items leave as they are fixed.

  5. Label

    label for

    Visible, before the control, with “(required)” or “(optional)” in its text. A placeholder is not a label.

  6. Message

    p id

    One for each field: the hint, then “Error: …”, “Correct.” or “Fixed. It is correct now.” The same id in every state.

  7. Control

    input aria-invalid aria-describedby

    A native control. aria-invalid is always there, and aria-describedby points at the message.

  8. Help

    details

    Beside a hard field: explains what is hard to answer (3.3.5).

  9. Submit

    button type="submit"

    Named for the action, never “Submit”. Never disabled to force a valid form.

Usage

Use cases

Fields with their hint and an error that gives the fix. Label, format and limits are in the hint before the mistake. Each is the real form and works: answer, pause or submit.

  • Email

    Suggest the domain.

    For your receipt. Example: ana@example.com Error: Email domain looks wrong. Did you mean ana@gmail.com?

    type="email" · autocomplete="email" · a hint, then the error

    The error says what is wrong and the fix: “Did you mean ana@gmail.com?”

  • Phone

    Format with the country code.

    With country code. Example: +39 333 123 4567 Error: Phone has no country code. Add it, as in +39 333 123 4567.

    type="tel" · the format in the hint

    Never type="number": a phone has a + and spaces.

  • Date

    Range in the hint, nearest valid in the error.

    Monday to Saturday, 1 to 30 November. Error: That day is a Sunday. Try Monday, 23 November.

    type="date" · min and max

    Typing must work. The error offers the next valid day.

  • Password

    Rules in the hint.

    12 characters or more, with a number. Error: Password has 9 characters. Add 3 more.

    autocomplete="new-password" · Show is a button with aria-pressed

    Paste is allowed. Repeat the password only for security.

  • Textarea

    Count what is missing.

    20 to 500 characters. Error: You have 9, so add 11 more.

    textarea · the limit in the hint

    “You have 9, so add 11 more.” says how far, not only that it is short.

  • File

    Name, limit, fix.

    PDF or JPG, up to 5 MB.

    type="file" · accept · the types and the size in the hint

    Choose a HEIC picture to see the error: it names the file and the fix.

  • Colour

    Offer one that passes.

    Needs 4.5:1 on white. Error: #9a9a9a is 2.8:1 on white. Try #777777 (4.5:1).

    #9a9a9a

    type="color" · the value shown as text

    The error measures the contrast and offers the nearest colour that passes.

  • Taken name

    Offer two free ones.

    3 to 20 letters, numbers or dots. Error: “ana” is taken. Try ana.r or ana23.

    autocomplete="username" · spellcheck="false"

    Try “ana”, “admin” or “mica”: they are taken.

  • One of 2 to 6

    Radios in a fieldset.

    Delivery (required)

    One message for the whole group.

    fieldset + legend · one shared message

    The options are label and input as siblings; each carries aria-invalid.

  • One of about 7 or more

    A select, first option empty.

    select · the first option has no value

    Never select multiple: one arrow key replaces the whole selection.

  • Several

    A checkbox group.

    Sessions (required)

    Choose 1 or 2.

    fieldset + legend · limits in the hint and the error

    A group cannot be required: the legend says it.

  • Booking

    A whole form: errors, review and result.

    Book a tasting

    All fields are required unless marked (optional).

    As on your ID.

    For your receipt. Example: ana@example.com

    With country code. Example: +39 333 123 4567

    Monday to Saturday, 1 to 30 November.

    Seats

    Up to 4 per booking.

    Why we ask for your country

    We show prices in your currency and check delivery.

    form named by its heading · novalidate · the error list

    Submit it empty, then with one mistake, then right.

Input types

The rule. Pick the type for the data, not for the look. The type sets the role, the keys and the mobile keyboard. When a type fights the user (number for a card number, date for a birth date), use type="text" with inputmode and autocomplete instead. Every type still needs a visible label. All of them follow the field rules: one message above the control, aria-describedby, novalidate with our own messages.

  • type="text"

    Any short text: names, postcodes, card numbers.

    Role: textbox

    Any text · default keyboard

  • type="email"

    An email address.

    Example: ana@example.com

    Role: textbox

    @ keyboard · autocomplete="email"

  • type="password"

    A secret.

    Role: textbox, protected

    autocomplete="current-password" · paste allowed

  • type="tel"

    A phone number.

    Include the country code

    Role: textbox

    Phone keypad · autocomplete="tel"

  • type="url"

    A web address.

    Start with https://

    Role: textbox

    / and .com keys · autocomplete="url"

  • type="number"

    A count: quantity, guests.

    From 1 to 10

    Role: spinbutton

    ↑ ↓ step · min, max, step announced

  • type="range"

    An approximate value.

    Shown as a value next to the slider

    Role: slider

    ← → ↑ ↓ · Home · End · Page Up/Down

  • type="date"

    A calendar date.

    Format follows the browser’s language

    Role: date (segments: day, month, year)

    Type each segment · ↑ ↓ change it · picker button

  • type="time"

    A time of day.

    Role: time (segments)

    Type or ↑ ↓ per segment · step for seconds

  • type="datetime-local"

    Date and time, no zone.

    Time in Rome (CET)

    Role: date and time (segments)

    No time zone: say which one

  • type="month"

    Month and year.

    Format: MM/YYYY

    Role: month and year

    Not in Firefox or desktop Safari: falls back to text

  • type="week"

    ISO week and year.

    Role: week and year

    Rarely understood; not in Firefox or desktop Safari

  • type="color"

    A colour value.

    Show the name, not only the hex

    Plum · #5e548e

    Role: button that opens a colour picker

    Enter/Space opens the OS picker · value read as hex

  • type="checkbox"

    On/off, or many of a set.

    Role: checkbox

    Space toggles · Tab to each one

  • type="radio"

    One of a set.

    Delivery

    Role: radio, in a group

    One Tab stop · arrows move and select

  • type="file"

    An upload.

    Role: button, named by label + chosen file

    Enter/Space opens the file dialog · accept, multiple

  • type="hidden"

    Data the server needs.

    Sent with the form: bundle-id = b-2026-11

    Role: none

    Not rendered, not in the accessibility tree, not in the Tab order

  • type="submit"

    Sends the form.

    Role: button

    Enter in a field submits · value is the name

  • type="reset"

    Clears every field.

    Role: button

    Clears every field: avoid

  • type="button"

    A scripted action.

    Role: button

    No default action: needs script

  • type="image"

    A graphical submit.

    Role: button (submit)

    alt is the name: required

Input types
TypeUse forautocompleteWatch out
textAny short text: names, postcodes, card numbersname, postal-code, cc-number…Add inputmode="numeric" for digits that are not quantities
emailAn email addressemailNative messages are vague: novalidate + our message. spellcheck="false"; multiple for a list
passwordA secretcurrent-password, new-passwordNever block paste (3.3.8). Rules visible before typing. Show password: a button with aria-pressed
telA phone numbertelAccept spaces, +, brackets; no pattern that rejects real numbers
urlA web addressurlNeeds the scheme: say “Start with https://” or add it for the user
searchA search queryoff or a custom tokenPut it in search or form role="search"; visible label or a labelled button; a real Clear button
numberA count: quantity, guests—Not for card numbers, postcodes, phone or IDs (leading zeros, spinners, wheel). Use text + inputmode="numeric"
rangeAn approximate value—Show the value as text; aria-valuetext when a unit matters (“40 percent”). Pair with number when precision matters
dateA calendar datebday (prefer three fields)Format follows the browser language; the picker is not fully accessible everywhere. Birth dates: day, month, year text fields
timeA time of day—12/24 h follows the locale. step="1" adds seconds
datetime-localDate and time, no zone—Has no time zone: state it in the hint
monthMonth and yearcc-exp (prefer text MM/YYYY)Not in Firefox or desktop Safari: falls back to text, so always give the format
weekISO week and year—Same support gap; few people know their week number. Prefer a date
colorA colour value—The value is read as hex: show a name too. Fixed choices: radios as swatches
checkboxOn/off, or many of a set—Many: fieldset + legend. indeterminate only through script. One required box (terms) needs its own message
radioOne of a set—fieldset + legend, one name. Arrowing selects, so never act on change (3.2.2)
fileUpload—Keep the native input, even disabled: never display: none. Style ::file-selector-button. Say type and size in the label; list chosen files as text. Drag and drop is an extra, not the only way
hiddenData the server needs—Not for anything the user must know; not a security measure
submitSends the form—Prefer button type="submit": it can hold markup
resetClears every field—Avoid: one slip wipes the form. If needed: confirm or undo
buttonA scripted action—Prefer button type="button"
imageA graphical submit—alt is the name and is required. Prefer button type="submit" with text

Props

Props
PropWhat it doesDefault
Form title · levelThe name of the form: its heading, and its level. The heading of the list of errors is one under it. Left out, the level FormLevel gives2
Form convention · marksSaid once at the top. With it, only an optional field says what it is; marks (optional or both) sets that by handfollows convention
Form submitThe submit button, named for the action—
Form onValid · onInvalidA submit with every field right, with the answers; and one that found something wrong—
Form startWithErrors the form opens with, by field name: the state of a drawing or a demo. Nothing is said for them—
Field name · label · hintThe name of the control, what it asks (the form adds “(required)” or “(optional)”), and the format and limits before the mistakeRequired: name, label
Field type · asAn input type, checkbox for one box, or as a textarea or a select (with options and empty, the words of its empty first option)text
Field required · missingWhether an empty answer is wrong, and the fix it saystrue
Field check(value, answers) => string: empty when the answer is right, else what is wrong and the fix—
Field dependsOnFields this one is checked against: it is looked at again when one changes—
Field help{ summary, body }: a details beside a hard field—
Field unit · valueTextFor a range: the unit drawn after its value, and the value in words—
FieldGroupname, legend, kind (radio or checkbox), options, inline, and check(values, answers)—
Review · Result · SuccessThe step before the result, the result, and the “Success:” line. A heading takes focus as it appears (focus), and is described by its line—

Example

import { Field, FieldGroup, Form } from '@/components/Form'

<Form
  title="Book a tasting"
  convention="All fields are required unless marked (optional)."
  submit="Review my booking"
  onValid={(answers) => review(answers)}
>
  <Field
    name="email"
    label="Email"
    type="email"
    hint="For your receipt. Example: ana@example.com"
    autoComplete="email"
    check={(value) =>
      value.includes('@') ? '' : 'Email has no @. Add it, as in ana@gmail.com.'
    }
  />
  <Field name="phone" label="Phone" type="tel" required={false} />
  <FieldGroup
    name="seats"
    legend="Seats"
    kind="radio"
    inline
    hint="Up to 4 per booking."
    options={[
      { value: '1', label: '1' },
      { value: '2', label: '2', defaultChecked: true },
    ]}
  />
</Form>

Specs

Structure

Per field, in order: label, hint or message, control, help. One column. Submit named for the action.

Structure
PartRule
Field orderLabel, hint or message, control, help. One column; two only when the DOM order stays row by row.
fieldset + legendOne question with several controls: radios, checkboxes, a split date. Sections of different fields: heading + list, not a fieldset. Label and input as siblings, aria-invalid and aria-describedby on every option, one shared message. A checkbox group cannot be required: say it in the legend.
No select multipleA checkbox group instead: one arrow key in a multiple select replaces the whole selection.
Label + inputSiblings: label for + id. No input inside its label.
Optional“(optional)” in the label. Empty is never “correct”: say nothing.
autocompletename, email, tel, country, street-address… on personal data (1.3.5).
NeverA placeholder as label, a floating label, a div rebuilt as a select, an input inside its label.

States

States
StateSignalsForced colours
DefaultThe hint only. aria-invalid="false".—
Error“Error:” prefix · red text · 4px bar · 3px border. aria-invalid="true".Prefix and border width survive; the bar is a border-inline-start, not a background.
CorrectThe words “Correct.” in green, once judged.Words carry it.
FixedThe words “Fixed. It is correct now.” in green, after an error.Words carry it.
FocusThe site’s ring on the control. Never removed.outline, never box-shadow alone.
Error listHeading · links underlined · 6px bar.Links keep LinkText.

Layout

Layout
WidthLayoutWhat changes
1280 CSS px · 100%One column of fields, max 40remError list above the first field. Buttons in DOM order.
640 CSS px · 200%Same column, full widthNothing moves sideways; messages stay above controls.
320 CSS px · 400% (1.4.10)One column, buttons full widthList items wrap. No toast or sticky bar that could cover the focused control (2.4.11).
390 · mobileSame as 320 + on-screen keyboardThe focused field scrolls above the keyboard; the error list scrolls with the page.
Text 200% (1.4.4)Control height from contentMessages and labels wrap; no fixed heights, no truncation.

Markup

<form novalidate aria-labelledby="book-h">
  <h2 id="book-h" tabindex="-1">Book a tasting</h2>
  <label for="email">Email (required)</label>
  <p id="email-msg">For your receipt. Example: ana@example.com</p>
  <input id="email" type="email" required autocomplete="email"
         aria-invalid="false" aria-describedby="email-msg" />

  <fieldset>
    <legend>Sessions (choose 1 or 2, required)</legend>
    <p id="ses-msg">Error: You chose 3 sessions. Untick 1 session.</p>
    <input id="s1" type="checkbox" name="ses" aria-invalid="true" aria-describedby="ses-msg" />
    <label for="s1">Monday</label>
    …
  </fieldset>
</form>

Tokens

Component tokens

Component tokens
PartPropertyTokenValue
ErrorText, bar and border—#c8102e · 5.9:1 on white
SuccessText and bar—#1a7f37 · 5.1:1 on white
ControlBorderink-soft1.5px · #4b4b55 · 8.6:1 on white
ControlCornerradius8px
Error controlBorder—3px, the padding gives back what it takes
MessageBar—4px
Error listBar—6px
FocusRingfocus-ring-primaryThe site’s ring: #0066ff, 3:1 or more against both

Guidelines

Controls by scenario and context

Pick the control that makes the mistake impossible, then validate what is left.

Controls by scenario
ScenarioControlRule, and the error to give
Short text (name)type="text"autocomplete, spellcheck="false". Say what is wrong with the value.
Emailtype="email"Suggest the domain: “Did you mean ana@gmail.com?”
Phonetype="tel"Format with country code in the hint. Never type="number".
URLtype="url"“Start with https://”
Digits that are not a quantity (code, ID)type="text" + inputmode="numeric"Not type="number".
Quantity with limitstype="number" + min, maxQuote the value and the limit. Under about 5 values: radios.
Date, timetype="date" | "time" | "datetime-local" + min, maxRange in the hint, nearest valid in the error. Typing must work.
Week, monthtype="week" | "month"A text box in some browsers: hint the format.
One of 2 to 6Radios in a fieldsetOne shared message.
One of about 7 or moreselect, first option empty“Choose a country.”
One of many, typedinput + datalistSuggestions are not validation: check the answer.
SeveralCheckbox groupLimits in hint and error. Never select multiple: one arrow key replaces the selection.
One yes/no (terms)Single checkboxaria-invalid on it.
On/off, acts at onceSwitchIn a form that sends later, a checkbox.
Secrettype="password"Rules in the hint. Show is a button with aria-pressed. autocomplete="new-password".
Long texttextareaLimit in the hint. “You have 9, so add 11 more.”
Filetype="file" + acceptTypes and size in the hint. Error: file name, limit, fix.
Approximate value on a scaletype="range" + a value as textaria-valuetext with the unit. Exact value: number.
Colourtype="color"Measure contrast, offer one that passes.
Usernametype="text"Taken: offer two free ones.
Controls by context
ContextDo
Acts at once (filter, sort)select or buttons. No error flow.
Sends laterSubmit, validate, review.
Money, legal, data changeReview step: confirm or reverse (3.3.4).
Sign-in, sign-upautocomplete: username, current-password, new-password, one-time-code. Paste allowed. No puzzle or retyping from memory (3.3.8). No suggestion that leaks (3.3.3).
Personal dataautocomplete token (1.3.5).
Already known or enteredPrefill, do not ask again. Repeat only the password (3.3.7).
Depends on another fieldAlways in the DOM. The hint says when it is needed. Recheck when the other changes.
Optional“(optional)”. Empty is never “correct”.
Submitting takes timeShow “Sending…” on the page and call ariaNotify() on the submit button, then the result. Keep the answers on failure.
Time limitWarn, let extend, keep the answers (2.2.1).
Several stepsValidate each step, focus its heading, Back keeps answers.
In a dialog or drawerclosedby="closerequest". Plain form if it must stay open. Cancel with formnovalidate.
Banner after submitOne ariaNotify() on the submit button: normal for the result, high only when sending failed and the next step is the user’s. Dismiss moves focus to a real control.
File drop zoneReal type="file" first. Drag is extra (2.5.7).
Shown, not editablereadonly, not disabled: disabled leaves the tab order and the submit. Say why in the hint.
Searchsearch or role="search", labelled input, button named Search.
Narrow screenOne column, 24 px targets.

Message

What is wrong, then the fix. In the list: “Field: message”.

Message
FieldDon’tDo
EmailInvalid inputError: Email has no @. Add it, as in ana@gmail.com. · Did you mean ana@gmail.com?
DateInvalid dateError: That day is a Sunday. Try Monday, 23 November.
SessionsToo many selectedError: You chose 3 sessions. Untick 1 session.
CommentToo shortError: You have 9 characters, so add 11 more.
UsernameNot availableError: “ana” is taken. Try ana.r or ana23.
Sign inWrong password for ana@gmail.comError: Email or password is not right. No suggestion: it would leak which one.
Error list itemPlease fix the errorsEmail: Email has no @. Add it, as in ana@gmail.com.

Debounce

Delay the judging, never the value in the field or the focus ring. No success criterion or numbered technique asks for it: it serves 3.3.1, 4.1.3 (ariaNotify(), with the words on the page) and 3.2.2.

Debounce
RuleHow
Written inputs waittext, email, tel, url, number, search, password, textarea. Judge 1 s after the last key. Every key restarts the wait.
Not waited forBlur and submit (at once). Choices: select, radio, checkbox, date, time, colour, range. A press that moves focus waits for the click, so a message does not move the button from under the pointer.
Cancel pending waitsSubmit, reset, refill and unmount cancel them. No timer outlives its form.
A fix shows at onceThe error leaves and green appears as soon as the value is right. Only saying it waits.
Speak only on changeNever repeat “correct” at each pause.
A pause is a hint, not a verdictSlow typing, dictation and switch users stop mid-answer. The result never takes focus, disables submit or blocks it. Submit checks everything.
Never delayThe value in the field and the focus ring.

Success and review

A valid submit goes to a review step. Confirm goes to a result. Both headings take focus and are described by a “Success: …” line that uses the person’s answers.

Success and review
RuleHow
Success is a p under the heading<p>Success: …</p>, green bar. The heading that takes focus has aria-describedby to it.
Use the person’s answersSeats, date, total, where it goes: “2 seats on Monday, 23 November. Total 48 €. Tickets go to ana@gmail.com.”
Review shows everythingEvery answer, the total, Confirm, Back. Cancel after confirming (3.3.4, 3.3.6).
Change buttons say what they changearia-labelledby="own-id question-id": heard as “Change Seats”. Change moves focus to that field.
Never ask twiceAnswers carry over; nothing is typed again (3.3.7).

Feedback by scenario and context

Each row is one scenario from Focus and what is said. Each column shows what that moment gets at that size. The drawings are the real components, small: the field, the list of errors, the buttons, the Success line and the ariaNotify() call.

  • Solid: required in that context, never dropped
  • Dashed: optional, or only if there is room
  • ariaNotify()Said with field.ariaNotify(): no element, nothing added to the DOM; the words are in the visible message

Zoom 400% · 320 CSS px. At 200% text messages wrap above the control.

  • Submit, one error

    Checkout · one field wrong

    Live example

    Desktop · 640 px and upFull width, one column of fields

    Field (focused) · message · Continue

    Focus moves to the field; its message is read with it. No list for one error, no ariaNotify().

    Smaller viewport · 400–639 pxNarrow column or card

    Field · Continue

    Same. Message stays above the control.

    Zoom 400% · 320 CSS pxReflow

    Field · Continue

    One column. The message wraps; nothing floats over the field.

  • Submit, two or more

    Long form

    Live example

    Desktop · 640 px and upFull width, one column of fields

    Error list (heading focused) · fields

    Errors rendered first, then focus to the list heading. Links focus the field. Items leave as they are fixed.

    Smaller viewport · 400–639 pxNarrow column or card

    Error list · fields

    List at the top of the form, never sticky.

    Zoom 400% · 320 CSS pxReflow

    Error list · fields

    List items wrap; heading still takes focus.

  • Typing

    Email, then a pause

    Live example

    Desktop · 640 px and upFull width, one column of fields

    Field · message · ariaNotify()

    ariaNotify()

    Visible fix at once. Spoken 1 s after the last key (every key restarts the wait), or at once on blur.

    Smaller viewport · 400–639 pxNarrow column or card

    Field · ariaNotify()

    ariaNotify()

    Same.

    Zoom 400% · 320 CSS pxReflow

    Field

    ariaNotify()

    Same; the call does not depend on the viewport.

  • Fixing a listed error

    Two were listed

    Live example

    Desktop · 640 px and upFull width, one column of fields

    Field · “Fixed.” · count · ariaNotify()

    ariaNotify()

    Choosing speaks at once. The count is read from the list as drawn and shown on the field fixed last.

    Smaller viewport · 400–639 pxNarrow column or card

    Field · ariaNotify()

    ariaNotify()

    Same.

    Zoom 400% · 320 CSS pxReflow

    Field

    ariaNotify()

    Count in the message under the field, never a sticky counter.

  • Sign-in failed

    No suggestion: it leaks

    Live example

    Desktop · 640 px and upFull width, one column of fields

    Field (focused) · message · Sign in

    One error, so focus goes to the email field. Paste and password managers allowed (3.3.8).

    Smaller viewport · 400–639 pxNarrow column or card

    Field · Sign in

    Same.

    Zoom 400% · 320 CSS pxReflow

    Field

    Same.

  • Review step

    Valid submit

    Live example

    Desktop · 640 px and upFull width, one column of fields

    Review heading (focused) · answers · Change · Confirm · Back

    Heading described by “Success: Your answers are saved. Check them, then confirm.” Change → that field.

    Smaller viewport · 400–639 pxNarrow column or card

    Heading · answers · Change · Confirm

    Answers stack under each question.

    Zoom 400% · 320 CSS pxReflow

    Heading · answers · Change · Confirm

    Buttons full width, 44 px tall, in DOM order.

  • Confirmed

    Result page

    Live example

    Desktop · 640 px and upFull width, one column of fields

    Result heading (focused) · Success line

    “Success: what happened, what is next”, with the person’s answers. Same words every time.

    Smaller viewport · 400–639 pxNarrow column or card

    Result heading · Success line

    Same.

    Zoom 400% · 320 CSS pxReflow

    Result heading · Success line

    Same; no toast.

Input attributes

Attributes that change accessibility
AttributeDoesRule
label for / idThe accessible nameAlways visible. placeholder is not a label
autocompleteLets browsers and assistive technology fill and identify the purposeRequired for personal data (1.3.5). Use the exact token
inputmodeMobile keyboard only, no validationnumeric, decimal, tel, email, url, search, text, none
enterkeyhintThe Enter key label on mobileenter, done, go, next, previous, search, send
requiredRequired state; native checkAlso “(required)” in the visible label; our own message with novalidate
aria-describedbyHint and message read after the namePoints at the one message above the control
aria-invalidInvalid stateAlways present: true after a submit or after typing stops, false otherwise
readonly / disabledReadonly: focusable, read, submitted. Disabled: skipped, not submittedPrefer readonly for values users need to read
maxlengthSilently stops typingShow the limit and a count; let them paste then explain
pattern + titleNative check; title is not a reliable messageOur own message in the field’s message slot
spellcheck, autocapitalizeCorrection on mobileOff for email, codes, usernames
Other form controls
ElementHeard asRule
textareaedit text, multi-lineLabel and message like any field; rows for the visible height; resize: vertical
selectcombo box / pop-up buttonNative first; size or multiple make it a list box. Option text is the name
datalistsuggestions on a text inputSupport for screen readers and zoom is uneven: use a combobox pattern when it matters
buttonbuttontype always set: submit, button or reset
fieldset + legendgroup with a nameEvery set of radios or checkboxes, and related fields such as an address
outputstatus (role="status")A live region: not for form feedback. Use ariaNotify()

Accessibility

Focus and what is said

Where focus goes and what is said, for every moment of the form.

Focus and what is said
WhenFocusSaid
Submit, one errorThe fieldFocus reads it: label, invalid, the message.
Submit, two or moreThe error list headingFocus reads it. No ariaNotify().
TypingStaysariaNotify() on the field, after 1 s pause or blur: “Email: not correct. …”, “Email: correct.”, “Email: fixed. It is correct now.”
Choosing (select, radio, checkbox, date)StaysSame, at once.
Fix, two or more were listedStaysAdds “N errors left.” or “No errors left.” Shown on the field fixed last.
Optional field emptyStaysNothing.
Valid submit, review stepThe review headingHeading, described by “Success: …”.
ChangeThat fieldFocus reads it.
Confirm, CancelThe result headingHeading, described by “Success: what happened, what is next”.
Form reset or refilledStaysClear messages, count and spoken state.

Announcements

Focus is the announcement for headings and for the field it lands on. Everything that arrives without focus is said with element.ariaNotify(): nothing enters the DOM, no live region. The same words are already in the field’s visible message.

Announcements
RuleWhy
field.ariaNotify(text, { priority: 'normal' })Host: the field the message is about. It is in the accessibility tree, it stays, and its lang sets the voice. There is no element to add and nothing to keep empty.
Never call it for what focus readsFocus already reads headings and fields; calling too says everything twice.
Written inputs: see DebounceChoices are said at once.
Speak only on changeOne call per event; normally only the latest call is spoken. Texts that settle together are one string.
Count from the list as drawn“2 errors left.” is read from the list items, never from a stored number.
No role="alert", no live region, no fallbackAn alert re-reads the list at every fix; a live region is a second channel. ariaNotify() only.
Page first, call secondWrite the visible message and set aria-invalid, then call. Never on load.
The words are on the pageThe call says the label plus the state text that is already visible in the message (4.1.3). It cannot be reread, so nothing exists only in speech.
normal; high only when sending failedhigh interrupts speech: only when the task stopped and the next step is the user’s.
Feature-detect'ariaNotify' in Element.prototype; where it is missing, do nothing. Support: Chrome and Edge 141, Firefox 150, Safari 27.

Keyboard

Keyboard
KeyDoes
Tab · Shift+TabOne stop for each native control, in DOM order, and one for a group of radios. Never a tabindex above 0, and no trap
EnterSubmits from a field. In a textarea it is a new line
SpaceToggles a checkbox, presses Show password, opens the help
Arrow keysChoose in a group of radios, move in a select, step a number, change a segment of a date
Home · End · Page Up · Page DownMove a slider

Roles and properties

What the browser exposes, as the tests read it. Nothing here was heard in a screen reader yet.

Roles and properties
PartExposed
FormA form landmark named by its heading, when it has one
LabelThe name of the control, with “(required)” or “(optional)” in it, so the name contains the visible words (2.5.3)
MessageThe description of the control, through aria-describedby: the hint, then the state
ControlIts state: aria-invalid is true or false, never absent
GroupA group named by its legend; every option carries aria-invalid and the same description
Error listA heading that takes focus, and links named “Field: message”
Review and resultA heading described by its “Success: …” line, so both are read once
Show passwordA button that keeps its name and says its state with aria-pressed

Support

Yes is run in the browser tests of the component; No is not run here.

Support
BrowserChecked
ChromiumYes
WebKit (Safari)No
FirefoxNo

WCAG criteria

What each success criterion asks of a form, and what meets it.

WCAG criteria
Success criterionLevelMet by
1.3.1 Info and RelationshipsAlabel for, fieldset + legend, aria-describedby to one message
1.3.2 Meaningful SequenceADOM order = visual order; message above the control
1.3.5 Identify Input PurposeAAautocomplete tokens on personal data
1.4.1 Use of ColorA“Error:”, “Correct.” words, bar and border width
1.4.3 Contrast (Minimum)AARed #c8102e 5.9:1, green #1a7f37 5.1:1
1.4.10 ReflowAAOne column at 320 px
1.4.11 Non-text ContrastAAControl outlines and focus ring 3:1
2.1.1 KeyboardANative controls, Enter submits
2.1.2 No Keyboard TrapANo trap; dialogs close with Escape
2.2.1 Timing AdjustableATime limit: warn, let extend, keep the answers
2.4.3 Focus OrderASubmit: field or list heading; links focus fields
2.4.6 Headings and LabelsAALabels say what to enter; submit named for the action
2.4.7 Focus VisibleAAThe site’s ring on every control
2.4.11 Focus Not Obscured (Minimum)AANo toast or sticky bar over the form
2.5.3 Label in NameA“(required)” in the label text; Change buttons named by question
2.5.7 Dragging MovementsAAFile drop zone: a real type="file" first
2.5.8 Target Size (Minimum)AA24 × 24 for options and links
3.2.2 On InputAJudging never moves focus or changes the page
3.3.1 Error IdentificationAError text names the field and what is wrong
3.3.2 Labels or InstructionsAFormat and limits in the hint before the mistake
3.3.3 Error SuggestionAAThe fix: “Try Monday, 23 November.” (not when it leaks)
3.3.4 Error Prevention (Legal, Financial, Data)AAReview step, Confirm, cancel after confirming
3.3.5 HelpAAAdetails beside hard fields
3.3.7 Redundant EntryANever ask twice
3.3.8 Accessible Authentication (Minimum)AAPaste and password managers allowed
4.1.2 Name, Role, ValueANative controls with visible labels; Show is a button with aria-pressed
4.1.3 Status MessagesAAThe words are in the visible message; field.ariaNotify() speaks them without moving focus. Nothing depends on the call being heard

Related

This section is still in progress.

References

This section is still in progress.