WordPress child theme accessibility skill implementing WCAG 2.1 AA compliance patterns, screen reader support, keyboard navigation, focus management, and automated testing for WordPress themes, blocks, and plugin forms.
This skill provides definitive patterns for making WordPress child themes fully WCAG 2.1 AA compliant. Every code example targets child theme implementation without modifying parent themes or core plugins.
| Success Criterion | WordPress Implementation |
|---|---|
| 1.1.1 Non-text Content | the_post_thumbnail() with alt via attachment meta; empty alt="" for decorative images |
| 1.2.1 Audio/Video | Embed captions via [video] shortcode caption attribute or block tracks |
| 1.3.1 Info and Relationships | Semantic HTML5 in template parts: <header>, <nav>, <main>, |
<footer>| 1.3.2 Meaningful Sequence | DOM order matches visual order; avoid CSS-only reordering of content |
| 1.4.1 Use of Color | Never rely on color alone for status; pair with icons or text |
| 1.4.3 Contrast Minimum | 4.5:1 normal text, 3:1 large text (18px+ regular or 14px+ bold) |
| 1.4.11 Non-text Contrast | 3:1 for UI components and graphical objects (borders, icons, focus rings) |
| Success Criterion | WordPress Implementation |
|---|---|
| 2.1.1 Keyboard | All interactive elements reachable and operable via Tab/Enter/Space/Escape |
| 2.4.1 Bypass Blocks | Skip link as first focusable element in header.php |
| 2.4.2 Page Titled | wp_title() or document_title_parts filter for descriptive <title> |
| 2.4.3 Focus Order | Logical tab order following DOM; avoid positive tabindex values |
| 2.4.6 Headings and Labels | Heading hierarchy (h1 > h2 > h3) without skipping levels |
| 2.4.7 Focus Visible | Visible focus indicator on all interactive elements (2px+ outline) |
| Success Criterion | WordPress Implementation |
|---|---|
| 3.1.1 Language of Page | language_attributes() in <html> tag outputs lang attribute |
| 3.1.2 Language of Parts | lang attribute on inline foreign-language text |
| 3.2.1 On Focus | No context change on focus alone; menus open on click/Enter |
| 3.3.1 Error Identification | Form errors described in text, linked to field via aria-describedby |
| 3.3.2 Labels or Instructions | Every input has a visible <label> with matching for/id pair |
| Success Criterion | WordPress Implementation |
|---|---|
| 4.1.1 Parsing | Valid HTML output; run wp_kses_post() on user content |
| 4.1.2 Name, Role, Value | ARIA attributes on custom widgets; native HTML elements preferred |
| 4.1.3 Status Messages | aria-live="polite" regions for AJAX responses and notifications |
The skip link must be the very first focusable element in the DOM, before the site header.
<!-- child theme header.php -->
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<a class="skip-link screen-reader-text" href="#primary-content">
<?php esc_html_e( 'Skip to content', 'oshin_child' ); ?>
</a>
<header id="masthead" role="banner">
/* child theme style.css */
.skip-link {
position: absolute;
top: -100%;
left: 0;
z-index: 100000;
padding: 0.75rem 1.5rem;
background: #000;
color: #fff;
font-size: 1rem;
text-decoration: none;
}
.skip-link:focus {
top: 0;
outline: 3px solid #ffbf47;
outline-offset: 0;
}
<!-- header.php -->
<header id="masthead" role="banner">
<nav id="site-navigation" role="navigation" aria-label="<?php esc_attr_e( 'Primary Menu', 'oshin_child' ); ?>">
<?php wp_nav_menu( array( 'theme_location' => 'primary', 'container' => false ) ); ?>
</nav>
</header>
<!-- single.php / page.php -->
<main id="primary-content" role="main">
<?php the_content(); ?>
</main>
<!-- sidebar.php -->
<aside id="secondary" role="complementary" aria-label="<?php esc_attr_e( 'Sidebar', 'oshin_child' ); ?>">
<?php dynamic_sidebar( 'sidebar-1' ); ?>
</aside>
<!-- footer.php -->
<footer id="colophon" role="contentinfo">
// child theme functions.php
register_sidebar( array(
'name' => __( 'Footer Widgets', 'oshin_child' ),
'id' => 'footer-widgets',
'before_widget' => '<section id="%1$s" class="widget %2$s" role="region" aria-label="%1$s">',
'after_widget' => '</section>',
'before_title' => '<h2 class="widget-title">',
'after_title' => '</h2>',
) );
:root {
/* 4.5:1+ on white backgrounds */
--color-text-primary: #1a1a1a; /* 16.15:1 on #fff */
--color-text-secondary: #505050; /* 7.08:1 on #fff */
--color-text-muted: #6d6d6d; /* 4.83:1 on #fff */
/* Large text (3:1 minimum) */
--color-text-large-accent: #767676; /* 4.54:1 on #fff */
/* Interactive elements */
--color-link: #0055a4; /* 7.26:1 on #fff */
--color-link-hover: #003d75; /* 10.5:1 on #fff */
--color-focus-ring: #005fcc; /* 6.58:1 on #fff */
/* Backgrounds */
--color-bg-primary: #ffffff;
--color-bg-secondary: #f5f5f5;
}
body {
color: var(--color-text-primary);
background: var(--color-bg-primary);
}
a {
color: var(--color-link);
}