Skip to content
academia.sh

Lesson 02 / 25

Text Inputs

The separate places the label, help text, error message, and placeholder occupy in the accessibility tree; computing the name and description, auditing the error binding, and the text field's keyboard contract.

Contents

In the button specification, the name came from a single source and did not change. The catalog’s search field and the borrowing form’s membership number field are different in this respect: four separate texts can stand around the field on screen — the label, the help text, the error message, and the placeholder inside the field. All four appear to the eye at the same distance.

In the accessibility tree, though, the four do not sit in the same place. One becomes the field’s name, one its description, one falls outside both, and one, when written incorrectly, overrides the others. This lesson computes that distribution.

The Problem the Field Solves

The text field is the control where the user types a value that cannot be enumerated in advance: a search string, a membership number, a note for staff. The measure is enumerability. If the “borrow duration” field in the borrowing form can only take three values, it is not a text field; when the options can be enumerated, a selection control or a dropdown list is used instead.

The second boundary is format. Asking for values like a date, time, color, or file as free text makes the user guess the format and leaves the entire validation burden to the code. Defined input types exist for these values.

The Native Element First

The counterpart is the input element; textarea for multi-line text. The type attribute determines both the field’s role and its validation: search brings the search-box role, email brings email-address validation, number brings the bounded number-field role, date brings date selection.

Choosing the type does three things at once. The role in the accessibility tree changes; the browser’s built-in constraint validation engages; on touch devices, the keyboard suited to the field opens. None of the three is written separately.

What kind of personal data a field collects can also be declared separately. The autocomplete attribute states a value’s purpose programmatically on fields like member name, address, and email address; this is the counterpart of the 1.3.5 Identify Input Purpose criterion, and it also lets the user autofill their own data.

Where the Four Texts Sit in the Tree

The label is the field’s name, and it is persistent. It is written with the label element; the for attribute ties to the field’s id, or the field is nested inside the label.

Help text is format or constraint information: “The 8-digit number on the card.” It is not the field’s name, it is its description, and it is tied with aria-describedby. It is preferred to sit above, not below, the field on screen; text below the field can end up under the keyboard that opens while the field is being filled in.

The error message is also a description, and it is tied with the same mechanism. It is written alongside the help text, not in its place.

The placeholder is the example value sitting inside the field, and it disappears once the field is filled in. It is last in the name computation order: it turns into the name only if no other source exists. This does not mean it can be used in place of a label.

// field.mjs — computing name, description, and state declarations in a text field

// Text-bearing nodes in the document (label and aria-describedby targets)
const DOCUMENT = {
  "member-help": "The 8-digit number on the card.",
  "member-error": "Membership number must be 8 digits.",
  "return-help": "At most 21 days from now can be selected.",
  "label:search": "Search the catalog",
  "label:member-no": "Membership number",
  "label:return": "Return date",
};

// Name order: aria-labelledby -> aria-label -> label element -> title -> placeholder
function name(a) {
  if (a["aria-labelledby"]) {
    const p = a["aria-labelledby"].split(" ").map((k) => DOCUMENT[k]).filter(Boolean);
    if (p.length > 0) return [p.join(" "), "aria-labelledby"];
  }
  if (a["aria-label"]) return [a["aria-label"], "aria-label"];
  if (DOCUMENT["label:" + a.id]) return [DOCUMENT["label:" + a.id], "label element"];
  if (a.title) return [a.title, "title (last resort)"];
  if (a.placeholder) return [a.placeholder, "placeholder (last resort)"];
  return ["", "no name"];
}

// Description order: aria-describedby -> title -> placeholder (if not already used for the name)
function description(a, nameSource) {
  if (a["aria-describedby"]) {
    const p = a["aria-describedby"].split(" ").map((k) => DOCUMENT[k]).filter(Boolean);
    if (p.length > 0) return [p.join(" "), "aria-describedby"];
  }
  if (a.title && nameSource !== "title (last resort)") return [a.title, "title"];
  if (a.placeholder && nameSource !== "placeholder (last resort)") return [a.placeholder, "placeholder"];
  return ["", "no description"];
}

function states(a) {
  const d = [];
  if (a.required || a["aria-required"] === "true") d.push("required");
  if (a["aria-invalid"] === "true") d.push("invalid");
  if (a.readonly) d.push("read-only");
  if (a.disabled) d.push("disabled");
  return d.length ? d.join(", ") : "-";
}

