| name | Web Accessibility |
| description | 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 applications accessible to all users following WCAG 2.1 AA/AAA standards. |
Web 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
<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>
<header>
<nav>
<a href="/">Home</a>
<a href="/shop">Shop</a>
</nav>
</header>
<main>
<article>...</article>
</main>
Landmarks
<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
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>
);
}
.product-card__link:focus,
.product-card__button:focus {
outline: 3px solid #B76E79;
outline-offset: 2px;
}
button:focus {
outline: none;
}
button:focus-visible {
outline: 3px solid #B76E79;
outline-offset: 2px;
}
Skip Links
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
<div role="button" tabindex="0" aria-label="Click me">Click</div>
<button>Click me</button>
ARIA Labels
<button
onClick={handleAddToCart}
aria-label="Add Diamond Ring to cart"
>
<PlusIcon aria-hidden="true" /> {}
Add to Cart
</button>
<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
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>
</>
);
}
.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
const accessibleColors = {
primaryDark: '#8B5663',
primary: '#B76E79',
minFontSize: '18px',
};
Never Rely on Color Alone
<span style={{ color: 'red' }}>Error</span>
<span style={{ color: '#EF4444' }}>
<ErrorIcon aria-hidden="true" />
<span>Error: Invalid email address</span>
</span>
Images & Media
Alt Text
<img
src="/jewelry/diamond-ring.jpg"
alt="18K rose gold engagement ring with 2-carat round diamond in cathedral setting"
/>
<img
src="/divider.svg"
alt="" // Empty alt for decorative
role="presentation"
/>
<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" />
{}
<track
kind="captions"
src="captions-en.vtt"
srcLang="en"
label="English"
default
/>
{}
<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 ? '' ''}
= ? '' }
/>
{errors.fullName && (
{errors.fullName}
)}
{/* Radio buttons */}
Shipping Method
Standard (5-7 days) - Free
Express (2-3 days) - $15
{/* Checkbox with description */}
Subscribe to newsletter
Receive updates about new collections and exclusive offers
Place Order
);
}
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) {
closeButtonRef.current?.focus();
document.body.style.overflow = 'hidden';
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleEscape);
return () => {
document.body.style.overflow = '';
document.removeEventListener('keydown', handleEscape);
};
}
}, [isOpen, onClose]);
(!isOpen) ;
(
);
}
WordPress Accessibility
Theme Support
function skyyrose_theme_support() {
add_theme_support('automatic-feed-links');
add_theme_support('title-tag');
add_theme_support('html5', [
'search-form',
'comment-form',
'comment-list',
'gallery',
'caption',
]);
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
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 ($); ?>"
></>
<!-- ' 3 -->
< ="-3">
<
="<? ($); ?>"
="<? ($); ?>"
/>
</>
</>
<?
}
}
Testing Accessibility
Automated Tools
npm install --save-dev @axe-core/cli
axe https://skyyrose.co --tags wcag2aa
lighthouse https://skyyrose.co --only-categories=accessibility
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
-
Zoom
- Zoom to 200% (browser zoom)
- Ensure no horizontal scroll
- Content remains readable
-
Motion Preferences
- Test with
prefers-reduced-motion
@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:
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.