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) => (
+
+ ))}
+
{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) => (
+
+ ))}
+
+ {" "}
+
+
+
{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.
+
\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 (
+
No problems found by these simple checks. That does not mean the page is accessible. Now test it by hand.
: (
+
+ {found.map((item, index) => (
+
{item.rule}: {item.message}
+ ))}
+
+ )}
+
+ This is a small teaching tool with a handful of checks, not a replacement for axe, Lighthouse, or WAVE. It reads HTML only, so it cannot judge colours, layout, scripts, or how a page feels to use.
+
+
+ );
+}
+
+export function PageAuditor() {
+ return {() => };
+}
+
+You have built the pages. You have added labels, roles, and live regions. But how do you **know** it works?
+
+You **test.** Accessibility testing is not one big exam at the end. It is a habit: a quick check as you build, a sturdier check before you ship, and a conversation with real people whenever you can. This lesson gives you a toolkit for all three, and a plain look at what each one can and cannot tell you.
+
+
+
+
+## What You Will Learn
+
+- The **three layers** of accessibility testing
+- What **automated tools** catch, and what they miss
+- A fast **keyboard, zoom, and colour** test you can do in five minutes
+- How to test with a **screen reader**
+- Why testing with **real people** matters
+- How to **report, prioritise, and fix** problems
+- How to stop them **coming back**
+
+## The Big Idea
+
+
+
+
+```html title="index.html"
+
+
+
+
+
+
+```
+
+
+
+
+
+<>
+
+>
+
+
+
+
+
+Run the messy page, fix every problem in the box, and then run it again. Then read on to see why a clean result is only the beginning.
+
+## The Three Layers of Testing
+
+| Layer | What it is | Good at | Weak at |
+| :-- | :-- | :-- | :-- |
+| **Automated** | Tools that scan the code or page | Fast, repeatable, catches missing labels, alt, contrast, and bad markup | Cannot judge meaning, order, or ease of use |
+| **Manual** | You test with a keyboard, zoom, and a screen reader | Finds focus, structure, and flow problems | Slower, and needs practice |
+| **With people** | Disabled and older users try real tasks | Finds the problems you did not think of | Takes planning, time, and care |
+
+You need **all three.** Automated testing is the cheapest and fastest, so it goes first. But it is only the start.
+
+## Layer 1: Automated Tools
+
+Automated tools scan your page and report problems they can detect from the code. Some of the most widely used:
+
+- **axe DevTools** (browser extension, and the `axe-core` library for your own tests)
+- **Lighthouse** (built into Chrome DevTools; gives an accessibility score)
+- **WAVE** (a browser extension that overlays icons on the page)
+- **Accessibility Insights** (a free tool with a guided manual check)
+- **Browser developer tools** (the Accessibility panel shows the tree, names, and roles)
+- **Linters** such as `eslint-plugin-jsx-a11y`, which flag problems as you type
+- **pa11y** and `jest-axe`, for running checks in automated builds
+
+### What they are good at
+
+Missing `alt`, missing form labels, empty buttons and links, low contrast, a missing `lang`, duplicate ids, bad ARIA attribute names and values, and skipped heading levels.
+
+### What they cannot do
+
+A tool can tell whether a label **exists.** It cannot tell whether it is **helpful.** It does not know whether your focus order makes sense, whether a modal traps focus, or whether your instructions depend on colour. Widely quoted estimates say automated tools find **only a portion of real problems,** often less than half. The exact number depends on the tool and the page.
+
+:::warning A perfect score is not "accessible"
+A Lighthouse score of 100 means the checks it ran did not fail. It does not mean the page is accessible. Treat the score as a smoke alarm, not a safety certificate.
+:::
+
+Try it yourself: for each problem below, decide whether a tool can catch it.
+
+
+<>
+
+>
+
+
+### A first automated test
+
+If you use JavaScript, `axe-core` runs in a few lines:
+
+```js title="a11y-check.js"
+// npm install axe-core
+import axe from "axe-core";
+
+const results = await axe.run(document);
+
+results.violations.forEach((violation) => {
+ console.log(violation.id, violation.help);
+ violation.nodes.forEach((node) => console.log(" ", node.target));
+});
+```
+
+Or open **Chrome DevTools, then Lighthouse, tick "Accessibility," and run it** on any page, with no setup.
+
+## Layer 2: Manual Testing
+
+You can find the biggest problems in **about five minutes,** with no special software.
+
+### The keyboard test
+
+1. **Put the mouse away.**
+2. Press **Tab** through the whole page. Can you reach every link, button, and field?
+3. Can you **always see** where you are?
+4. Does the order make sense?
+5. Can you **operate** each control: Enter for links and buttons, Space for buttons and checkboxes, arrows for menus, tabs, and radios?
+6. Do menus and dialogs let you leave with **Esc,** and does focus return to where you were?
+7. Is there a **skip link** at the start, and does it work?
+
+Most serious failures show up here. See [Keyboard Accessibility](./keyboard-accessibility) and [Focus Management](./focus-management).
+
+### The zoom and reflow test
+
+1. Zoom your browser to **200 percent,** then **400 percent,** or make the window **320 CSS pixels** wide.
+2. Is everything still readable and usable?
+3. Do you need to scroll **sideways** to read ordinary text? (Wide tables and maps may be exceptions.)
+4. Increase **text size only.** Is anything cut off or overlapping?
+
+### The colour test
+
+1. Use a **contrast checker** (such as the one in the browser tools) on body text, buttons, and form borders.
+2. View the page in **greyscale.** Can you still tell the states apart without colour?
+3. Try **dark mode** and **high contrast** or **forced colours** mode, if your system has it.
+
+### The motion and media test
+
+1. Turn on your system's **reduce motion** setting. Do animations calm down?
+2. Do videos have **captions,** and do audio clips have a **transcript**?
+3. Does anything **flash** more than three times per second?
+
+### The screen reader test
+
+You do not need to be an expert. Even a few minutes of listening will teach you a lot. Common choices (check current versions and combinations):
+
+| System | Screen reader | Often paired with |
+| :-- | :-- | :-- |
+| Windows | **NVDA** (free) | Firefox or Chrome |
+| Windows | **JAWS** (paid) | Chrome or Edge |
+| Windows | **Narrator** (built in) | Edge |
+| Mac and iPhone | **VoiceOver** (built in) | Safari |
+| Android | **TalkBack** (built in) | Chrome |
+
+Then try this:
+
+1. Listen to the page from the top. Does it make sense?
+2. Open the **headings list** and the **landmarks list.** Is the outline sensible?
+3. Complete your page's **main task** (sign up, search, check out) using only the screen reader and keyboard.
+4. Cause an **error** and a **success message.** Are they announced?
+5. Walk through your **icons and images.** Any noise, or missing names?
+
+:::tip You will sound clumsy at first, and that is fine
+Learn four or five commands: start and stop, next item, next heading, next link, and list of headings. Expert users move much faster than you will, so do not judge by your own pace. Use your testing to find missing names and broken structure, not to measure how fast someone can go.
+:::
+
+### Make a plan
+
+Here is a starter test plan. Use it on any page you build:
+
+
+<>
+
+>
+
+
+## Layer 3: Test with Real People
+
+No checklist replaces watching a person use your product. Disabled and older users find things that tools and experts miss, because **they live with the barriers every day.**
+
+- **Recruit a range:** people who use screen readers, magnifiers, voice control, switch devices, captions, and people with cognitive differences.
+- **Pay them** for their time and expertise.
+- **Give them real tasks,** not a tour: "Find a recipe and save it."
+- **Watch, do not rescue.** Note where they struggle, but do not explain the page. If you have to explain it, that is the finding.
+- **Ask for consent** before recording anything, and explain what you will keep.
+- **Keep the sessions short and kind.**
+
+If you cannot run formal sessions, offer a **feedback route** on your site (an email address or form) and a public **accessibility statement** that says what is supported and how to report a problem. Then answer the messages.
+
+## Reporting and Fixing Problems
+
+A good bug report is easy to act on. Write down:
+
+1. **Where:** the page and the part of the page.
+2. **What happened,** and what you expected.
+3. **How to reproduce it,** step by step, with the browser and assistive technology used (for example, "NVDA with Firefox").
+4. **Why it matters:** who is blocked, and from what task.
+5. **The rule it breaks,** if you know it (for example, a WCAG success criterion).
+6. **A suggested fix,** if you have one.
+
+### Deciding what to fix first
+
+Fix problems in this order, as a general guide:
+
+1. **Blockers:** people cannot finish a key task at all (a keyboard trap, an unlabelled "Pay" button).
+2. **Major:** people can finish, but with great difficulty or help.
+3. **Minor:** annoying, but there is a workaround.
+4. **Polish:** small improvements.
+
+Also consider how **many people** hit the problem, and how **central** the page is.
+
+## Stop Problems Coming Back
+
+A fixed bug can come back with the next release. To keep it fixed:
+
+- **Run a linter** in your editor, so common mistakes are caught as you type.
+- **Add automated checks** to your build or pull requests (`axe-core`, `pa11y`, or `jest-axe`).
+- **Test components once,** and reuse them. A well-tested button, dialog, and form field help every page.
+- **Add a keyboard and screen reader pass** to your "definition of done."
+- **Re-test after big design changes,** and after upgrading frameworks or libraries.
+- **Train the team.** Designers, writers, and developers all affect accessibility.
+- **Track problems** like any other bug, with an owner and a date.
+
+:::info Test early
+The cheapest time to fix an accessibility problem is **while you are designing it.** Check sketches for contrast, focus, and labels before any code is written.
+:::
+
+## Standards and Reports
+
+Teams often test against **WCAG** (usually 2.1 or 2.2, at level AA). You can write up the results in an **accessibility conformance report** (sometimes called a VPAT), and publish an **accessibility statement.** Requirements differ between countries, sectors, and contracts, so check what applies to you. This is not legal advice.
+
+For more on levels and success criteria, see [WCAG Basics](./wcag-basics).
+
+## Good Habits
+
+- **Test as you build,** not only at the end.
+- **Start with a keyboard,** and then add a screen reader.
+- **Use at least one automated tool,** and keep it in your build.
+- **Fix the cause,** not only the symptom: a bad component makes the same bug everywhere.
+- **Test on real devices,** especially phones.
+- **Test with several browsers and assistive technologies.**
+- **Write down** what you tested, with versions.
+- **Listen to real users,** and thank them.
+
+## Accessibility Checklist
+
+- [ ] Automated checks run with no serious errors (axe, Lighthouse, or WAVE)
+- [ ] The whole page works with a keyboard, with visible focus
+- [ ] The page works at 200 and 400 percent zoom (or 320 pixels wide)
+- [ ] Text and controls have enough contrast, and nothing depends on colour alone
+- [ ] The page works with reduce motion on, and videos have captions
+- [ ] The main task works with a screen reader
+- [ ] Errors and status messages are announced
+- [ ] Headings and landmarks give a sensible outline
+- [ ] Mobile checked with a screen reader and touch
+- [ ] An accessibility statement and a way to give feedback are published
+- [ ] Checks are part of the build and the "definition of done"
+
+## Security and Privacy Notes
+
+- **Do not paste private pages into online checkers.** A page behind a login can contain personal data. Use local tools or a test account.
+- **Use test data.** Never test with real customers' records, and mask personal details in screenshots and recordings.
+- **Check extension permissions.** Browser extensions can read the pages you visit. Install testing tools only from trusted sources, and turn them off for sensitive sites.
+- **Be careful with recordings.** A recording of a screen reader session may capture names, emails, or messages. Get consent, store it safely, and delete it when you no longer need it.
+- **Consent and fairness:** compensate participants, explain how their data is used, and let them stop at any time.
+- **Do not rely on "overlay" widgets** that promise to fix accessibility automatically. Many do not work, and some collect user data. Fix the code instead.
+
+## Global Audience Notes
+
+- **Test in each language you support.** Labels, error messages, and live announcements need real translations, and the `lang` attribute must match.
+- **Test right-to-left layouts,** including focus order, icons, and zoom.
+- **Test on common, modest devices and slow networks.** An accessible page that never loads is not accessible.
+- **Screen reader availability and popularity differ around the world.** Ask your own users which tools they use.
+- **Test with input methods** used locally: on-screen keyboards, voice input, and IME typing for languages such as Japanese, Chinese, and Korean.
+- **Check regional rules,** as accessibility laws and standards differ by country.
+
+## Common Mistakes to Avoid
+
+1. **Trusting a score.** A Lighthouse 100 is not proof of accessibility.
+2. **Only testing with a mouse.**
+3. **Only testing one browser,** one device, or one screen reader.
+4. **Testing once, at the very end,** when fixes are most expensive.
+5. **Treating automated warnings as noise,** or "fixing" them just to silence the tool.
+6. **Never listening to a screen reader.**
+7. **Judging screen reader speed** by your own beginner pace.
+8. **Skipping real users,** or testing only with colleagues.
+9. **Using overlay "fix-it" widgets** instead of fixing the code.
+10. **Not writing bugs down,** so they are forgotten.
+11. **Fixing symptoms,** not the shared component that causes them.
+12. **No accessibility statement and no feedback route.**
+
+## Practice Time
+
+Try these in your browser and your editor:
+
+1. Run the mini page auditor on the messy page, fix every problem, and run it again.
+2. Take the "what can a tool catch" quiz. Which answer surprised you?
+3. Install axe DevTools or WAVE, and run it on a page you built.
+4. Run Lighthouse on the same page. Compare what it finds with axe.
+5. Do the five-minute keyboard test on your own site, and write down every problem.
+6. Zoom to 400 percent. What breaks?
+7. Turn on a screen reader and complete one task. What did you learn?
+8. Write one bug report using the six points above.
+9. Add `eslint-plugin-jsx-a11y` or `pa11y` to a project.
+10. Bonus: ask a friend who uses assistive technology (with their consent and a fair reward) to try your main task, and watch without helping.
+
+
+
+
+## Quick Recap
+
+Click a question to check your answer.
+
+
+1. What are the three layers of accessibility testing?
+
+Automated tools, manual testing (keyboard, zoom, screen reader), and testing with real people. You need all three.
+
+
+
+
+2. What can automated tools find, and what can't they?
+
+They find mechanical problems, such as missing alt text, missing labels, low contrast, and bad markup. They cannot judge whether a label is helpful, whether the focus order makes sense, or how usable the page really is.
+
+
+
+
+3. What is the fastest useful manual test?
+
+Put the mouse away and use the page with the keyboard alone, with visible focus, a logical order, and no traps.
+
+
+
+
+4. Which zoom levels should you check?
+
+Check 200 percent and 400 percent (or a 320 CSS pixel wide window), looking for cut-off text, overlapping, and sideways scrolling for ordinary text.
+
+
+
+
+5. Why test with real people?
+
+They find problems that tools and checklists miss, because they live with those barriers. It is the best way to learn what actually works.
+
+
+
+
+6. How do you stop accessibility bugs coming back?
+
+Add linters and automated checks to your build, reuse well-tested components, include keyboard and screen reader checks in your "definition of done," and track bugs like any others.
+
+
+
+## Key Takeaways
+
+- **Test early, often, and in three layers:** tools, hands-on checks, and real people.
+- **Automated tools are fast but limited.** A clean score is not proof.
+- **The keyboard test finds the biggest problems** in minutes.
+- **Zoom, contrast, motion, and screen reader checks** cover the rest of the basics.
+- **Write clear bug reports,** and fix the worst problems first.
+- **Put checks into your build,** so fixed bugs stay fixed.
+- **Protect privacy** when you test, and avoid "fix-it" overlays.
+- **Listen to the people who use your site.**
+
+You have now finished the accessibility section. Go back to the [section overview](/tutorials/html/accessibility), and pick any lesson you want to revisit. Then take what you learned and test one of your own pages today.
\ No newline at end of file
diff --git a/tutorials/html/13-accessibility/aria-live.mdx b/tutorials/html/13-accessibility/aria-live.mdx
index e69de29bb..2993ce3a4 100644
--- a/tutorials/html/13-accessibility/aria-live.mdx
+++ b/tutorials/html/13-accessibility/aria-live.mdx
@@ -0,0 +1,605 @@
+---
+id: html-aria-live
+title: "ARIA Live"
+sidebar_label: "ARIA Live"
+sidebar_position: 21
+slug: /html/accessibility/aria-live
+description: "Learn how aria-live and live region roles announce changes to screen reader users without moving focus: status messages, errors, search results, chat, and timers. Understand polite versus assertive, aria-atomic, aria-relevant, and why a live region must exist before it changes. Includes a live region lab with real regions, an atomic lab, and a live aria-live checker."
+keywords:
+ - aria-live
+ - live region
+ - role status
+ - role alert
+ - role log
+ - aria-atomic
+ - aria-relevant
+ - aria-busy
+ - status messages
+ - WCAG 4.1.3
+ - screen reader announcements
+ - learn HTML
+ - CodeHarborHub HTML
+tags: [html, web-development, beginner, accessibility, aria, aria-live, live-regions, status-messages, a11y, frontend]
+---
+
+import {useState, useRef, useEffect} 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 function LiveRegionLab() {
+ const [mode, setMode] = useState("polite");
+ const [ready, setReady] = useState(true);
+ const [count, setCount] = useState(0);
+ const [text, setText] = useState("");
+ const [log, setLog] = useState([]);
+ const send = (message) => {
+ const next = count + 1;
+ setCount(next);
+ setText(message);
+ setLog((old) => [{n: next, message, mode, ready}].concat(old).slice(0, 5));
+ };
+ const regionProps = mode === "off" ? {} : {"aria-live": mode, "aria-atomic": "true"};
+ const region = (
+
+ {text}
+
+ );
+ return (
+
+
+
+ {region}
+
+
+
+
+
+
What was sent:
+
+ {log.length === 0 ?
Nothing yet. Press a button.
: null}
+ {log.map((item) => (
+
"{item.message}" with {item.mode === "off" ? "no live attribute" : 'aria-live="' + item.mode + '"'}, region {item.ready ? "already present" : "created together with the text"}.
+ ))}
+
+
+ The box above is a real live region on this page. If you use a screen reader, switch it on and try each setting. In general, a polite region waits for the reader to finish speaking, an assertive one interrupts, and one with no attribute is silent, but exact behaviour differs between screen readers and browsers. When the box is created at the same moment as its text (untick the checkbox), many screen readers say nothing at all. This is the most common live region bug.
+
+ Basket: {items} items, total {items * 4} dollars
+
+
+
+
+
+
A screen reader may say: {last ? '"' + last + '"' : "Press a button."}
+
+ The status box is real, but the "may say" line is a simplified simulation. With aria-atomic="true" the whole region is read, so the message makes sense. With false, a screen reader may read only the parts that changed, here just the number (the total changes too, so you may hear two numbers with no words). Real screen readers vary, so test yours.
+
+
+ );
+}
+
+export function LiveTypesLab() {
+ const [pick, setPick] = useState("status");
+ const [entries, setEntries] = useState([]);
+ const [message, setMessage] = useState("");
+ const [seconds, setSeconds] = useState(10);
+ const [running, setRunning] = useState(false);
+ useEffect(() => {
+ if (!running) return undefined;
+ const id = setInterval(() => {
+ setSeconds((old) => {
+ if (old <= 1) { setRunning(false); return 0; }
+ return old - 1;
+ });
+ }, 1000);
+ return () => clearInterval(id);
+ }, [running]);
+ const info = {
+ status: {role: "status", live: "polite", use: "Saved, loading finished, 12 results found. Not urgent."},
+ alert: {role: "alert", live: "assertive", use: "A serious error, or time-critical warning. Interrupts the reader."},
+ log: {role: "log", live: "polite", use: "A history where new items are added at the end: chat, activity feed."},
+ timer: {role: "timer", live: "off", use: "A running clock. Not announced on every tick. Keep it from being noisy."},
+ };
+ const current = info[pick];
+ const act = () => {
+ if (pick === "status") setMessage("12 results found.");
+ if (pick === "alert") setMessage("Error: Your session ends in one minute.");
+ if (pick === "log") setEntries((old) => old.concat("Message " + (old.length + 1) + " from Sam").slice(-4));
+ if (pick === "timer") { setSeconds(10); setRunning(true); }
+ };
+ return (
+
+
+
+ {pick === "status" ?
{message}
: null}
+ {pick === "alert" ?
{message}
: null}
+ {pick === "log" ?
{entries.map((entry, index) =>
{entry}
)}
: null}
+ {pick === "timer" ?
{seconds} seconds left
: null}
+
+
+
+
Implied aria-live: {current.live}
+
Use it for: {current.use}
+
+
+ These are real live roles. Note that the status, alert and log containers here are swapped in when you choose a role, so the very first message after switching may not be announced. In your own pages, keep the region in the HTML from the start. The listed live values are the defaults the ARIA specification gives each role. A few screen readers handle alerts and timers a little differently.
+
\n\n',
+};
+
+export function LiveCheckerInner() {
+ const [which, setWhich] = useState("messy");
+ const [html, setHtml] = useState(liveSamples.messy);
+ const doc = new DOMParser().parseFromString("" + html, "text/html");
+ const rows = [];
+ const liveValues = ["off", "polite", "assertive"];
+ const liveRoles = ["status", "alert", "log", "timer", "marquee"];
+ const selector = "[aria-live], " + liveRoles.map((r) => '[role="' + r + '"]').join(", ");
+ const regions = Array.from(doc.querySelectorAll(selector));
+ const assertiveCount = regions.filter((el) => el.getAttribute("aria-live") === "assertive" || el.getAttribute("role") === "alert").length;
+ regions.forEach((el) => {
+ const tag = el.tagName.toLowerCase();
+ const role = el.getAttribute("role");
+ const live = el.getAttribute("aria-live");
+ const text = (el.textContent || "").replace(/\s+/g, " ").trim();
+ const issues = [];
+ const notes = [];
+ if (live !== null && liveValues.indexOf(live) === -1) issues.push("aria-live=\"" + live + "\" is not a valid value. Use polite, assertive, or off.");
+ if (role === "alert" && live === "polite") issues.push("role=\"alert\" is assertive by default, but aria-live=\"polite\" overrides it. Remove one.");
+ if (el.hasAttribute("hidden") || /display\s*:\s*none/i.test(el.getAttribute("style") || "")) issues.push("A hidden live region is not in the accessibility tree, so changes are not announced. Keep it present, and hide it visually (not with display: none) if needed.");
+ const atomic = el.getAttribute("aria-atomic");
+ if (atomic !== null && atomic !== "true" && atomic !== "false") issues.push("aria-atomic=\"" + atomic + "\" is not valid. Use true or false.");
+ if (el.querySelector("a[href], button, input, select, textarea")) notes.push("Contains links or controls. Live regions are for short messages. Keep interactive content outside the region.");
+ if (text.length > 160) notes.push("This region holds a lot of text (" + text.length + " characters). Announcements should be short.");
+ else if (text.length > 0 && (live !== null || role !== null)) notes.push("It already contains text in the HTML. Text that is present at page load is usually not announced. Only changes after load are.");
+ if (el.getAttribute("aria-relevant") && /all/.test(el.getAttribute("aria-relevant"))) notes.push("aria-relevant=\"all\" also announces removals, which is noisy. The default (additions and text) is usually right.");
+ if (role === "timer" && live === "assertive") notes.push("An assertive timer would interrupt every tick. Keep timers quiet, and announce only key moments.");
+ rows.push({tag: tag + (role ? "[role=" + role + "]" : "") + (live ? "[aria-live=" + live + "]" : ""), text, issues, notes});
+ });
+ if (assertiveCount > 1) rows.push({tag: "page", text: "", issues: [], notes: [assertiveCount + " assertive regions on the page. Too many interruptions are tiring. Reserve assertive for real emergencies."]});
+ Array.from(doc.querySelectorAll('[class*="toast"], [class*="snackbar"], [class*="notification"], [class*="flash"]')).forEach((el) => {
+ if (el.matches(selector) || el.closest(selector)) return;
+ rows.push({tag: el.tagName.toLowerCase() + "." + (el.getAttribute("class") || "").split(/\s+/)[0], text: (el.textContent || "").trim(), issues: ["Looks like a toast or message but has no live role. Screen reader users will not hear it. Add role=\"status\" (or alert), and keep the element in the page before it is filled."], notes: []});
+ });
+ const bad = rows.reduce((sum, row) => sum + row.issues.length, 0);
+ const choose = (value) => { setWhich(value); setHtml(liveSamples[value]); };
+ return (
+
+ This reads the HTML only. It cannot see changes made by scripts, so it cannot tell whether a region is present before it updates, or what is actually announced. Test with a real screen reader.
+
+
+ );
+}
+
+export function LiveChecker() {
+ return {() => };
+}
+
+You click "Save," and a little message flashes: "Saved!" A sighted person sees it at once. A screen reader user hears **nothing,** because focus never moved, and the screen reader only reads what you point it at.
+
+That gap is what **live regions** fix. A live region is a part of the page that a screen reader **watches.** When its content changes, the change is **announced,** without moving focus and without the user going to look for it. Search results, form errors, "added to basket," chat messages, and countdowns all rely on this.
+
+Live regions are powerful, and easy to get wrong. Most of the trouble comes from one detail you will learn early in this lesson: **the region must already be in the page before its text changes.**
+
+
+
+
+## What You Will Learn
+
+- What a **live region** is, and when you need one
+- The difference between **polite** and **assertive**
+- The built-in live roles: **`status`, `alert`, `log`,** and **`timer`**
+- How **`aria-atomic`, `aria-relevant`,** and **`aria-busy`** change what is read
+- Why a live region must **exist before it changes**
+- How to write **short, useful** announcements
+- How this meets **WCAG 4.1.3 Status Messages**
+- How to **check** your live regions
+
+## The Big Idea
+
+
+
+
+```html title="index.html"
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+
+
+<>
+
+>
+
+
+
+
+
+Try each setting. Then untick the box that says the region is already present, and press Save again. If you use a screen reader, you may notice the message disappear from the speech.
+
+:::tip The golden rule
+**Put the live region in the HTML first. Fill it with text later.** Do not create a new region and put the message into it at the same moment. Many screen readers will say nothing.
+:::
+
+## Polite or Assertive?
+
+`aria-live` takes three values:
+
+| Value | What it does | Use it for |
+| :-- | :-- | :-- |
+| `off` | The default. Changes are not announced | Most content |
+| `polite` | Waits until the reader has stopped speaking, then announces | Saved, search results, progress, "added to basket" |
+| `assertive` | Interrupts whatever is being spoken | Serious errors, a session about to expire |
+
+**Default to polite.** An interruption in the middle of a sentence is jarring, and the user may lose their place. Reserve **assertive** for messages the user must hear **right now.** If everything is urgent, nothing is.
+
+## The Live Roles
+
+You do not always need to write `aria-live` yourself. Several roles **come with** the right behaviour.
+
+| Role | Implied behaviour | Use it for |
+| :-- | :-- | :-- |
+| `status` | Polite, atomic | Saved, loaded, "12 results found" |
+| `alert` | Assertive, atomic | Errors and urgent warnings |
+| `log` | Polite, not atomic | Chat, activity feeds, a growing list |
+| `timer` | Off | Countdowns and clocks, not read on every tick |
+| `marquee` | Off | Rarely used; scrolling values like a ticker |
+
+See them in action:
+
+
+<>
+
+>
+
+
+### Which should I pick?
+
+- **Result of an action, not urgent** (saved, deleted, loaded): `role="status"`.
+- **Something went wrong, or action is needed now:** `role="alert"`.
+- **A running list of items:** `role="log"`.
+- **Not sure:** `role="status"`. It is the gentlest choice.
+
+:::info role or aria-live?
+`role="status"` and `aria-live="polite"` behave almost the same, but the role also tells assistive technology *what* the region is. When a role fits, use the role. Use `aria-live` on its own when you only need to set the politeness, such as on a custom widget.
+:::
+
+## Make the Whole Message Make Sense: aria-atomic
+
+When part of a live region changes, should the screen reader read **just the changed bit,** or **the whole region?**
+
+- **`aria-atomic="true"`:** read the **whole** region every time.
+- **`aria-atomic="false"`:** read only the **changed** parts. This is the default for `aria-live` on its own.
+
+It matters when a message is made of pieces. If only the number changes in "Basket: 3 items, total 12 dollars," reading only "4" would be baffling.
+
+
+<>
+
+>
+
+
+The simplest fix is to **keep the message as one piece of text** and replace the whole thing. Then atomic or not does not matter.
+
+## Other Controls
+
+### aria-relevant
+
+`aria-relevant` says **which kinds of change** to announce. Its values are `additions`, `removals`, `text`, and `all`. The default (`additions text`) is right nearly always. Setting it to `all` makes a screen reader announce **removals** as well, which is usually just noise. Support also varies, so do not rely on it.
+
+### aria-busy
+
+`aria-busy="true"` tells assistive technology: "I am still updating, please **wait** and read it all at the end." It is useful while a list loads in pieces.
+
+```html
+
+
+
+```
+
+Remember to set it back to `false`. Forget, and the region stays silent forever.
+
+## Where Live Regions Fit
+
+### Status messages (WCAG 4.1.3)
+
+**WCAG 2.1 success criterion 4.1.3 (Status Messages, Level AA)** says that when a message tells you the result of an action, or the state of the page, and **focus does not move to it,** assistive technology must be able to **announce** it. A live region is how. (Check the exact wording and level at w3.org before you rely on it.)
+
+### Search and filter results
+
+When a user types in a filter and the list changes, tell them how many results there are. Do not read the whole list.
+
+```html
+
+
+
+
...
+```
+
+When results change, set `count` to "5 recipes found." The list itself stays quiet.
+
+### Form errors
+
+Form errors need care. After a failed submit, a good pattern is to **move focus** to an error summary (see [Focus Management](./focus-management)). For single-field errors, link the message with `aria-describedby` (see [aria-describedby](./aria-describedby)), and you can also use `role="alert"` for brief, urgent ones.
+
+### Chat and feeds
+
+A chat box is a classic `role="log"`: new messages are added, and the reader hears each one. For busy chats, give users a way to **pause or slow** announcements.
+
+### Loading and progress
+
+Announce the start ("Loading...") only if it takes a while, and the finish ("12 recipes loaded"). Avoid announcing every percent.
+
+### Toasts and snackbars
+
+A "toast" is a small message that pops up and disappears. For a screen reader, it needs `role="status"` (or `alert`), **in the page before it fills,** and it must stay long enough to be heard. A message that vanishes in 2 seconds can be missed by anyone, and is a problem for people who read slowly.
+
+## Writing Good Announcements
+
+- **Short.** "Recipe saved." is better than a paragraph.
+- **Clear on their own.** Include the thing: "Rye bread added to basket." not just "Added."
+- **Plain text.** No links, buttons, or lists inside. Live regions are not for things people need to *operate.*
+- **Not repeated.** Setting the same text again may not be announced. If you must repeat it, clear the region, then set the text again a moment later.
+- **Rare.** Announce **outcomes,** not every keystroke or hover.
+- **In the page language,** matching the rest of the page.
+
+## What Not to Do
+
+- **Do not announce what focus already says.** If focus moves to the new content, the screen reader reads it. A live region on top makes a double announcement.
+- **Do not put a live region around a big block.** `aria-live` on `` or on a long list can make a screen reader read **everything** on each change.
+- **Do not hide a live region with `display: none`.** It then leaves the accessibility tree. (A visually hidden region, described in [aria-hidden](./aria-hidden), is fine.)
+- **Do not use `aria-live="assertive"` on lots of things.** It becomes an alarm that never stops.
+- **Do not use live regions for essential, permanent instructions.** Messages disappear. If the user needs it, show it as normal text, too.
+- **Do not make a toast the only place an important error appears.** Keep the message visible until fixed.
+
+## Check Your Live Regions
+
+
+
+
+```html title="check.html"
+
+
Saved!
+
Updating
+
Error
+
Status
+
...
+```
+
+
+
+
+
+<>
+
+>
+
+
+
+
+
+Run the messy sample, fix every problem, then try the fixed version.
+
+### Check it by hand
+
+1. **Turn on a screen reader** (NVDA or JAWS on Windows, VoiceOver on Mac and iPhone, TalkBack on Android). Trigger each message with the keyboard.
+2. **Listen carefully.** Is the message heard once? Is it complete? Is it too early or too late?
+3. **Try more than one screen reader and browser.** Live regions are one of the less consistent areas of ARIA.
+4. **Test the same message twice.** Is it announced the second time?
+5. **Check your page after a page load.** Is the region in the HTML, empty, from the start?
+6. **Use the browser's Accessibility panel** to see which elements have a live role or `aria-live`.
+
+## Good Habits
+
+- **Add the region to the HTML first,** empty, and update its text later.
+- **Use the role that fits** (`status`, `alert`, `log`) before writing `aria-live` by hand.
+- **Default to polite.** Keep assertive for emergencies.
+- **Keep messages short, plain, and complete.**
+- **Make the message visible too,** for people who do not use a screen reader.
+- **Use one region for many messages** instead of creating a new one each time.
+- **Do not combine live regions with focus changes** for the same message.
+- **Test with real screen readers.**
+
+## Accessibility Checklist
+
+- [ ] Every important status message has a live region (`status`, `alert`, or `aria-live`)
+- [ ] The live region is in the HTML before it is filled
+- [ ] Polite is the default, and assertive is rare
+- [ ] Messages are short, complete, and plain text
+- [ ] No links or buttons inside live regions
+- [ ] Live regions are not hidden with `display: none` or `hidden`
+- [ ] Toasts stay on screen long enough, and can be dismissed
+- [ ] The same message is not also announced by a focus move
+- [ ] Important errors stay visible until fixed
+- [ ] Tested with at least one real screen reader
+
+## Security and Privacy Notes
+
+- **Announcements are spoken aloud.** A live region can read out text in a public place, on a speaker, or in a screen-sharing call. Do not announce passwords, card numbers, health details, or other private information.
+- **Never put untrusted text into a live region with `innerHTML`.** If the message includes user input (a username in a chat), use `textContent` so a script cannot be injected.
+- **Do not let messages from other users flood a region.** A chat or notification feed that anyone can post to can be used to drown out or distract a screen reader user. Limit and filter it.
+- **Do not use announcements to mislead.** A message that says "Payment confirmed" must be true, and must match what is on screen.
+
+## Global Audience Notes
+
+- **Announcements are read in the page language** (the `lang` attribute), or in the user's screen reader voice. Set `lang` correctly, and mark foreign words with their own `lang`.
+- **Translate your messages,** and keep them short. Auto-translation may change the wording, so avoid depending on exact phrases.
+- **Numbers, dates, and currencies** should follow the user's local format, so they are read correctly.
+- **Right-to-left languages:** live regions work the same, but check that mixed text (numbers inside Arabic or Hebrew) reads in the right order.
+- **Slow connections:** announce "Loading" early, and the result when it finishes, so the user is not left in silence. But do not announce every step.
+- **Screen readers differ by country and device.** Free options like NVDA and TalkBack are widely used worldwide. Test with the tools your readers have.
+
+## Common Mistakes to Avoid
+
+1. **Creating the live region and its text at the same time.** The most common bug.
+2. **Using `display: none` or `hidden` on the region.**
+3. **Making everything `assertive`.**
+4. **Putting `aria-live` on a huge container,** like the whole main area.
+5. **`role="alert"` with `aria-live="polite"`,** which contradicts itself.
+6. **Messages that are too long, or have links or buttons in them.**
+7. **Toasts that disappear in a couple of seconds.**
+8. **Announcing the same thing with focus and a live region.**
+9. **Forgetting `aria-busy="false"`.**
+10. **Repeating the same text and expecting it to be read again.**
+11. **No visible version of the message** for people who do not use a screen reader.
+12. **Never testing with a screen reader.**
+
+## Practice Time
+
+Try these in your browser and your editor:
+
+1. Use the live region lab with each setting. If you can, listen with a screen reader.
+2. Untick "already in the page," and compare it with the ticked version.
+3. In the atomic lab, switch `aria-atomic` on and off, and explain why it matters.
+4. Use the live types lab. For each role, say when you would use it.
+5. Run the checker on the messy HTML, fix every problem, and run it again.
+6. Add a "Saved" message to a form in a page you built, using `role="status"`.
+7. Add a result count to a filterable list.
+8. Turn an existing toast into one that works for a screen reader, and stays long enough.
+9. Make a chat box with `role="log"`.
+10. Bonus: build a countdown that only announces at one minute, ten seconds, and zero.
+
+
+
+
+## Quick Recap
+
+Click a question to check your answer.
+
+
+1. What is a live region?
+
+A part of the page that a screen reader watches. When its content changes, the change is announced without moving focus.
+
+
+
+
+2. What is the difference between `polite` and `assertive`?
+
+Polite waits until the screen reader has finished speaking. Assertive interrupts. Use polite by default, and assertive only for urgent messages.
+
+
+
+
+3. Which role would you use for "Recipe saved"?
+
+`role="status"`. It is polite and atomic, and fits results that are not urgent. Use `role="alert"` for serious errors.
+
+
+
+
+4. Why must the live region exist before its text changes?
+
+Screen readers watch for *changes* inside a region they already know about. A region created together with its text is often not announced at all.
+
+
+
+
+5. What does `aria-atomic="true"` do?
+
+It makes the screen reader read the whole region each time, instead of only the part that changed, so the message makes sense.
+
+
+
+
+6. Which WCAG criterion covers status messages?
+
+4.1.3 Status Messages (Level AA in WCAG 2.1). Messages that report results or state, without taking focus, must be announced by assistive technology.
+
+
+
+## Key Takeaways
+
+- **A live region announces changes without moving focus.**
+- **Put the region in the page first, empty,** then update its text later.
+- **Default to `polite`.** Use `assertive` or `role="alert"` rarely.
+- **Use the roles** `status`, `alert`, `log`, and `timer` where they fit.
+- **Keep messages short, plain, and complete,** and show them on screen too.
+- **Do not hide live regions with `display: none`,** or wrap big blocks in them.
+- **Do not announce secrets,** and use `textContent` for any user text.
+- **Test with real screen readers.** Behaviour varies.
+
+Next, learn how to test everything you have built in this section, in [Accessibility Testing](./accessibility-testing).
\ No newline at end of file