1. Recover from an error
Submit an empty form. Follow a specific error back to its field, correct it and complete the demo.
Digital accessibility · Hands-on practice
Feel what a useful error message, a well-managed dialog and a keyboard-friendly tab set do. Try each example, then inspect the pattern behind it.
Submit an empty form. Follow a specific error back to its field, correct it and complete the demo.
Open a dialog with the keyboard. Explore its content, close it and check where focus returns.
Move between tab labels without changing the panel. Activate one when you are ready.
These are learning examples, not a complete WCAG assessment. Combine keyboard checks with screen reader testing, zoom, contrast and real task completion.
Example 01 · Forms
Practice only: this does not book a place or send email. Use fictional details. This demo sends and saves none of your entries.
Persistent visible labels identify each field. Required fields are marked in words. Errors explain how to recover and remain linked to the affected input. Correcting an error removes its invalid state and keeps the original hint.
Initial validation happens on submit. Corrections update quietly; the demo does not announce every keystroke. A polite status confirms a successful action.
For real personal-information forms, appropriate autocomplete tokens identify input purpose. Keep server validation too: this local demo is not a backend or an email-delivery check.
Pattern references: WAI form labels, WAI error and success notifications, WCAG 2.2 · 1.3.5 input purpose (AA).
A focused excerpt. Use the full example and test its behavior in context.
<label for="email">Email (required)</label>
<input id="email" type="email" autocomplete="email"
required aria-describedby="email-hint">
<p id="email-hint">For example, [email protected].</p>
<p id="email-error" hidden></p>
// After an unsuccessful submit:
email.setAttribute("aria-invalid", "true");
email.setAttribute("aria-describedby", "email-hint email-error");
error.textContent = "Enter an email such as [email protected].";
error.hidden = false;
// Add a linked error to the summary, then focus the summary.
summary.hidden = false;
summary.focus();
// When the error is corrected, keep the permanent hint:
email.removeAttribute("aria-invalid");
email.setAttribute("aria-describedby", "email-hint");
error.hidden = true;
Example 02 · Focus management
Open a short checklist. Notice the starting point and the visible focus indicator as you move through it.
Press Enter on the opener. Use Tab and Shift + Tab to cycle through the modal controls. Try Escape, reopen it, and try the close button.
This uses native <dialog> with showModal(). The heading is an intentional initial focus target. The background becomes inert while the modal is open, and dismissal restores focus to its opener.
Choose initial focus for the task: a reading position may suit explanatory content; a safe action may suit a confirmation. Only contain focus while a modal is open.
Pattern reference: W3C APG modal dialog. APG describes interaction patterns; it is not a WCAG conformance certificate.
A focused excerpt. Use the full example and test its behavior in context.
<button id="open-notes" type="button">Open notes</button>
<dialog id="notes" aria-labelledby="notes-title">
<h2 id="notes-title" tabindex="-1">Practice notes</h2>
<p>Read the notes, then close this dialog.</p>
<button id="close-notes" type="button">Close dialog</button>
</dialog>
const opener = document.getElementById("open-notes");
const dialog = document.getElementById("notes");
opener.addEventListener("click", () => {
dialog.showModal();
document.getElementById("notes-title").focus();
});
document.getElementById("close-notes").addEventListener("click",
() => dialog.close());
dialog.addEventListener("close", () => opener.focus());
// Native Escape dismisses the modal. The full demo additionally
// wraps Tab from its last control to its first, and Shift+Tab
// from its first control (or initial heading) to its last.
// Scope any wrapping handler to the open dialog only.
Example 03 · Composite widgets
Reach every control without a mouse. Look for a visible focus outline and a logical route. Complete the action, then find your way back.
Listen for each control's name, role and state. Check whether errors, selected tabs and the dialog title give you enough information to continue.
Increase browser zoom and text size. Check that controls and messages remain reachable, and focused items are not hidden by fixed content.
Only one tab is in the page's Tab sequence. The active tab exposes aria-selected="true"; each panel is labelled by its tab. All panels remain ordinary readable sections when JavaScript is unavailable.
Pattern reference: W3C APG tabs. This example uses manual activation, separating focus from selection.
A focused excerpt. Use the full example and test its behavior in context.
// Run after adding tablist, tab and tabpanel roles.
// Each tab's aria-controls points to its labelled panel.
function activate(next) {
tabs.forEach((tab, index) => {
const selected = index === next;
tab.setAttribute("aria-selected", String(selected));
tab.tabIndex = selected ? 0 : -1;
panels[index].hidden = !selected;
});
}
// On Left/Right/Home/End: move focus, not the displayed panel.
function moveFocus(next) {
tabs.forEach((tab, index) => {
tab.tabIndex = index === next ? 0 : -1;
});
tabs[next].focus();
}
// Activate on click. Native buttons also produce a click for
// Enter and Space. Do not cancel their default key behavior.
tabs.forEach((tab, index) => {
tab.addEventListener("click", () => activate(index));
});
Inspector off.
The readout shows an element name and ID, never your form entries. It is deliberately not a live region, so it does not interrupt screen reader announcements on every focus change.
Also test at zoom and with sticky headers: WCAG 2.2 · 2.4.11 Focus Not Obscured (Minimum), AA requires a focused component not to be entirely hidden by author-created content.
Record the task, browser, assistive technology, expected behavior and what actually happened. A working example helps you learn the pattern; testing a complete journey tells you whether your product works for people.