// --- Catalog search field and borrowing form fields -------------------------
const FIELDS = [
  ["label + placeholder",
   { id: "search", type: "search", placeholder: "title, author, or ISBN" }],
  ["placeholder only",
   { id: "search-2", type: "search", placeholder: "title, author, or ISBN" }],
  ["label + help text",
   { id: "member-no", required: true, "aria-describedby": "member-help" }],
  ["error replaces help",
   { id: "member-no", required: true, "aria-invalid": "true", "aria-describedby": "member-error" }],
  ["error added to help",
   { id: "member-no", required: true, "aria-invalid": "true",
     "aria-describedby": "member-help member-error" }],
  ["error link broken",
   { id: "member-no", required: true, "aria-invalid": "true",
     "aria-describedby": "member-help member-error-2" }],
  ["date, label + help",
   { id: "return", type: "date", "aria-describedby": "return-help" }],
  ["title only",
   { id: "note", title: "A note for staff" }],
  ["aria-label overrides label",
   { id: "member-no", "aria-label": "Number" }],
];

const G = 31;   // field column width

console.log("field".padEnd(G) + "accessible name".padEnd(28) + "name source".padEnd(27) + "state");
for (const [heading, a] of FIELDS) {
  const [n, k] = name(a);
  console.log(heading.padEnd(G) + (n === "" ? "(empty)" : '"' + n + '"').padEnd(28) +
    k.padEnd(27) + states(a));
}

console.log("\n" + "field".padEnd(G) + "description".padEnd(71) + "source");
for (const [heading, a] of FIELDS) {
  const [, k] = name(a);
  const [c, ck] = description(a, k);
  console.log(heading.padEnd(G) + (c === "" ? "(empty)" : '"' + c + '"').padEnd(71) + ck);
}

// --- Rule check ---------------------------------------------------------
const VISIBLE = { search: "Search the catalog", "member-no": "Membership number", return: "Return date" };

console.log("\nrule check:");
let findings = 0;
for (const [heading, a] of FIELDS) {
  const [n, k] = name(a);
  const [c] = description(a, k);
  const b = [];
  if (n === "") b.push("field is nameless");
  if (k.includes("last resort")) b.push("name comes from a last-resort source, no persistent label");
  const visible = VISIBLE[a.id];
  if (visible && n && !n.toLocaleLowerCase("en-US").includes(visible.toLocaleLowerCase("en-US")))
    b.push(`visible label ("${visible}") is not contained in the accessible name`);
  if (a["aria-describedby"]) {
    const broken = a["aria-describedby"].split(" ").filter((x) => !DOCUMENT[x]);
    if (broken.length) b.push("broken reference: " + broken.join(", "));
  }
  if (a["aria-invalid"] === "true" && !c.includes(DOCUMENT["member-error"]))
    b.push("invalid field, error message is not in the description");
  if (a["aria-invalid"] === "true" && a["aria-describedby"] &&
      !a["aria-describedby"].split(" ").includes("member-help"))
    b.push("error notification overrode the help text");
  for (const x of b) { findings++; console.log("  " + heading.padEnd(G) + x); }
}
console.log(`\n${FIELDS.length} fields, ${findings} findings`);
field                          accessible name             name source                state
label + placeholder            "Search the catalog"        label element              -
placeholder only               "title, author, or ISBN"    placeholder (last resort)  -
label + help text              "Membership number"         label element              required
error replaces help            "Membership number"         label element              required, invalid
error added to help            "Membership number"         label element              required, invalid
error link broken              "Membership number"         label element              required, invalid
date, label + help             "Return date"               label element              -
title only                     "A note for staff"          title (last resort)        -
aria-label overrides label     "Number"                    aria-label                 -

field                          description                                                            source
label + placeholder            "title, author, or ISBN"                                               placeholder
placeholder only               (empty)                                                                no description
label + help text              "The 8-digit number on the card."                                      aria-describedby
error replaces help            "Membership number must be 8 digits."                                  aria-describedby
error added to help            "The 8-digit number on the card. Membership number must be 8 digits."  aria-describedby
error link broken              "The 8-digit number on the card."                                      aria-describedby
date, label + help             "At most 21 days from now can be selected."                            aria-describedby
title only                     (empty)                                                                no description
aria-label overrides label     (empty)                                                                no description

rule check:
  placeholder only               name comes from a last-resort source, no persistent label
  error replaces help            error notification overrode the help text
  error link broken              broken reference: member-error-2
  error link broken              invalid field, error message is not in the description
  title only                     name comes from a last-resort source, no persistent label
  aria-label overrides label     visible label ("Membership number") is not contained in the accessible name

9 fields, 6 findings

Reading the Findings

