From 9a1e9e35a16560513ebdc30ff924f7dcaa7eb24f Mon Sep 17 00:00:00 2001 From: Ajay Dhangar Date: Sun, 25 Oct 2026 20:50:55 +0530 Subject: [PATCH] Done Accessibility --- .../accessibility-testing.mdx | 589 +++++++++++++++++ tutorials/html/13-accessibility/aria-live.mdx | 605 ++++++++++++++++++ 2 files changed, 1194 insertions(+) diff --git a/tutorials/html/13-accessibility/accessibility-testing.mdx b/tutorials/html/13-accessibility/accessibility-testing.mdx index e69de29bb..460e5f5e6 100644 --- a/tutorials/html/13-accessibility/accessibility-testing.mdx +++ b/tutorials/html/13-accessibility/accessibility-testing.mdx @@ -0,0 +1,589 @@ +--- +id: html-accessibility-testing +title: "Accessibility Testing" +sidebar_label: "Accessibility Testing" +sidebar_position: 22 +slug: /html/accessibility/accessibility-testing +description: "Learn how to test a web page for accessibility: automated tools, a five-minute keyboard and zoom check, screen reader checks, and testing with real people. Understand what automation can and cannot catch, how to report and fix problems, and how to keep them from coming back. Includes a manual test plan lab, a what-can-a-tool-catch quiz, and a live mini page auditor." +keywords: + - accessibility testing + - a11y testing + - axe DevTools + - Lighthouse accessibility + - WAVE + - keyboard testing + - screen reader testing + - manual accessibility testing + - accessibility audit + - WCAG testing + - learn HTML + - CodeHarborHub HTML +tags: [html, web-development, beginner, accessibility, testing, axe, lighthouse, screen-readers, audit, a11y, frontend] +--- + +import {useState} from 'react'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import BrowserOnly from '@docusaurus/BrowserOnly'; +import AdsComponent from '@site/src/components/AdsComponent'; +import BrowserWindow from '@site/src/components/BrowserWindow'; + +export const planSteps = [ + {id: "kb", group: "Keyboard", text: "Put the mouse away. Can you reach and use every link, button, and field with Tab, Shift+Tab, Enter, Space, and arrow keys?", fix: "Use real buttons and links, and fix any custom widgets."}, + {id: "focus", group: "Keyboard", text: "Can you always see where focus is?", fix: "Never remove the outline without a clear replacement."}, + {id: "order", group: "Keyboard", text: "Does the focus order match the reading order, and is there no keyboard trap?", fix: "Fix the DOM order rather than using positive tabindex."}, + {id: "zoom", group: "Zoom and reflow", text: "Zoom the browser to 200 percent, then to 400 percent (or make the window 320 pixels wide). Is everything still readable, with no sideways scrolling for ordinary text?", fix: "Use relative units and flexible layouts."}, + {id: "text", group: "Zoom and reflow", text: "Increase the text size only. Is anything cut off or overlapping?", fix: "Avoid fixed heights on text containers."}, + {id: "contrast", group: "Colour", text: "Check text and key controls with a contrast checker. Is any text too faint?", fix: "Darken text or lighten the background until it passes."}, + {id: "colour", group: "Colour", text: "Look at the page in greyscale. Is anything understood only by colour?", fix: "Add text, an icon, or a pattern."}, + {id: "sr", group: "Screen reader", text: "Turn on a screen reader and listen to the main task. Does every control have a clear name, role, and state?", fix: "Add labels, correct roles, and states."}, + {id: "headings", group: "Screen reader", text: "List the headings and landmarks. Does the outline make sense by itself?", fix: "Fix heading levels and add landmarks."}, + {id: "live", group: "Screen reader", text: "Trigger an error and a success message. Are they announced without moving focus around?", fix: "Use a live region or move focus to the error summary."}, + {id: "motion", group: "Motion and media", text: "Turn on 'reduce motion' in your system. Do animations calm down? Do videos have captions, and does nothing flash?", fix: "Respect the reduced-motion setting and add captions."}, + {id: "forms", group: "Forms", text: "Submit a form with mistakes. Are the errors clear, near the field, and linked to it?", fix: "Add text errors, aria-invalid, and aria-describedby."}, +]; + +export function TestPlanLab() { + const [results, setResults] = useState({}); + const set = (id, value) => setResults({...results, [id]: value}); + const done = planSteps.filter((step) => results[step.id] !== undefined).length; + const passes = planSteps.filter((step) => results[step.id] === "pass").length; + const fails = planSteps.filter((step) => results[step.id] === "fail"); + const groups = planSteps.reduce((list, step) => (list.indexOf(step.group) === -1 ? list.concat(step.group) : list), []); + return ( +
+

Open any page you built, and go through these checks. Mark each one as you test it.

+ {groups.map((group) => ( +
+ {group} + {planSteps.filter((step) => step.group === group).map((step) => ( +
+

{step.text}

+ + +
+ ))} +
+ ))} +

{done} of {planSteps.length} checks done. {passes} passed, {fails.length} failed.

+ {fails.length > 0 ? ( +
+

Your to-fix list:

+
    + {fails.map((step) => (
  • {step.group}: {step.fix}
  • ))} +
+
+ ) : null} +

+ This is a starter plan, not a full audit, and passing it does not prove a page meets WCAG. It is a fast way to find the biggest problems. Nothing here is saved. +

+
+ ); +} + +export const catchItems = [ + {id: "alt-missing", text: "An image has no alt attribute at all", answer: "tool", why: "A tool can see that the attribute is missing."}, + {id: "alt-bad", text: "An image has alt text \"image123.png\"", answer: "partly", why: "Some tools flag suspicious file names, but only a person can tell whether the text is useful."}, + {id: "label-missing", text: "A form field has no label", answer: "tool", why: "The missing name can be found in the code."}, + {id: "label-bad", text: "A label says \"Field\" for the email box", answer: "human", why: "A tool sees a label. Only a person sees that it is unhelpful."}, + {id: "contrast", text: "Grey text on white has low contrast", answer: "tool", why: "Contrast is a calculation. Tools are good at it, except over images and gradients."}, + {id: "keyboard-trap", text: "Focus gets stuck inside a pop-up and cannot leave", answer: "human", why: "This needs real keyboard use."}, + {id: "focus-order", text: "Tab order jumps around the page in a confusing way", answer: "human", why: "A tool can warn about positive tabindex, but judging the order needs a person."}, + {id: "lang", text: "The page has no lang attribute", answer: "tool", why: "A simple presence check."}, + {id: "heading-skip", text: "A heading jumps from h2 to h4", answer: "tool", why: "The order can be read from the code. Whether the headings are meaningful is for a person."}, + {id: "instructions", text: "Instructions say \"click the red button\"", answer: "human", why: "Sensory-only instructions need human reading."}, + {id: "captions", text: "A video has captions that are wrong or out of sync", answer: "human", why: "A tool can see a track exists, not whether it is accurate."}, + {id: "button-empty", text: "A button contains only an icon, with no name", answer: "tool", why: "The empty accessible name is found by a tool."}, +]; + +export function CatchQuiz() { + const [guesses, setGuesses] = useState({}); + const [shown, setShown] = useState(false); + const labels = {tool: "A tool can catch it", partly: "A tool can partly help", human: "A person must check"}; + const right = catchItems.filter((item) => guesses[item.id] === item.answer).length; + const answered = Object.keys(guesses).length; + return ( +
+

For each problem, decide whether an automated tool can find it.

+ {catchItems.map((item) => ( +
+ {item.text} + {Object.keys(labels).map((key) => ( + + ))} + {shown && guesses[item.id] ? ( +

{guesses[item.id] === item.answer ? "Correct." : "Not quite: " + labels[item.answer] + "."} {item.why}

+ ) : null} +
+ ))} +

+ {" "} + +

+

{shown ? "You got " + right + " of " + answered + " answered correctly." : ""}

+

+ Different tools check different things, so treat the answers as general guidance. The big lesson: tools find the mechanical problems, and people find the human ones. +

+
+ ); +} + +export const auditSamples = { + messy: '\n\nclick here\n\n\n

Rye Bread

\n

Ingredients

\n

Steps

\n
Skip
\n
One
\n
Two
\n', + fixed: '
\nA loaf of rye bread on a board\nRead more about rye bread\nDownload the recipe\n\n\n\n

Rye Bread

\n

Ingredients

\n

Steps

\n
One
\n
Two
\n\n
', +}; + +export function auditText(el) { + return (el.textContent || "").replace(/\s+/g, " ").trim(); +} + +export function auditHasName(el, doc) { + if ((el.getAttribute("aria-label") || "").trim()) return true; + const by = (el.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean); + if (by.some((id) => doc.getElementById(id) && auditText(doc.getElementById(id)))) return true; + if (auditText(el)) return true; + if (el.querySelector("img[alt]:not([alt=''])")) return true; + if ((el.getAttribute("title") || "").trim()) return true; + return false; +} + +export function PageAuditorInner() { + const [which, setWhich] = useState("messy"); + const [html, setHtml] = useState(auditSamples.messy); + const doc = new DOMParser().parseFromString("" + html, "text/html"); + const found = []; + const add = (rule, message, tag) => found.push({rule, message, tag}); + Array.from(doc.querySelectorAll("img")).forEach((el) => { + if (!el.hasAttribute("alt")) add("Images", "An image has no alt attribute. Add alt text, or alt=\"\" if it is decoration.", "img"); + }); + Array.from(doc.querySelectorAll("a[href]")).forEach((el) => { + if (!auditHasName(el, doc)) add("Links", "A link has no name. Add text, or an aria-label.", "a"); + else if (/^(click here|here|read more|more|link)$/i.test(auditText(el))) add("Links", "The link text \"" + auditText(el) + "\" does not make sense out of context. Say where it goes.", "a"); + }); + Array.from(doc.querySelectorAll("button, [role='button']")).forEach((el) => { + if (el.closest("[aria-hidden='true'], [hidden]")) return; + if (!auditHasName(el, doc)) add("Buttons", "A button has no name. Add text or an aria-label.", "button"); + }); + Array.from(doc.querySelectorAll("input:not([type='hidden']):not([type='submit']):not([type='button']), select, textarea")).forEach((el) => { + const id = el.getAttribute("id"); + const hasLabel = (id && doc.querySelector('label[for="' + id + '"]')) || el.closest("label") || el.hasAttribute("aria-label") || el.hasAttribute("aria-labelledby"); + if (!hasLabel) add("Forms", "A form field has no label" + (el.hasAttribute("placeholder") ? " (a placeholder is not a label)" : "") + ". Add a label connected to it.", el.tagName.toLowerCase()); + }); + const h1s = doc.querySelectorAll("h1"); + if (h1s.length > 1) add("Headings", "There are " + h1s.length + " h1 headings. Most pages should have one.", "h1"); + let last = 0; + Array.from(doc.querySelectorAll("h1, h2, h3, h4, h5, h6")).forEach((el) => { + const level = parseInt(el.tagName.charAt(1), 10); + if (last && level > last + 1) add("Headings", "The heading level jumps from h" + last + " to h" + level + ". Do not skip levels.", el.tagName.toLowerCase()); + last = level; + }); + Array.from(doc.querySelectorAll("[tabindex]")).forEach((el) => { + if (parseInt(el.getAttribute("tabindex"), 10) > 0) add("Focus", "A positive tabindex (" + el.getAttribute("tabindex") + ") creates a confusing focus order. Use 0 or -1.", el.tagName.toLowerCase()); + }); + const ids = {}; + Array.from(doc.querySelectorAll("[id]")).forEach((el) => { ids[el.id] = (ids[el.id] || 0) + 1; }); + Object.keys(ids).forEach((id) => { if (ids[id] > 1) add("IDs", "The id \"" + id + "\" is used " + ids[id] + " times. Ids must be unique.", "id"); }); + Array.from(doc.querySelectorAll("[aria-hidden='true']")).forEach((el) => { + if (el.matches("a[href], button, input, select, textarea, [tabindex]") || el.querySelector("a[href], button, input, select, textarea, [tabindex]")) add("ARIA", "aria-hidden=\"true\" on something focusable. Use hidden or inert instead.", el.tagName.toLowerCase()); + }); + if (doc.querySelectorAll("*").length > 6 && !doc.querySelector("main, [role='main']")) add("Landmarks", "No main landmark. Wrap the main content in a main element.", "main"); + const choose = (value) => { setWhich(value); setHtml(auditSamples[value]); }; + return ( +
+
+ Which HTML do you want to audit? + + +
+

+
+