Web Accessibility
This skill activates when users discuss WCAG compliance, screen readers, keyboard navigation, ARIA, semantic HTML, or accessibility issues. Provides guidance on making WordPress themes and web applica
SKILL.mdwordpress-copilot/skills/web-accessibility/SKILL.md
GitHubWeb Accessibility (A11y)
Build inclusive, accessible web experiences that work for everyone.
When This Activates
- "accessibility", "a11y", "WCAG", "screen reader"
- "keyboard navigation", "ARIA", "semantic HTML"
- "color contrast", "focus states", "alt text"
- User asks about making site accessible
- Discussing inclusive design
WCAG 2.1 Quick Reference
| Level | Requirement |
|---|---|
| A | Basic accessibility (minimum legal requirement) |
| AA | Standard for most websites (Target: SkyyRose) |
| AAA | Enhanced accessibility (ideal, not always possible) |
SkyyRose Target: WCAG 2.1 AA compliance
Semantic HTML
Use the Right Element
<!-- ❌ Bad: Divs for everything -->
<div class="header">
<div class="nav">
<div onclick="navigate()">Home</div>
<div onclick="navigate()">Shop</div>
</div>
</div>
<div class="main">
<div class="article">...</div>
</div>
<!-- ✅ Good: Semantic elements -->
<header>
<nav>
<a href="/">Home</a>
<a href="/shop">Shop</a>
</nav>
</header>
<main>
<article>...</article>
</main>
Landmarks
<!-- Screen readers use landmarks for navigation -->
<header role="banner">
<nav role="navigation" aria-label="Primary">...</nav>
</header>
<main role="main">
<article>...</article>
<aside role="complementary">...</aside>
</main>
<footer role="contentinfo">...</footer>
Keyboard Navigation
Focus Management
// All interactive elements must be keyboard accessible
function ProductCard({ product }: { product: Product }) {
return (
<article className="product-card">
<a
href={`/products/${product.id}`}
className="product-card__link"
// Focus styles in CSS
aria-label={`View ${product.name}`}
>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</a>
<button
onClick={() => addToCart(product.id)}
className="product-card__button"
aria-label={`Add ${product.name} to cart`}
>
Add to Cart
</button>
</article>
);
}
/* Clear, visible focus states */
.product-card__link:focus,
.product-card__button:focus {
outline: 3px solid #B76E79; /* SkyyRose brand color */
outline-offset: 2px;
}
/* Never remove outline without replacement */
button:focus {
outline: none; /* ❌ BAD - makes keyboard nav impossible */
}
button:focus-visible {
/* ✅ GOOD - visible focus for keyboard, hidden for mouse */
outline: 3px solid #B76E79;
outline-offset: 2px;
}
Skip Links
// Allow keyboard users to skip repetitive content
function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<a
href="#main-content"
className="skip-link"
// Position off-screen, visible on focus
>
Skip to main content
</a>
<Header />
<main id="main-content" tabIndex={-1}>
{children}
</main>
<Footer />
</>
);
}
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #B76E79;
color: white;
padding: 8px 16px;
text-decoration: none;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
ARIA (Accessible Rich Internet Applications)
When to Use ARIA
First Rule of ARIA: Don't use ARIA if you can use native HTML
<!-- ❌ Bad: Unnecessary ARIA -->
<div role="button" tabindex="0" aria-label="Click me">Click</div>
<!-- ✅ Good: Native button -->
<button>Click me</button>
ARIA Labels
// When visual label isn't enough
<button
onClick={handleAddToCart}
aria-label="Add Diamond Ring to cart" // Context for screen readers
>
<PlusIcon aria-hidden="true" /> {/* Hide decorative icons */}
Add to Cart
</button>
// Form inputs
<label htmlFor="email">Email Address</label>
<input
type="email"
id="email"
name="email"
aria-required="true"
aria-invalid={hasError}
aria-describedby={hasError ? 'email-error' : undefined}
/>
{hasError && <p id="email-error" role="alert">Please enter valid email</p>}
ARIA Live Regions
// Announce dynamic changes to screen readers
function Cart() {
const [items, setItems] = useState<CartItem[]>([]);
const [message, setMessage] = useState('');
const addItem = (item: CartItem) => {
setItems([...items, item]);
setMessage(`${item.name} added to cart`);
};
return (
<>
{/* Screen reader announcements */}
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only" // Visually hidden
>
{message}
</div>
<div aria-label="Shopping cart" role="region">
<h2>Cart ({items.length})</h2>
{items.map((item) => (
<CartItem key={item.id} item={item} />
))}
</div>
</>
);
}
/* Screen reader only class */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
Color & Contrast
WCAG AA Contrast Requirements
| Text Size | Ratio |
|---|---|
| Normal text (< 18px) | 4.5:1 |
| Large text (≥ 18px or ≥ 14px bold) | 3:1 |
| UI components (buttons, inputs) | 3:1 |
Check Contrast
// SkyyRose brand colors (#B76E79) on white background
// Contrast ratio: 3.8:1 (FAILS for normal text, PASSES for large text)
// Solutions:
const accessibleColors = {
// Option 1: Darker variant for small text
primaryDark: '#8B5663', // Contrast: 5.2:1 ✅
// Option 2: Use primary for large text only
primary: '#B76E79', // Use for headings (≥18px)
// Option 3: Ensure text is large enough
minFontSize: '18px', // When using #B76E79
};
Never Rely on Color Alone
// ❌ Bad: Color is the only indicator
<span style={{ color: 'red' }}>Error</span>
// ✅ Good: Icon + text + color
<span style={{ color: '#EF4444' }}>
<ErrorIcon aria-hidden="true" />
<span>Error: Invalid email address</span>
</span>
Images & Media
Alt Text
// Descriptive alt text
<img
src="/jewelry/diamond-ring.jpg"
alt="18K rose gold engagement ring with 2-carat round diamond in cathedral setting"
// NOT: alt="ring" or alt="image"
/>
// Decorative images
<img
src="/divider.svg"
alt="" // Empty alt for decorative
role="presentation"
/>
// Complex images (charts, diagrams)
<figure>
<img
src="/sales-chart.png"
alt="Bar chart showing sales growth"
aria-describedby="chart-description"
/>
<figcaption id="chart-description">
Sales increased from $10k in January to $50k in December,
with the largest jump in November ($40k to $50k).
</figcaption>
</figure>
Video Accessibility
<video
controls
aria-label="Product showcase video"
>
<source src="showcase.mp4" type="video/mp4" />
{/* Captions for deaf/hard of hearing */}
<track
kind="captions"
src="captions-en.vtt"
srcLang="en"
label="English"
default
/>
{/* Descriptions for blind users */}
<track
kind="descriptions"
src="descriptions-en.vtt"
srcLang="en"
label="English descriptions"
/>
Your browser doesn't support video.
</video>
Forms
Accessible Form Patterns
function CheckoutForm() {
return (
<form aria-labelledby="checkout-heading">
<h2 id="checkout-heading">Checkout</h2>
{/* Required field indicator */}
<p>
<span aria-hidden="true">*</span>
<span className="sr-only">Required field</span>
</p>
{/* Text input */}
<div className="form-field">
<label htmlFor="full-name">
Full Name <span aria-label="required">*</span>
</label>
<input
type="text"
id="full-name"
name="fullName"
aria-required="true"
aria-invalid={errors.fullName ? 'true' : 'false'}
aria-describedby={errors.fullName ? 'name-error' : undefined}
/>
{errors.fullName && (
<p id="name-error" role="alert" className="error">
{errors.fullName}
</p>
)}
</div>
{/* Radio buttons */}
<fieldset>
<legend>Shipping Method</legend>
<div>
<input
type="radio"
id="standard"
name="shipping"
value="standard"
/>
<label htmlFor="standard">Standard (5-7 days) - Free</label>
</div>
<div>
<input
type="radio"
id="express"
name="shipping"
value="express"
/>
<label htmlFor="express">Express (2-3 days) - $15</label>
</div>
</fieldset>
{/* Checkbox with description */}
<div>
<input
type="checkbox"
id="subscribe"
name="subscribe"
aria-describedby="subscribe-description"
/>
<label htmlFor="subscribe">Subscribe to newsletter</label>
<p id="subscribe-description" className="help-text">
Receive updates about new collections and exclusive offers
</p>
</div>
<button type="submit" aria-label="Complete purchase">
Place Order
</button>
</form>
);
}
Modal Dialogs
import { useEffect, useRef } from 'react';
function Modal({ isOpen, onClose, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) {
// Focus trap: Keep focus inside modal
closeButtonRef.current?.focus();
// Prevent body scroll
document.body.style.overflow = 'hidden';
// Handle Escape key
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleEscape);
return () => {
document.body.style.overflow = '';
document.removeEventListener('keydown', handleEscape);
};
}
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
ref={modalRef}
className="modal-overlay"
onClick={(e) => {
// Close on backdrop click
if (e.target === e.currentTarget) onClose();
}}
>
<div className="modal-content">
<button
ref={closeButtonRef}
onClick={onClose}
aria-label="Close modal"
className="modal-close"
>
×
</button>
<h2 id="modal-title">Modal Title</h2>
{children}
</div>
</div>
);
}
WordPress Accessibility
Theme Support
// functions.php
function skyyrose_theme_support() {
// Accessibility features
add_theme_support('automatic-feed-links');
add_theme_support('title-tag');
add_theme_support('html5', [
'search-form',
'comment-form',
'comment-list',
'gallery',
'caption',
]);
// Skip link
add_action('wp_body_open', function() {
echo '<a class="skip-link screen-reader-text" href="#primary">Skip to content</a>';
});
}
add_action('after_setup_theme', 'skyyrose_theme_support');
Navigation Menus
// Accessible navigation menu
wp_nav_menu([
'theme_location' => 'primary',
'container' => 'nav',
'container_aria_label' => 'Primary Navigation',
'menu_class' => 'primary-menu',
'fallback_cb' => false,
]);
Custom Elementor Widgets
class Skyy_Product_3D_Widget extends \Elementor\Widget_Base {
protected function render() {
?>
<div
class="skyy-product-3d"
role="region"
aria-label="3D Product Viewer"
>
<button
class="product-3d__rotate"
aria-label="Rotate product 360 degrees"
aria-pressed="false"
>
<span aria-hidden="true">↻</span>
Rotate
</button>
<canvas
id="product-canvas"
role="img"
aria-label="3D model of <?php echo esc_attr($product_name); ?>"
></canvas>
<!-- Fallback for users who can't see 3D -->
<div class="product-3d__fallback">
<img
src="<?php echo esc_url($product_image); ?>"
alt="<?php echo esc_attr($product_name); ?>"
/>
</div>
</div>
<?php
}
}
Testing Accessibility
Automated Tools
# axe-core (best automated testing)
npm install --save-dev @axe-core/cli
# Test page
axe https://skyyrose.co --tags wcag2aa
# Lighthouse (includes a11y audit)
lighthouse https://skyyrose.co --only-categories=accessibility
# Pa11y
npm install --save-dev pa11y
pa11y https://skyyrose.co --standard WCAG2AA
Manual Testing
Keyboard Navigation
- Tab through all interactive elements
- Ensure focus is visible
- Check modal/dropdown keyboard trapping
Screen Reader
- macOS: VoiceOver (Cmd+F5)
- Windows: NVDA (free) or JAWS
- Test landmarks, headings, alt text
Color Contrast
- Use browser DevTools contrast checker
- Or: https://webaim.org/resources/contrastchecker/
Zoom
- Zoom to 200% (browser zoom)
- Ensure no horizontal scroll
- Content remains readable
Motion Preferences
- Test with
prefers-reduced-motion
- Test with
/* Respect user motion preferences */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
SkyyRose Accessibility Checklist
For every page/component:
- Semantic HTML: Proper headings, landmarks, lists
- Keyboard: All interactive elements keyboard accessible
- Focus: Visible focus indicators (3px #B76E79 outline)
- Color: 4.5:1 contrast for text, 3:1 for UI components
- Alt Text: Descriptive alt for images, empty for decorative
- Forms: Labels, error messages, fieldsets
- ARIA: Only when needed, labels for icon buttons
- Motion: Respect
prefers-reduced-motion - Screen Reader: Test with VoiceOver/NVDA
- Zoom: Works at 200% zoom
Common Issues & Fixes
| Issue | Fix |
|---|---|
| Missing alt text | Add descriptive alt to all images |
| Poor contrast | Use darker colors or larger text |
| No focus indicators | Add visible :focus styles |
| Keyboard trap | Implement proper focus management in modals |
| Missing labels | Add labels to all form inputs |
| Non-semantic HTML | Use button/nav/main instead of divs |
| Icon-only buttons | Add aria-label |
| Auto-playing videos | Provide pause button, captions |
When User Asks About Accessibility
- Identify issues: Run automated scan (axe, Lighthouse)
- Prioritize: Fix critical issues first (keyboard, contrast)
- Implement: Use semantic HTML, ARIA sparingly
- Test manually: Keyboard, screen reader, zoom
- Document: Note accessibility features in README
Always aim for WCAG 2.1 AA compliance as baseline, AAA where possible.