QA How-To
Keyboard Accessibility Testing Guide for QA
Keyboard accessibility testing guide for QA: build a focus map, test forms and dialogs with Playwright, verify visible focus, and report reproducible defects.
20 min read | 3,035 words
TL;DR
Build a keyboard focus map for one important journey, complete it without a pointer, and automate the critical transitions. Check reachability, activation, visible focus, dialog containment, and recovery after actions.
Key Takeaways
- Map the expected focus path before asserting what Tab does.
- Test activation, reverse navigation, Escape, and post-action focus as well as reachability.
- Use native controls and a modal dialog to reduce custom keyboard behavior.
- Assert focused elements in Playwright, then inspect visible focus in a headed browser.
- Report keyboard defects with a starting state, exact keys, observed focus, and task impact.
- Treat automated coverage as a regression guard for named journeys, not a site-wide accessibility verdict.
A Keyboard Accessibility Testing Guide for QA should answer a practical question: can someone finish the important task with a keyboard, understand where focus is, and get out of every interaction? You will test a small checkout flow with only Tab, Shift+Tab, Enter, Space, Escape, and the form keys that native controls support. The result is an auditable path, not a vague claim that the page is "keyboard accessible."
This tutorial builds a local page and a Playwright regression suite. It also gives you a manual pass that catches visual and context problems a scripted assertion can miss. For a broader inventory beyond this one flow, use the accessibility testing checklist. The testing keyboard navigation guide covers more widget patterns.
A passing script establishes only the behaviors it asserts in the browser you ran. Pair it with human observation of labels, meaning, layout, zoom, and assistive technology output before making a release claim.
What You Will Build
- A local checkout fixture with a skip link, a short form, a modal help dialog, and a completion message.
- A written focus map that states where Tab, Shift+Tab, Enter, Space, and Escape should lead.
- Playwright tests for forward and reverse focus, control operation, dialog boundaries, and focus recovery.
- A small visual-focus check and a manual defect record that can be reused on your product.
The example deliberately uses native links, labels, inputs, buttons, and <dialog>. Those elements give the browser useful keyboard behavior before any custom JavaScript runs. You will still verify the behavior rather than assuming native markup guarantees an accessible experience.
| Evidence | What it proves | What it cannot prove alone |
|---|---|---|
| Keyboard-only task pass | A person can attempt the whole flow without a pointer | Consistent behavior across every browser and assistive technology |
| Focus assertions | A specific control receives focus after a key press | The order makes sense to a person reading the page |
| Computed style and viewport check | An indicator exists and the focused box is on screen | The indicator is easy to distinguish in every theme or zoom level |
| Automated accessibility scan | Some detectable markup and rule failures | Every keyboard path, dialog exit, or task outcome |
Prerequisites
Use a currently supported Node.js release: the Playwright installation requirements list the latest 22.x, 24.x, or 26.x lines at the time of writing. Check your exact local version with node --version and npm --version; record the output in your test run. Install the latest compatible @playwright/test from the official npm distribution and use npx playwright --version to record its exact resolved version. Do not copy an assumed package version from an article into a long-lived lockfile. Commit the lockfile produced by your own installation when you move the example into a real repository.
You need a shell, an editor, and permission to install Playwright's Chromium build. The commands below use a new directory so the relative paths stay consistent. On a managed machine, follow your organization's browser-install process; the test code itself does not require a hosted service. If Chromium is already installed for the same Playwright package version, the install command reuses the matching browser binary.
node --version
npm --version
mkdir keyboard-audit
cd keyboard-audit
npm init -y
npm install --save-dev @playwright/test@latest
npx playwright install chromium
npx playwright --version
Verify: The last command prints the version actually installed, and npx playwright install chromium exits successfully. If your Node line is outside the supported lines in the current Playwright documentation, switch Node first and reinstall dependencies. The browser binary must match the Playwright package that launches it.
Step 1: Create a Keyboard-First Checkout Page
Create checkout.html in keyboard-audit. This is a deliberately small product surface, not a synthetic collection of focusable tags. The person can skip navigation, enter contact data, request help, and submit. The completion message receives programmatic focus so a keyboard user does not lose their place after the form disappears.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Checkout keyboard audit</title>
<style>
body { font: 1rem/1.5 system-ui, sans-serif; max-width: 44rem; margin: 2rem auto; padding: 0 1rem; }
a, button, input { font: inherit; }
label { display: block; margin-top: 1rem; }
input { padding: .4rem; }
button { margin-top: 1rem; padding: .5rem .8rem; }
:focus-visible { outline: 3px solid #174ea6; outline-offset: 3px; }
.skip { position: absolute; left: -10000px; }
.skip:focus-visible { position: static; }
dialog { border: 2px solid #333; border-radius: .4rem; padding: 1.5rem; }
</style>
</head>
<body>
<a class="skip" href="#main">Skip to checkout</a>
<header><a href="#help">Help topics</a></header>
<main id="main" tabindex="-1">
<h1>Checkout</h1>
<form id="checkout">
<label for="name">Full name</label>
<input id="name" name="name" autocomplete="name" required>
<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<label><input id="updates" type="checkbox"> Send me updates</label>
<button id="open-help" type="button">Delivery help</button>
<button type="submit">Place order</button>
</form>
<p id="result" tabindex="-1" hidden>Order placed.</p>
<section id="help"><h2>Help topics</h2><p>Ask about delivery before ordering.</p></section>
</main>
<dialog id="delivery-dialog" aria-labelledby="dialog-title">
<h2 id="dialog-title">Delivery help</h2>
<label for="question">Your question</label>
<input id="question">
<button id="close-help" type="button">Close help</button>
</dialog>
<script>
const form = document.querySelector('#checkout');
const dialog = document.querySelector('#delivery-dialog');
const opener = document.querySelector('#open-help');
const question = document.querySelector('#question');
const result = document.querySelector('#result');
opener.addEventListener('click', () => {
dialog.showModal();
question.focus();
});
document.querySelector('#close-help').addEventListener('click', () => dialog.close());
dialog.addEventListener('close', () => opener.focus());
form.addEventListener('submit', event => {
event.preventDefault();
form.hidden = true;
result.hidden = false;
result.focus();
});
</script>
</body>
</html>
Use tabindex="-1" only for destinations that need scripted or fragment focus. It does not add the main region or result paragraph to the regular Tab sequence. The skip link becomes visible when it receives keyboard focus. The dialog uses showModal(), which makes background content inert while open. The explicit focus calls make the start and end of the interaction predictable. On a real page, do not hide a completed form if the user still needs to review or correct data; this fixture keeps the post-submit transition easy to see.
Verify: Run test -f checkout.html in the project directory. Open the file in a browser, press Tab from the address bar or page start, and confirm the skip link appears. The fixture is local; it does not send checkout data anywhere.
Step 2: Write the Expected Focus Map Before Automating
A focus-order test is meaningful only if you decide the expected order from the task, not from whatever the current DOM happens to do. Write focus-map.md in the same directory. This record is a small test oracle for the fixture and a model for reviewing a production flow with a designer or product owner. Include reverse paths and dynamic changes, since one-way Tab checks miss many failures.
# Checkout keyboard contract
1. From page start, Tab reaches Skip to checkout, then Help topics,
then Full name, Email address, Send me updates,
Delivery help, and Place order.
2. Enter on Skip to checkout moves focus to the main region.
The next Tab reaches Full name.
3. Space on Send me updates toggles it. Shift+Tab can return
from Delivery help to Send me updates.
4. Enter or Space on Delivery help opens the modal dialog.
Focus starts on Your question. Tab reaches Close help;
another Tab wraps to Your question. Escape closes the dialog
and restores focus to Delivery help.
5. A valid Place order submission moves focus to Order placed.
A user can still understand what happened without a pointer.
The map intentionally names the user-facing controls. An array of CSS selectors alone would hide whether the order tells a coherent story. Review the map against the rendered layout: the help link in the header comes before the form, while the help dialog is encountered only when requested. If a responsive layout changes presentation order, check that the keyboard sequence still preserves meaning and operation. WCAG's focus order explanation does not require one universal sequence; it requires a meaningful one when sequence matters.
Verify: Run test -f focus-map.md and read it alongside checkout.html. Every named control should exist with the same visible label. For your application, resolve any mismatch between design intent and DOM order before capturing assertions, or an automated test will preserve the wrong behavior.
Step 3: Apply the Keyboard Accessibility Testing Guide to Focus Order
Create tests/keyboard.spec.ts. The first test starts at the document and presses Tab, rather than using locator.focus() to jump to each expected element. That distinction matters: forcing focus can make an unreachable control appear testable. Use Playwright's keyboard API for input and auto-retrying focus assertions for the result.
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { expect, test } from '@playwright/test';
const fixture = resolve(process.cwd(), 'checkout.html');
async function openCheckout(page: import('@playwright/test').Page) {
await page.setContent(await readFile(fixture, 'utf8'));
}
test('forward and reverse focus follow the checkout task', async ({ page }) => {
await openCheckout(page);
const order = [
page.getByRole('link', { name: 'Skip to checkout' }),
page.getByRole('link', { name: 'Help topics' }),
page.getByRole('textbox', { name: 'Full name' }),
page.getByRole('textbox', { name: 'Email address' }),
page.getByRole('checkbox', { name: 'Send me updates' }),
page.getByRole('button', { name: 'Delivery help' }),
page.getByRole('button', { name: 'Place order' }),
];
for (const control of order) {
await page.keyboard.press('Tab');
await expect(control).toBeFocused();
}
for (const control of order.slice(0, -1).reverse()) {
await page.keyboard.press('Shift+Tab');
await expect(control).toBeFocused();
}
});
The code uses roles and accessible names, so the failure points to a user-facing control. The checkbox sits within a label, which gives it the name "Send me updates." Do not interpret this single sequence as proof that every route is reachable. Repeat the map for the login, search, checkout, upload, or other tasks that matter to your release. In a long production page, split the test at stable landmarks so one new footer link does not obscure a checkout regression.
Verify: Run mkdir -p tests before saving the file, then npx playwright test tests/keyboard.spec.ts --grep "forward and reverse". The test should report one pass. If the first Tab lands somewhere unexpected, inspect the active element and any browser extension or injected banner before changing the expected map.
Step 4: Exercise Skip, Toggle, and Submit With Real Keys
Append the next test to tests/keyboard.spec.ts. It checks an important difference between focus and operation. A button can be reachable by Tab and still fail to respond to Enter or Space if a developer replaced native behavior with an incomplete custom handler. A checkbox can be visible but have no usable label or state change.
test('skip link, checkbox, and submission work by keyboard', async ({ page }) => {
await openCheckout(page);
await page.keyboard.press('Tab');
await expect(page.getByRole('link', { name: 'Skip to checkout' })).toBeFocused();
await page.keyboard.press('Enter');
await expect(page.locator('#main')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByRole('textbox', { name: 'Full name' })).toBeFocused();
await page.keyboard.type('Avery Tester');
await page.keyboard.press('Tab');
await page.keyboard.type('avery@example.test');
await page.keyboard.press('Tab');
const updates = page.getByRole('checkbox', { name: 'Send me updates' });
await expect(updates).toBeFocused();
await page.keyboard.press('Space');
await expect(updates).toBeChecked();
await page.keyboard.press('Space');
await expect(updates).not.toBeChecked();
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await expect(page.getByRole('button', { name: 'Place order' })).toBeFocused();
await page.keyboard.press('Enter');
await expect(page.getByText('Order placed.')).toBeFocused();
});
The skip-link assertion checks that fragment navigation lands on the main region, then Tab resumes at the first form field. The .test email domain is sample data, not an address to contact. Submit occurs only after valid fields are filled, so a browser validation message cannot disguise a focus-management failure. Test the invalid path separately in your product: a required-field error should be perceivable, identify the field, and allow correction without forcing the user to restart.
Verify: Run npx playwright test tests/keyboard.spec.ts --grep "skip link". Expect one pass and a focused completion message. Also run the fixture manually: after Space checks the box, Space again should clear it without scrolling the page. Native checkbox behavior supplies that interaction here; custom switches need their own keyboard contract.
Step 5: Prove the Modal Is Contained and Escapable
A modal should move focus inside, keep Tab and Shift+Tab within its controls, close with Escape, and return focus to the trigger when the trigger still exists. The WAI-ARIA dialog pattern describes those expectations. It also explains why aria-modal="true" alone cannot make background controls inert. Our fixture uses the native dialog's modal behavior and checks the actual focus path.
test('dialog keeps keyboard focus and returns it on Escape', async ({ page }) => {
await openCheckout(page);
const opener = page.getByRole('button', { name: 'Delivery help' });
for (let i = 0; i < 6; i += 1) await page.keyboard.press('Tab');
await expect(opener).toBeFocused();
await page.keyboard.press('Enter');
const dialog = page.getByRole('dialog', { name: 'Delivery help' });
await expect(dialog).toBeVisible();
const question = page.getByRole('textbox', { name: 'Your question' });
const close = page.getByRole('button', { name: 'Close help' });
await expect(question).toBeFocused();
await page.keyboard.press('Tab');
await expect(close).toBeFocused();
await page.keyboard.press('Tab');
await expect(question).toBeFocused();
await page.keyboard.press('Shift+Tab');
await expect(close).toBeFocused();
await page.keyboard.press('Escape');
await expect(dialog).toBeHidden();
await expect(opener).toBeFocused();
});
The loop counts the six presses from page start to the opener in this exact fixture: skip link, help link, two textboxes, checkbox, and button. For a production page, favor a task-specific start condition over a hard-coded count when banners or account menus can appear. The test checks both forward wrap and reverse wrap so the dialog does not accidentally leak to the checkout form. Escape is a recovery path, not just a convenience. Also test the visible Close help button in your product because some users may choose it instead of Escape.
Verify: Run npx playwright test tests/keyboard.spec.ts --grep "dialog keeps". Expect one pass. Then run npx playwright test tests/keyboard.spec.ts --grep "dialog keeps" --headed and watch focus move within the dialog. The visible focus indicator should move with it.
Step 6: Inspect Focus Visibility and Obscuration
A focused element can pass toBeFocused() while its outline is removed, clipped, hidden behind a sticky header, or indistinguishable from its surroundings. Add a targeted check for the fixture's focus indicator. The computed-style assertion is a useful guard against outline: none; the viewport assertion catches a fully offscreen focus target. Human review remains necessary for contrast, partial obscuration, zoom, and theme changes.
test('keyboard focus has an indicator and stays in view', async ({ page }) => {
await openCheckout(page);
await page.keyboard.press('Tab');
const skip = page.getByRole('link', { name: 'Skip to checkout' });
await expect(skip).toBeFocused();
const style = await skip.evaluate(element => {
const css = getComputedStyle(element);
const box = element.getBoundingClientRect();
return {
outlineStyle: css.outlineStyle,
outlineWidth: css.outlineWidth,
top: box.top,
bottom: box.bottom,
viewportHeight: innerHeight,
};
});
expect(style.outlineStyle).not.toBe('none');
expect(parseFloat(style.outlineWidth)).toBeGreaterThan(0);
expect(style.bottom).toBeGreaterThan(0);
expect(style.top).toBeLessThan(style.viewportHeight);
});
The check intentionally makes a narrow claim: the skip link receives a nonzero outline and at least part of its box is within the viewport. It does not calculate whether the focus indicator meets a particular contrast ratio. In a real app, repeat the visual pass for the darkest and lightest themes, at increased zoom, and when sticky navigation is present. WCAG 2.2's focus visible guidance and focus not obscured guidance give distinct review questions. Record a screenshot or short clip when the indicator is lost, because a DOM assertion alone does not show the user impact.
Verify: Run npx playwright test tests/keyboard.spec.ts --grep "indicator". Expect one pass. Temporarily change the :focus-visible CSS rule to outline: none in a copy of the fixture and confirm that this test fails; then restore it. That negative control demonstrates that the assertion can detect the intended regression.
Step 7: Turn the Keyboard Accessibility Testing Guide Into a Release Check
Run all four tests together, then perform a manual pass on the same workflow. The automated suite protects repeatable transitions. The manual pass asks whether the sequence and visible state make sense. Use the recorded package and browser versions when filing a defect so someone else can reproduce it. If a production app changes based on authentication, viewport, or feature flags, include those conditions in the report.
npx playwright test tests/keyboard.spec.ts --reporter=list
npx playwright test tests/keyboard.spec.ts --headed --grep "dialog keeps"
Use this compact run sheet during the headed pass:
Environment: browser/build, operating system, viewport, zoom, theme
Start: reload checkout and keep hands off the pointer
Keys: Tab forward, Shift+Tab backward, Enter, Space, Escape
Observe: focus location, indicator, control state, dialog boundaries
Finish: submit valid data and locate the completion message
Recovery: reopen help, Escape, then continue from Delivery help
Evidence: exact focused label before and after the failing key
For release coverage, run the same journey at a narrow viewport and at increased zoom, then compare the focus sequence with the desktop map. A mobile layout may move controls visually while leaving the DOM sequence intact; the result is acceptable only when meaning and operation still follow a sensible path. Check an error state after submitting empty required fields, a success state after valid data, and a reopened dialog after Escape. Those transitions often reveal stale focus targets that a happy-path test never visits. If the application includes shortcuts, verify that a shortcut does not fire while someone is typing in a text field. Record the browser and input configuration alongside the outcome, because native control behavior can differ across platforms.
Do not call every repeated Tab stop a defect. A composite widget may use arrow keys internally, while Tab moves out of it. Do call out dead ends, missing visible focus, a dialog that leaves its background reachable, a button that ignores expected activation keys, or focus that disappears after a state change. The Playwright focus-order tutorial goes deeper on stabilizing those assertions in an existing app. For a larger test strategy, the accessibility testing with Playwright guide shows where automation fits.
Verify: The command reports four passing tests, and you can complete the fixture manually without touching a pointer. If the manual result disagrees with the suite, treat the disagreement as a coverage gap. Add a precise assertion only after you understand the observed failure; do not change the expected result to make the test green.
Step 8: Capture a Defect With a Reproducible Keyboard Path
A keyboard defect report should name the starting state, exact key sequence, expected focus or action, actual focus or action, and impact. "Tab order broken" leaves too much for a developer to guess. The example below is a template for a real failure you might find after replacing the fixture dialog with an application component. It is not a claim that the working fixture has this bug.
Title: Escape closes Delivery help but focus moves to the browser toolbar
Environment: Chromium build recorded from the Playwright run, desktop viewport
Start: Checkout loaded; no pointer input; focus on Delivery help
Steps: Press Enter; confirm focus on Your question; press Escape
Expected: Dialog closes and Delivery help receives visible focus
Actual: Dialog closes; no control in the page shows visible focus
Impact: Keyboard user must search from the start of the page to continue
Evidence: short recording plus failing toBeFocused() assertion
Tie the defect to a task. If focus returns to a removed trigger after a successful delete action, returning to that trigger is impossible; a nearby heading or next logical control can be the correct destination. If a modal requires a nonstandard exit key, the method must be explained to users and tested. Separate a WCAG criterion from a specific failure: one broken interaction may touch keyboard operation, focus order, and focus visibility, but the reproduction should show exactly what failed. The guide to building an accessibility test checklist helps turn these observations into release criteria.
Verify: Run npx playwright test tests/keyboard.spec.ts --grep "dialog keeps" to confirm the expected dialog path, then copy the template into a draft issue and have a teammate follow the steps without verbal coaching. They should reproduce the same observed focus change. If they cannot, add the missing state, browser setting, or key sequence before filing. A reproducible report shortens the path from detection to a fixed interaction.
Troubleshooting
Problem: Playwright says the Chromium executable does not exist -> Run npx playwright install chromium from the project with the installed @playwright/test package. Check npx playwright --version and reinstall the browser after updating the package; a browser downloaded for another version may not match. The Playwright browser installation guide describes that pairing.
Problem: The first Tab does not focus the skip link -> Start from a fresh page and avoid clicking into the document before the test. Check for extension UI, injected consent banners, or app startup code that calls .focus(). In a real app, document the initial focus state instead of assuming it is the browser's default.
Problem: The skip-link assertion fails after Enter -> Confirm the target exists and is focusable with tabindex="-1". A fragment link can scroll without placing focus where the next Tab should resume, especially after client-side routing. Test the next Tab too; the destination alone is not the whole user path.
Problem: The dialog test passes visually but Tab escapes behind it -> Verify the dialog was opened with showModal(), not show(), and that only one modal is active. A styled <div role="dialog"> does not supply native modal focus containment. Inspect focus after both Tab and Shift+Tab at the boundaries.
Problem: toBeFocused() passes but users cannot find the focus indicator -> Inspect :focus-visible, outline clipping, color contrast, sticky headers, and zoomed layout. Keep the test that guards a basic outline, then add visual evidence from the affected theme and viewport. Focus presence and focus visibility are separate observations.
Problem: The form submits but the next key appears to do nothing -> Inspect document.activeElement after the transition. If the focused submit button was removed, move focus to a logical result or next action and expose the outcome in text. Avoid automatically focusing an unrelated navigation item merely to make Tab resume somewhere.
Interview Questions and Answers
The runnable example supplies evidence for the interview answers in the interviewQnA field below. Explain how you would test the user's task, how you would know where focus moved, and which visual or assistive technology checks still require a person. For broader preparation, review accessibility testing interview questions.
Common Mistakes
- Using
locator.focus()as proof that Tab can reach an element. It can bypass a broken sequence. - Checking forward Tab only. Shift+Tab and Escape reveal recovery failures.
- Treating every option inside a composite widget as a separate Tab stop. Follow the widget's documented key pattern.
- Accepting
toBeFocused()as proof of visible focus. Inspect the rendered indicator and its position. - Testing an empty form only, then overlooking validation and post-submit focus.
- Recording a failure without the exact starting state, keys, browser, and observed target.
Where To Go Next
Move this fixture-based method into one high-value product journey. Start with the page where a keyboard user must log in, upload a resume, or complete an order. Write its focus map with product and design, then replace the fixture loader with a navigation to that route. Keep the same distinctions among reachability, operation, visibility, and recovery. A pass on one route is a useful regression check; it is not a site-wide certification.
Pair keyboard work with the automated accessibility with axe-core guide for detectable markup issues, and use the CI accessibility checks guide to place stable tests in your pipeline. Those checks complement, rather than replace, a person completing the task with the keyboard. If you are preparing for QA interviews, practice describing one defect in terms of the user's blocked task and the exact key sequence that reproduces it.
The immediate next step is simple: choose a real workflow, press Tab from a known state, write down every focus destination, and complete the task without a pointer. Turn the surprising transitions into focused tests and actionable defects.
Interview Questions and Answers
How would you plan a keyboard accessibility test for checkout?
I would map the expected focus order from page entry through submission, including skip navigation, fields, optional controls, and the result state. Then I would complete the task without a pointer, checking reverse navigation and visible focus. I would automate stable transitions and record any divergence with exact keys and starting state.
Why is calling focus() insufficient as a reachability test?
Programmatic focus can place the cursor on an element that Tab cannot reach. To test reachability, I start from a known focus state, press the same keys a user would, and assert the next focused control. I reserve focus() for setup only when the test purpose is another behavior.
How do you distinguish focus order from DOM order?
DOM order often determines sequential navigation, but CSS layout and scripted focus can change what users perceive. I compare the actual focus path with the task and reading sequence, not merely with an element list. If two orders are possible, I ask whether the chosen order preserves meaning and operation.
What keyboard behaviors would you assert for a modal?
I would check initial focus inside the dialog, forward and reverse containment, Escape dismissal, and a logical return destination. I would also verify that underlying controls cannot be reached while the modal is active. The visible close control needs a separate activation check.
How would you test focus visibility beyond toBeFocused()?
I would inspect the rendered page in keyboard mode at representative themes, zoom levels, and viewport sizes. A code-level check can catch a missing outline or offscreen target, but it cannot fully judge whether a human can see the indicator. I would attach visual evidence when reporting a failure.
What is a keyboard trap, and how would you report one?
A keyboard trap occurs when focus enters a component but cannot leave using the keyboard in an expected or explained way. My report would state the entry control, the exact keys tried, the focused element after each key, and the task blocked. A modal containing focus while open is intentional only if the user can dismiss or complete it.
What is the right automation scope for keyboard accessibility?
I would automate critical and stable journeys such as sign-in, checkout, and key dialogs, asserting specific focus transitions and outcomes. I would avoid a brittle test that snapshots every focusable element across the whole site. Manual exploration remains necessary for new widgets, visual focus, and unexpected states.
Frequently Asked Questions
What is keyboard accessibility testing?
It is the practice of completing a web task using a keyboard and checking that focus is reachable, ordered, visible, and recoverable. A QA pass also checks expected activation keys, dynamic content, and exits from dialogs or other overlays.
Which keys should QA use in a keyboard-only test?
Start with Tab and Shift+Tab for sequential navigation, Enter and Space for activation, and Escape for dismissible overlays. Add arrow keys, Home, and End when the specific widget pattern calls for them; do not assume every widget uses the same keys.
Does a passing Playwright keyboard test prove WCAG compliance?
No. It proves the named assertions in the tested browser and state. Manual review is still needed for meaningful order, clear focus indication, zoom, alternate states, and assistive technology experience.
Should every item in a menu be reachable with Tab?
Not necessarily. Many composite widgets use one Tab stop and arrow keys to move among their internal choices. Check the applicable widget pattern and ensure a keyboard user can enter, operate, and leave it.
How do I test a modal dialog with a keyboard?
Open it with the keyboard, confirm focus moves inside, use Tab and Shift+Tab at its boundaries, close it with Escape, and verify focus returns to a logical destination. Also test any visible close control and confirm background controls are unavailable while the modal is open.
What should happen to focus after a form submission?
Focus should move to a logical place that communicates the result or supports the next task. If the original submit button disappears, leaving focus on a removed node can strand a keyboard user. Test both success and validation-error paths.
How can I tell whether the focus indicator is accessible?
Inspect it while navigating with the keyboard in the browser. Check that it remains visible, is distinguishable from surrounding colors, and is not hidden behind overlays or sticky content; computed CSS alone is insufficient.