WordPress Accessibility Audit
Use this when making a WordPress plugin, admin page, frontend feature, or classic theme accessible. The target baseline is WCAG 2.2 Level A and AA, matching WordPress' stated accessibility commitment. AAA is encouraged where practical, but do not claim full AAA unless every relevant AAA criterion has been audited.
This skill is not a legal certification. It is an implementation and review checklist for code.
When to Use This Skill
- The task says accessibility, a11y, WCAG, screen reader, keyboard, akadalymentesites, ARIA, or accessible forms.
- Reviewing admin screens, settings pages, metaboxes, list tables, media frames, custom dialogs, AJAX UI, or frontend shortcode output.
- Adding labels/help text/errors for fields.
- Fixing color contrast, focus visibility, target sizes, font sizing, reduced motion, or hover-only interactions.
- Making live updates announce through
wp.a11y.speak().
Baseline Rules
- Content and controls are usable with keyboard only.
- Every interactive element has a correct native role or ARIA role.
- Every form control has an accessible name.
- Focus is visible and not hidden behind sticky headers/toolbars.
- Text and UI contrast meet WCAG AA.
- Text can be zoomed to 200% without loss of content or functionality.
- Motion-heavy UI respects reduced-motion preferences.
- Errors are visible, understandable, and programmatically associated with fields.
- Dynamic updates are announced when they change user-relevant state.
- Automated tests are supplemented by keyboard and screen-reader checks.
ARIA Decision Rule
Prefer native HTML first. ARIA patches semantics; it does not make broken interaction accessible by itself.
Use this order:
- Correct native element:
<button>, <a>, <label>, <input>, <select>, <textarea>, <fieldset>, <legend>, <dialog>, <nav>, <main>.
- Visible text labels.
aria-labelledby when visible text elsewhere labels the control.
aria-label only when no visible label is possible, usually icon-only buttons or named landmarks.
aria-describedby for help text, constraints, and errors. It is not the field's name.
Do not add ARIA roles that duplicate or contradict native semantics.
Form Fields
Every input/select/textarea needs a real accessible name.
Good visible label:
<label for="myplugin_api_key"><?php esc_html_e( 'API key', 'textdomain' ); ?></label>
<input
type="text"
id="myplugin_api_key"
name="myplugin_options[api_key]"
value="<?php echo esc_attr( $api_key ); ?>"
aria-describedby="myplugin_api_key_help"
>
<p id="myplugin_api_key_help" class="description">
<?php esc_html_e( 'Create this key in your provider dashboard.', 'textdomain' ); ?>
</p>
- Do not use placeholder text as the only label.
- Do not use
aria-label when a visible <label> can exist.
- Keep
for and id exactly paired.
- Use
aria-describedby for help text and constraints.
- For required fields, use native
required where validation supports it, plus visible required text where needed.
- For invalid fields, set
aria-invalid="true" and connect the error with aria-describedby.
- Radio/checkbox groups need
<fieldset> and <legend>.
Error example:
<label for="myplugin_email"><?php esc_html_e( 'Notification email', 'textdomain' ); ?></label>
<input
type="email"
id="myplugin_email"
name="myplugin_email"
value="<?php echo esc_attr( $email ); ?>"
aria-describedby="myplugin_email_help myplugin_email_error"
aria-invalid="true"
>
<p id="myplugin_email_help" class="description"><?php esc_html_e( 'Used for failure alerts.', 'textdomain' ); ?></p>
<p id="myplugin_email_error" class="notice notice-error inline">
<?php esc_html_e( 'Enter a valid email address.', 'textdomain' ); ?>
</p>
Buttons, Links, Icons
- Use
<button type="button"> for actions that change UI state.
- Use
<a href="..."> for navigation.
- Never use
<a href="#"> or clickable <div> for buttons.
- Icon-only buttons need visible text,
.screen-reader-text, or aria-label.
- Decorative icons inside named controls should use
aria-hidden="true" and not receive focus.
- Repeated links like "Read more" need extra context through visible text or
.screen-reader-text.
WordPress 7.1 adds wp_get_tooltip() for a short control name and
wp_get_toggletip() for longer supporting context. Visible labels are still
preferred. Read references/tooltips-71.md before
using the helpers or supplying custom trigger markup.
Icon button:
<button type="button" class="button myplugin-refresh">
<span class="dashicons dashicons-update" aria-hidden="true"></span>
<span class="screen-reader-text"><?php esc_html_e( 'Refresh import status', 'textdomain' ); ?></span>
</button>
Keyboard and Focus
Tab reaches every interactive element in a logical order.
Shift+Tab works backward.
Enter activates links and submit buttons.
Space activates buttons, checkboxes, and toggles.
Escape closes modals, popovers, and autocomplete popups.
Arrow-key behavior is implemented for custom menu/listbox/tab/slider patterns only when the ARIA APG pattern requires it.
Do not use positive tabindex.
Use tabindex="-1" only for programmatic focus targets such as error summaries or modal containers.
Never remove outlines without a visible replacement.
After AJAX save/delete/filter operations, keep focus stable or move it deliberately to the next useful place.
Do not trap focus except in true modal dialogs.
When closing a modal, return focus to the control that opened it.
Focus CSS:
.myplugin-ui :focus-visible {
outline: 2px solid #1d2327;
outline-offset: 2px;
}
@media (forced-colors: active) {
.myplugin-ui :focus-visible {
outline: 2px solid CanvasText;
}
}
Dynamic Updates and Notices
For admin JavaScript that updates state without a full page load, enqueue wp-a11y and announce meaningful changes.
wp_enqueue_script(
'myplugin-admin',
plugins_url( 'assets/admin.js', __FILE__ ),
array( 'wp-a11y', 'wp-i18n' ),
'1.0.0',
true
);
const { __ } = wp.i18n;
wp.a11y.speak( __( 'Settings saved.', 'textdomain' ) );
- Announce results, not implementation details.
- Use
polite announcements for normal updates and assertive only for urgent errors.
- Visible notices still matter; screen-reader announcements do not replace visible feedback.
- Error summaries should be focusable with
tabindex="-1" and focused after failed validation.
Color, Text, and Layout
Normal text contrast: at least 4.5:1.
Large text contrast: at least 3:1.
UI components and graphical state indicators: at least 3:1.
Do not use color as the only way to show errors, selected state, required fields, or links in prose.
Use relative units for text: rem, em, %.
As design guidance, start body text at 1rem, avoid UI text below 14px, and
prefer 16px for content/form-heavy screens. WCAG does not define a universal
minimum font-size pass/fail threshold.
Use line-height around 1.4 to 1.6 for readable body text.
Avoid fixed-height containers for text that can wrap or zoom.
Test text zoom at 200%, and test reflow at 400% zoom / a 320 CSS-pixel-wide
viewport without two-dimensional scrolling except for allowed content such
as data tables.
Test WCAG text-spacing overrides: line height 1.5, paragraph spacing 2em,
letter spacing 0.12em, and word spacing 0.16em; content and controls must
remain available.
Reduced motion:
@media (prefers-reduced-motion: reduce) {
.myplugin-nonessential-animation {
scroll-behavior: auto !important;
animation: none !important;
transition: none !important;
}
}
Scope reduced-motion changes to nonessential effects. Do not globally shorten
animations when application logic waits for animationend/transitionend;
provide a no-motion code path and test that completion still occurs.
- WCAG 2.2 AA target size is 24 by 24 CSS pixels, with defined exceptions for
sufficient spacing, inline text, equivalent larger controls, user-agent
controls, and essential presentation. Audit the exception before reporting
every smaller compact control as a failure.
- Prefer 44 by 44 CSS pixels for touch-heavy frontend UI.
- If the visual icon is smaller, increase clickable padding.
Landmarks, Headings, and Tables
- Use one meaningful
h1 for the screen/admin page title.
- Keep headings in logical order; do not choose heading levels by visual size.
- Admin pages should use the normal
.wrap > h1 pattern.
- Use landmarks for major areas:
main, nav, aside, header, footer.
- Multiple
nav landmarks need names with aria-label or aria-labelledby.
- Data tables need header cells with
scope="col" or scope="row".
- On WordPress 7.1 list tables, the primary column—not the checkbox column—is
the row header. Custom
WP_List_Table renderers must preserve that structure.
- Do not use tables for layout.
Media and Images
- Informative images need meaningful alt text.
- Decorative images use empty
alt="".
- Do not repeat adjacent text in alt text.
- SVG icons that are decorative use
aria-hidden="true" focusable="false".
- Audio/video must not autoplay with sound.
- Captions/transcripts are required when media conveys information.
Custom Components
Before building a custom widget, check whether native HTML or a WordPress component already solves it.
Use the WAI-ARIA APG pattern for the component type.
Implement the documented keyboard interaction.
Manage focus deliberately.
Keep ARIA state synchronized: aria-expanded, aria-selected, aria-checked, aria-disabled, aria-controls.
Test with keyboard and at least one screen reader.
Modal dialogs.
Autocomplete/listbox.
Tabs.
Accordions/disclosures.
Drag-and-drop UIs.
Toasts/live updates.
Date pickers.
Audit Workflow
- Identify every interactive element.
- Verify accessible name, role, value, and state.
- Tab through the whole UI without a mouse.
- Trigger validation errors and verify visible/focus/ARIA behavior.
- Test 200% text zoom, 400%/320-CSS-pixel reflow, and text-spacing overrides.
- Check contrast for text, focus, borders, icons, and error states.
- Disable animations through reduced-motion preference.
- Run an automated checker, then manually verify anything it cannot know.
- Record issues with severity and WCAG/WordPress rationale.
Severity Guide
- Critical: keyboard trap, unreachable primary action, missing accessible names on required controls, modal focus broken, security/checkout/account flow unusable.
- High: invalid fields not announced, focus invisible, insufficient contrast on important text/actions, destructive action ambiguity, dynamic state not announced.
- Medium: poor heading order, missing landmark names, weak help text
association, target size below 24px without a WCAG exception, or
nonessential motion not reduced.
- Low: redundant labels, minor screen-reader verbosity, cosmetic focus inconsistency that remains usable.
Common Mistakes
- Common failures: replacing visible labels with
aria-label, using
aria-describedby as the name, hiding labels with display:none, removing
outlines, positive tabindex, click handlers on non-interactive elements,
color-only state, silent AJAX completion, and trusting an automated scan as
proof of accessibility.
Cross-References
- Pair with
wp-admin-settings-api, wp-admin-list-table, or
wp-admin-media-frame for those specific admin components.
- Use
classic-theme-accessibility-semantics for classic theme document structure and landmarks.
References
1---2name: wp-accessibility-audit3description: Audit or implement accessibility for WordPress plugins, admin screens, frontend plugin output, and classic themes against WCAG 2.2 A/AA and WordPress accessibility patterns. Use when the user asks for akadalymentesites/accessibility/a11y, form field labels, `aria-label`, `aria-describedby`, keyboard navigation, focus states, admin notices, live AJAX updates, modal/dialog UI, color contrast, font sizing, reduced motion, target size, screen-reader text, image alt text, or making a plugin/theme usable without a mouse or screen.4---56# WordPress Accessibility Audit78Use this when making a WordPress plugin, admin page, frontend feature, or classic theme accessible. The target baseline is WCAG 2.2 Level A and AA, matching WordPress' stated accessibility commitment. AAA is encouraged where practical, but do not claim full AAA unless every relevant AAA criterion has been audited.910This skill is not a legal certification. It is an implementation and review checklist for code.1112## When to Use This Skill1314- The task says accessibility, a11y, WCAG, screen reader, keyboard, akadalymentesites, ARIA, or accessible forms.15- Reviewing admin screens, settings pages, metaboxes, list tables, media frames, custom dialogs, AJAX UI, or frontend shortcode output.16- Adding labels/help text/errors for fields.17- Fixing color contrast, focus visibility, target sizes, font sizing, reduced motion, or hover-only interactions.18- Making live updates announce through `wp.a11y.speak()`.1920## Baseline Rules2122- Content and controls are usable with keyboard only.23- Every interactive element has a correct native role or ARIA role.24- Every form control has an accessible name.25- Focus is visible and not hidden behind sticky headers/toolbars.26- Text and UI contrast meet WCAG AA.27- Text can be zoomed to 200% without loss of content or functionality.28- Motion-heavy UI respects reduced-motion preferences.29- Errors are visible, understandable, and programmatically associated with fields.30- Dynamic updates are announced when they change user-relevant state.31- Automated tests are supplemented by keyboard and screen-reader checks.3233## ARIA Decision Rule3435Prefer native HTML first. ARIA patches semantics; it does not make broken interaction accessible by itself.3637Use this order:38391. Correct native element: `<button>`, `<a>`, `<label>`, `<input>`, `<select>`, `<textarea>`, `<fieldset>`, `<legend>`, `<dialog>`, `<nav>`, `<main>`.402. Visible text labels.413. `aria-labelledby` when visible text elsewhere labels the control.424. `aria-label` only when no visible label is possible, usually icon-only buttons or named landmarks.435. `aria-describedby` for help text, constraints, and errors. It is not the field's name.4445Do not add ARIA roles that duplicate or contradict native semantics.4647## Form Fields4849Every input/select/textarea needs a real accessible name.5051Good visible label:5253```php54<label for="myplugin_api_key"><?php esc_html_e( 'API key', 'textdomain' ); ?></label>55<input56 type="text"57 id="myplugin_api_key"58 name="myplugin_options[api_key]"59 value="<?php echo esc_attr( $api_key ); ?>"60 aria-describedby="myplugin_api_key_help"61>62<p id="myplugin_api_key_help" class="description">63 <?php esc_html_e( 'Create this key in your provider dashboard.', 'textdomain' ); ?>64</p>65```6667- Do not use placeholder text as the only label.68- Do not use `aria-label` when a visible `<label>` can exist.69- Keep `for` and `id` exactly paired.70- Use `aria-describedby` for help text and constraints.71- For required fields, use native `required` where validation supports it, plus visible required text where needed.72- For invalid fields, set `aria-invalid="true"` and connect the error with `aria-describedby`.73- Radio/checkbox groups need `<fieldset>` and `<legend>`.7475Error example:7677```php78<label for="myplugin_email"><?php esc_html_e( 'Notification email', 'textdomain' ); ?></label>79<input80 type="email"81 id="myplugin_email"82 name="myplugin_email"83 value="<?php echo esc_attr( $email ); ?>"84 aria-describedby="myplugin_email_help myplugin_email_error"85 aria-invalid="true"86>87<p id="myplugin_email_help" class="description"><?php esc_html_e( 'Used for failure alerts.', 'textdomain' ); ?></p>88<p id="myplugin_email_error" class="notice notice-error inline">89 <?php esc_html_e( 'Enter a valid email address.', 'textdomain' ); ?>90</p>91```9293## Buttons, Links, Icons9495- Use `<button type="button">` for actions that change UI state.96- Use `<a href="...">` for navigation.97- Never use `<a href="#">` or clickable `<div>` for buttons.98- Icon-only buttons need visible text, `.screen-reader-text`, or `aria-label`.99- Decorative icons inside named controls should use `aria-hidden="true"` and not receive focus.100- Repeated links like "Read more" need extra context through visible text or `.screen-reader-text`.101102WordPress 7.1 adds `wp_get_tooltip()` for a short control name and103`wp_get_toggletip()` for longer supporting context. Visible labels are still104preferred. Read [references/tooltips-71.md](references/tooltips-71.md) before105using the helpers or supplying custom trigger markup.106107Icon button:108109```php110<button type="button" class="button myplugin-refresh">111 <span class="dashicons dashicons-update" aria-hidden="true"></span>112 <span class="screen-reader-text"><?php esc_html_e( 'Refresh import status', 'textdomain' ); ?></span>113</button>114```115116## Keyboard and Focus117118- Tab reaches every interactive element in a logical order.119- Shift+Tab works backward.120- Enter activates links and submit buttons.121- Space activates buttons, checkboxes, and toggles.122- Escape closes modals, popovers, and autocomplete popups.123- Arrow-key behavior is implemented for custom menu/listbox/tab/slider patterns only when the ARIA APG pattern requires it.124125- Do not use positive `tabindex`.126- Use `tabindex="-1"` only for programmatic focus targets such as error summaries or modal containers.127- Never remove outlines without a visible replacement.128- After AJAX save/delete/filter operations, keep focus stable or move it deliberately to the next useful place.129- Do not trap focus except in true modal dialogs.130- When closing a modal, return focus to the control that opened it.131132Focus CSS:133134```css135.myplugin-ui :focus-visible {136 outline: 2px solid #1d2327;137 outline-offset: 2px;138}139140@media (forced-colors: active) {141 .myplugin-ui :focus-visible {142 outline: 2px solid CanvasText;143 }144}145```146147## Dynamic Updates and Notices148149For admin JavaScript that updates state without a full page load, enqueue `wp-a11y` and announce meaningful changes.150151```php152wp_enqueue_script(153 'myplugin-admin',154 plugins_url( 'assets/admin.js', __FILE__ ),155 array( 'wp-a11y', 'wp-i18n' ),156 '1.0.0',157 true158);159```160161```js162const { __ } = wp.i18n;163164wp.a11y.speak( __( 'Settings saved.', 'textdomain' ) );165```166167- Announce results, not implementation details.168- Use `polite` announcements for normal updates and `assertive` only for urgent errors.169- Visible notices still matter; screen-reader announcements do not replace visible feedback.170- Error summaries should be focusable with `tabindex="-1"` and focused after failed validation.171172## Color, Text, and Layout173174- Normal text contrast: at least 4.5:1.175- Large text contrast: at least 3:1.176- UI components and graphical state indicators: at least 3:1.177- Do not use color as the only way to show errors, selected state, required fields, or links in prose.178179- Use relative units for text: `rem`, `em`, `%`.180- As design guidance, start body text at `1rem`, avoid UI text below 14px, and181 prefer 16px for content/form-heavy screens. WCAG does not define a universal182 minimum font-size pass/fail threshold.183- Use line-height around `1.4` to `1.6` for readable body text.184- Avoid fixed-height containers for text that can wrap or zoom.185- Test text zoom at 200%, and test reflow at 400% zoom / a 320 CSS-pixel-wide186 viewport without two-dimensional scrolling except for allowed content such187 as data tables.188- Test WCAG text-spacing overrides: line height `1.5`, paragraph spacing `2em`,189 letter spacing `0.12em`, and word spacing `0.16em`; content and controls must190 remain available.191192Reduced motion:193194```css195@media (prefers-reduced-motion: reduce) {196 .myplugin-nonessential-animation {197 scroll-behavior: auto !important;198 animation: none !important;199 transition: none !important;200 }201}202```203204Scope reduced-motion changes to nonessential effects. Do not globally shorten205animations when application logic waits for `animationend`/`transitionend`;206provide a no-motion code path and test that completion still occurs.207208- WCAG 2.2 AA target size is 24 by 24 CSS pixels, with defined exceptions for209 sufficient spacing, inline text, equivalent larger controls, user-agent210 controls, and essential presentation. Audit the exception before reporting211 every smaller compact control as a failure.212- Prefer 44 by 44 CSS pixels for touch-heavy frontend UI.213- If the visual icon is smaller, increase clickable padding.214215## Landmarks, Headings, and Tables216217- Use one meaningful `h1` for the screen/admin page title.218- Keep headings in logical order; do not choose heading levels by visual size.219- Admin pages should use the normal `.wrap > h1` pattern.220- Use landmarks for major areas: `main`, `nav`, `aside`, `header`, `footer`.221- Multiple `nav` landmarks need names with `aria-label` or `aria-labelledby`.222- Data tables need header cells with `scope="col"` or `scope="row"`.223- On WordPress 7.1 list tables, the primary column—not the checkbox column—is224 the row header. Custom `WP_List_Table` renderers must preserve that structure.225- Do not use tables for layout.226227## Media and Images228229- Informative images need meaningful alt text.230- Decorative images use empty `alt=""`.231- Do not repeat adjacent text in alt text.232- SVG icons that are decorative use `aria-hidden="true" focusable="false"`.233- Audio/video must not autoplay with sound.234- Captions/transcripts are required when media conveys information.235236## Custom Components237238Before building a custom widget, check whether native HTML or a WordPress component already solves it.239240- Use the WAI-ARIA APG pattern for the component type.241- Implement the documented keyboard interaction.242- Manage focus deliberately.243- Keep ARIA state synchronized: `aria-expanded`, `aria-selected`, `aria-checked`, `aria-disabled`, `aria-controls`.244- Test with keyboard and at least one screen reader.245246- Modal dialogs.247- Autocomplete/listbox.248- Tabs.249- Accordions/disclosures.250- Drag-and-drop UIs.251- Toasts/live updates.252- Date pickers.253254## Audit Workflow2552561. Identify every interactive element.2572. Verify accessible name, role, value, and state.2583. Tab through the whole UI without a mouse.2594. Trigger validation errors and verify visible/focus/ARIA behavior.2605. Test 200% text zoom, 400%/320-CSS-pixel reflow, and text-spacing overrides.2616. Check contrast for text, focus, borders, icons, and error states.2627. Disable animations through reduced-motion preference.2638. Run an automated checker, then manually verify anything it cannot know.2649. Record issues with severity and WCAG/WordPress rationale.265266## Severity Guide267268- Critical: keyboard trap, unreachable primary action, missing accessible names on required controls, modal focus broken, security/checkout/account flow unusable.269- High: invalid fields not announced, focus invisible, insufficient contrast on important text/actions, destructive action ambiguity, dynamic state not announced.270- Medium: poor heading order, missing landmark names, weak help text271 association, target size below 24px without a WCAG exception, or272 nonessential motion not reduced.273- Low: redundant labels, minor screen-reader verbosity, cosmetic focus inconsistency that remains usable.274275## Common Mistakes276- Common failures: replacing visible labels with `aria-label`, using277 `aria-describedby` as the name, hiding labels with `display:none`, removing278 outlines, positive `tabindex`, click handlers on non-interactive elements,279 color-only state, silent AJAX completion, and trusting an automated scan as280 proof of accessibility.281282## Cross-References283284- Pair with `wp-admin-settings-api`, `wp-admin-list-table`, or285 `wp-admin-media-frame` for those specific admin components.286- Use `classic-theme-accessibility-semantics` for classic theme document structure and landmarks.287288## References289290- Official documentation: <https://developer.wordpress.org/coding-standards/wordpress-coding-standards/accessibility/>291- Official documentation: <https://www.w3.org/TR/WCAG22/>292- Official documentation: <https://www.w3.org/WAI/ARIA/apg/>293- Official documentation: <https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/>294- Official documentation: <https://www.section508.gov/develop/guide-accessible-web-design-development/>295- Verified source paths:296 - `wp-admin/css/common.css`297 - `wp-includes/js/dist/a11y.js`298 - `wp-includes/script-loader.php`299 - `wp-includes/comment-template.php`300 - `wp-includes/media-template.php`301 - `wp-includes/class-wp-customize-control.php`302 - `wp-admin/js/common.js`