The placeholder turns into the name, but turning into the name is not enough. In the second row, the search field’s name becomes “title, author, or ISBN.” The computation accepts this, and the automated checker does not see a presence-rule violation either. The defect is elsewhere: once the user starts typing into the field, the name is wiped from the screen, and what the field wants stays invisible. The first row shows the correct arrangement — the label is the name, the placeholder carries the description, and each does its own job.

The error message can override the help text. In the fourth row, the aria-describedby value has been replaced with the help text’s id. The field’s description is now only the error message: the user hears the error but loses the “8-digit” information. The fifth row shows the correct operation — it is added to the id list, both ids stand together in the list, and the description becomes the combination of both texts. This is the direct consequence of aria-describedby being a space-separated ID reference list.

A broken reference drops silently. In the sixth row, the error node’s id has been written incorrectly. The computation skips that id, and the description comes only from the help text: the field is reported as invalid, but the reason is announced nowhere. This is the check’s most valuable finding, because the error message is visibly present on screen, and someone looking at it cannot see the defect.

A name declaration overrides the visible label. In the last row, the field’s label element says “Membership number,” and its aria-label declaration says “Number.” The 2.5.3 Label in Name criterion requires the visible label text to be contained within the accessible name; here it is not.

Keyboard Contract

Key Behavior
Tab Brings focus to the field; the field is a single tab stop.
Typing keys Changes the value.
Home / End Moves the cursor to the start and end of the line.
Arrow keys Moves the cursor within the text.
Enter Performs implicit submission in a form with a single text field.

The contract’s real constraint is which keys the field must not capture. Arrow keys, Home, End, and Backspace belong to text editing; a component that contains the field cannot use these keys for its own navigation. This constraint is binding in dropdown list and menu patterns: if a suggestion list tied to the search field is going to use the arrow keys, it has to leave the key to the field whenever the cursor needs to move within the text.

Also, seizing the field’s focus programmatically — focusing the search field the moment the page loads — cuts the keyboard user off from the navigation at the top of the page. Focus is moved only after an action the user has initiated.

Measurable Constraints

3.3.2 Labels or Instructions requires that a label or instruction be provided for every field that requests input from the user. This is the criterion behind the placeholder not substituting for the label.

1.4.3 Contrast (Minimum) also covers placeholder text: the placeholder has to carry at least a 4.5:1 contrast ratio with the field surface. Fading the placeholder is a common design preference, and it removes the criterion directly.

1.4.11 Non-text Contrast covers the field’s border: the boundary showing where the field starts and ends has to separate from the page surface by at least 3:1. In fields built with only an underline, this measure is computed over the single edge.

1.4.4 Resize Text requires that text be scalable up to 200 percent; a fixed-height field box clips the text at that scale.

3.3.1 Error Identification requires the error to be conveyed in text. A red border alone is not a notification; the aria-invalid state and the tied error message are the notification itself.

Common Mistake

The label being visually removed and moved into the placeholder. Catching it is the check’s second row: every field whose name source is flagged as “last resort” is in this state. If the label genuinely should not be visible — like the standalone search field above the results table — the element itself stays in the document and is only hidden from the screen; it is not removed from the tree.

The error message being shown only on screen. A text node added below the field is not tied to the field in the tree unless the aria-describedby binding is established. Catching it means checking whether every field flagged as invalid has the error text within its description; the check’s last two rules do this.

Requiredness being conveyed only with an asterisk. The asterisk next to the label is a visual mark; in the tree, requiredness comes from the required attribute. What the asterisk means has to be written separately, and the attribute has to be present in every case.

Summary

  • A text field is used when the value cannot be enumerated in advance; a selection control or a dropdown list is chosen for enumerable values.
  • The type attribute determines the role, built-in validation, and touch keyboard together; autocomplete declares the field’s input purpose programmatically.
  • The label goes to the name; the help text and error message go to the description. aria-describedby is an ID reference list, and the error message is added alongside the help text, not in its place.
  • The placeholder is last in the name computation order; turning into the name does not make it a label, because it is wiped from the screen once the field is filled in.
  • A broken ID reference drops silently, and the field appears invalid without a reason; the check catches this despite the error message being visible on screen.
  • Arrow keys, Home, and End belong to text editing; a component that contains the field cannot use these keys for its own navigation.

Next Step

A text field carries a single value, and that value’s name is the field’s name. The “Notification method” options in the borrowing form, by contrast, consist of three separate controls, and all three share one question: “How should the notification be sent?” That question is not any control’s name — it is the group’s name. The next lesson takes up the checkbox, the radio button, and the switch: it specifies how the group is named, which state declaration each of the three controls carries, and why only one of them has arrow keys in its keyboard contract.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close