claude-plugin-wordpress-manager 1.4.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +7 -3
- package/CHANGELOG.md +111 -0
- package/README.md +10 -3
- package/agents/wp-accessibility-auditor.md +206 -0
- package/agents/wp-content-strategist.md +18 -0
- package/agents/wp-deployment-engineer.md +34 -2
- package/agents/wp-performance-optimizer.md +12 -0
- package/agents/wp-security-auditor.md +20 -0
- package/agents/wp-security-hardener.md +266 -0
- package/agents/wp-site-manager.md +14 -0
- package/agents/wp-test-engineer.md +207 -0
- package/docs/GUIDE.md +68 -15
- package/docs/guides/INDEX.md +46 -0
- package/docs/guides/wp-blog.md +590 -0
- package/docs/guides/wp-design-system.md +976 -0
- package/docs/guides/wp-ecommerce.md +786 -0
- package/docs/guides/wp-landing-page.md +762 -0
- package/docs/guides/wp-portfolio.md +713 -0
- package/docs/plans/2026-02-27-design-system-guide-design.md +30 -0
- package/docs/plans/2026-02-27-local-dev-tools-assessment.md +332 -0
- package/docs/plans/2026-02-27-local-env-design.md +179 -0
- package/docs/plans/2026-02-27-site-type-guides-design.md +44 -0
- package/package.json +7 -3
- package/skills/wordpress-router/SKILL.md +25 -5
- package/skills/wordpress-router/references/decision-tree.md +59 -3
- package/skills/wp-accessibility/SKILL.md +170 -0
- package/skills/wp-accessibility/references/a11y-audit-tools.md +248 -0
- package/skills/wp-accessibility/references/a11y-testing.md +222 -0
- package/skills/wp-accessibility/references/block-a11y.md +247 -0
- package/skills/wp-accessibility/references/interactive-a11y.md +272 -0
- package/skills/wp-accessibility/references/media-a11y.md +254 -0
- package/skills/wp-accessibility/references/theme-a11y.md +309 -0
- package/skills/wp-audit/SKILL.md +4 -0
- package/skills/wp-block-development/SKILL.md +5 -0
- package/skills/wp-block-themes/SKILL.md +4 -0
- package/skills/wp-deploy/SKILL.md +12 -0
- package/skills/wp-e2e-testing/SKILL.md +186 -0
- package/skills/wp-e2e-testing/references/ci-integration.md +174 -0
- package/skills/wp-e2e-testing/references/jest-wordpress.md +114 -0
- package/skills/wp-e2e-testing/references/phpunit-wordpress.md +141 -0
- package/skills/wp-e2e-testing/references/playwright-wordpress.md +108 -0
- package/skills/wp-e2e-testing/references/test-data-generation.md +127 -0
- package/skills/wp-e2e-testing/references/visual-regression.md +107 -0
- package/skills/wp-e2e-testing/references/wp-env-setup.md +97 -0
- package/skills/wp-e2e-testing/scripts/test_inspect.mjs +375 -0
- package/skills/wp-headless/SKILL.md +168 -0
- package/skills/wp-headless/references/api-layer-choice.md +160 -0
- package/skills/wp-headless/references/cors-config.md +245 -0
- package/skills/wp-headless/references/frontend-integration.md +331 -0
- package/skills/wp-headless/references/headless-auth.md +286 -0
- package/skills/wp-headless/references/webhooks.md +277 -0
- package/skills/wp-headless/references/wpgraphql.md +331 -0
- package/skills/wp-headless/scripts/headless_inspect.mjs +321 -0
- package/skills/wp-i18n/SKILL.md +170 -0
- package/skills/wp-i18n/references/js-i18n.md +201 -0
- package/skills/wp-i18n/references/multilingual-setup.md +219 -0
- package/skills/wp-i18n/references/php-i18n.md +196 -0
- package/skills/wp-i18n/references/rtl-support.md +206 -0
- package/skills/wp-i18n/references/translation-workflow.md +178 -0
- package/skills/wp-i18n/references/wpcli-i18n.md +177 -0
- package/skills/wp-i18n/scripts/i18n_inspect.mjs +330 -0
- package/skills/wp-interactivity-api/SKILL.md +4 -0
- package/skills/wp-local-env/SKILL.md +233 -0
- package/skills/wp-local-env/references/localwp-adapter.md +156 -0
- package/skills/wp-local-env/references/mcp-adapter-setup.md +153 -0
- package/skills/wp-local-env/references/studio-adapter.md +127 -0
- package/skills/wp-local-env/references/wpenv-adapter.md +121 -0
- package/skills/wp-local-env/scripts/detect_local_env.mjs +404 -0
- package/skills/wp-playground/SKILL.md +13 -1
- package/skills/wp-plugin-development/SKILL.md +6 -0
- package/skills/wp-rest-api/SKILL.md +4 -0
- package/skills/wp-security/SKILL.md +179 -0
- package/skills/wp-security/references/api-restriction.md +147 -0
- package/skills/wp-security/references/authentication-hardening.md +105 -0
- package/skills/wp-security/references/filesystem-hardening.md +105 -0
- package/skills/wp-security/references/http-headers.md +105 -0
- package/skills/wp-security/references/incident-response.md +144 -0
- package/skills/wp-security/references/user-capabilities.md +115 -0
- package/skills/wp-security/references/wp-config-security.md +129 -0
- package/skills/wp-security/scripts/security_inspect.mjs +393 -0
- package/skills/wp-wpcli-and-ops/SKILL.md +6 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Interactive Component Accessibility
|
|
2
|
+
|
|
3
|
+
Use this file when building accessible interactive components (modals, tabs, accordions, tooltips) in WordPress.
|
|
4
|
+
|
|
5
|
+
## Modal / Dialog
|
|
6
|
+
|
|
7
|
+
Based on [ARIA Authoring Practices Guide (APG) Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/).
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div role="dialog" aria-modal="true"
|
|
11
|
+
aria-labelledby="dialog-title"
|
|
12
|
+
aria-describedby="dialog-desc">
|
|
13
|
+
<h2 id="dialog-title">Confirm Action</h2>
|
|
14
|
+
<p id="dialog-desc">Are you sure you want to proceed?</p>
|
|
15
|
+
<button>Confirm</button>
|
|
16
|
+
<button>Cancel</button>
|
|
17
|
+
</div>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Keyboard behavior
|
|
21
|
+
|
|
22
|
+
| Key | Action |
|
|
23
|
+
|-----|--------|
|
|
24
|
+
| Tab | Move focus to next focusable element inside dialog |
|
|
25
|
+
| Shift+Tab | Move focus to previous focusable element |
|
|
26
|
+
| Escape | Close dialog |
|
|
27
|
+
|
|
28
|
+
### Focus management
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
class AccessibleModal {
|
|
32
|
+
open(trigger) {
|
|
33
|
+
this.trigger = trigger;
|
|
34
|
+
this.dialog.hidden = false;
|
|
35
|
+
this.dialog.setAttribute('aria-modal', 'true');
|
|
36
|
+
|
|
37
|
+
// Trap focus inside dialog
|
|
38
|
+
this.firstFocusable = this.dialog.querySelector(
|
|
39
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
40
|
+
);
|
|
41
|
+
this.firstFocusable?.focus();
|
|
42
|
+
|
|
43
|
+
// Prevent background scroll
|
|
44
|
+
document.body.style.overflow = 'hidden';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
close() {
|
|
48
|
+
this.dialog.hidden = true;
|
|
49
|
+
document.body.style.overflow = '';
|
|
50
|
+
|
|
51
|
+
// Return focus to trigger element
|
|
52
|
+
this.trigger?.focus();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
trapFocus(e) {
|
|
56
|
+
if (e.key !== 'Tab') return;
|
|
57
|
+
const focusable = this.dialog.querySelectorAll(
|
|
58
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
59
|
+
);
|
|
60
|
+
const first = focusable[0];
|
|
61
|
+
const last = focusable[focusable.length - 1];
|
|
62
|
+
|
|
63
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
last.focus();
|
|
66
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
first.focus();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Tabs
|
|
75
|
+
|
|
76
|
+
Based on [APG Tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<div class="tabs">
|
|
80
|
+
<div role="tablist" aria-label="Settings">
|
|
81
|
+
<button role="tab" id="tab-1" aria-selected="true"
|
|
82
|
+
aria-controls="panel-1" tabindex="0">
|
|
83
|
+
General
|
|
84
|
+
</button>
|
|
85
|
+
<button role="tab" id="tab-2" aria-selected="false"
|
|
86
|
+
aria-controls="panel-2" tabindex="-1">
|
|
87
|
+
Advanced
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0">
|
|
91
|
+
General settings content
|
|
92
|
+
</div>
|
|
93
|
+
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0"
|
|
94
|
+
hidden>
|
|
95
|
+
Advanced settings content
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Keyboard behavior
|
|
101
|
+
|
|
102
|
+
| Key | Action |
|
|
103
|
+
|-----|--------|
|
|
104
|
+
| Arrow Right | Activate next tab |
|
|
105
|
+
| Arrow Left | Activate previous tab |
|
|
106
|
+
| Home | Activate first tab |
|
|
107
|
+
| End | Activate last tab |
|
|
108
|
+
| Tab | Move focus into the active tab panel |
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
tablist.addEventListener('keydown', (e) => {
|
|
112
|
+
const tabs = [...tablist.querySelectorAll('[role="tab"]')];
|
|
113
|
+
const index = tabs.indexOf(document.activeElement);
|
|
114
|
+
|
|
115
|
+
let newIndex;
|
|
116
|
+
switch (e.key) {
|
|
117
|
+
case 'ArrowRight':
|
|
118
|
+
newIndex = (index + 1) % tabs.length;
|
|
119
|
+
break;
|
|
120
|
+
case 'ArrowLeft':
|
|
121
|
+
newIndex = (index - 1 + tabs.length) % tabs.length;
|
|
122
|
+
break;
|
|
123
|
+
case 'Home':
|
|
124
|
+
newIndex = 0;
|
|
125
|
+
break;
|
|
126
|
+
case 'End':
|
|
127
|
+
newIndex = tabs.length - 1;
|
|
128
|
+
break;
|
|
129
|
+
default:
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
activateTab(tabs[newIndex]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
function activateTab(tab) {
|
|
137
|
+
// Deactivate all
|
|
138
|
+
tablist.querySelectorAll('[role="tab"]').forEach((t) => {
|
|
139
|
+
t.setAttribute('aria-selected', 'false');
|
|
140
|
+
t.setAttribute('tabindex', '-1');
|
|
141
|
+
document.getElementById(t.getAttribute('aria-controls')).hidden = true;
|
|
142
|
+
});
|
|
143
|
+
// Activate selected
|
|
144
|
+
tab.setAttribute('aria-selected', 'true');
|
|
145
|
+
tab.setAttribute('tabindex', '0');
|
|
146
|
+
tab.focus();
|
|
147
|
+
document.getElementById(tab.getAttribute('aria-controls')).hidden = false;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Accordion
|
|
152
|
+
|
|
153
|
+
Based on [APG Accordion pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/).
|
|
154
|
+
|
|
155
|
+
```html
|
|
156
|
+
<div class="accordion">
|
|
157
|
+
<h3>
|
|
158
|
+
<button aria-expanded="true" aria-controls="sect1"
|
|
159
|
+
id="accordion1-header">
|
|
160
|
+
Section 1
|
|
161
|
+
</button>
|
|
162
|
+
</h3>
|
|
163
|
+
<div id="sect1" role="region" aria-labelledby="accordion1-header">
|
|
164
|
+
<p>Section 1 content</p>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<h3>
|
|
168
|
+
<button aria-expanded="false" aria-controls="sect2"
|
|
169
|
+
id="accordion2-header">
|
|
170
|
+
Section 2
|
|
171
|
+
</button>
|
|
172
|
+
</h3>
|
|
173
|
+
<div id="sect2" role="region" aria-labelledby="accordion2-header"
|
|
174
|
+
hidden>
|
|
175
|
+
<p>Section 2 content</p>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Keyboard behavior
|
|
181
|
+
|
|
182
|
+
| Key | Action |
|
|
183
|
+
|-----|--------|
|
|
184
|
+
| Enter / Space | Toggle section |
|
|
185
|
+
| Arrow Down | Next header |
|
|
186
|
+
| Arrow Up | Previous header |
|
|
187
|
+
| Home | First header |
|
|
188
|
+
| End | Last header |
|
|
189
|
+
|
|
190
|
+
## Tooltip
|
|
191
|
+
|
|
192
|
+
```html
|
|
193
|
+
<button aria-describedby="tooltip-1">
|
|
194
|
+
Settings
|
|
195
|
+
</button>
|
|
196
|
+
<div role="tooltip" id="tooltip-1" class="tooltip" hidden>
|
|
197
|
+
Configure application settings
|
|
198
|
+
</div>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Behavior
|
|
202
|
+
|
|
203
|
+
- Show on hover and focus
|
|
204
|
+
- Hide on Escape
|
|
205
|
+
- Do not use for essential information (tooltips are supplementary)
|
|
206
|
+
- Keep content brief (one sentence max)
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
const trigger = document.querySelector('[aria-describedby]');
|
|
210
|
+
const tooltip = document.getElementById('tooltip-1');
|
|
211
|
+
|
|
212
|
+
trigger.addEventListener('mouseenter', () => tooltip.hidden = false);
|
|
213
|
+
trigger.addEventListener('mouseleave', () => tooltip.hidden = true);
|
|
214
|
+
trigger.addEventListener('focus', () => tooltip.hidden = false);
|
|
215
|
+
trigger.addEventListener('blur', () => tooltip.hidden = true);
|
|
216
|
+
trigger.addEventListener('keydown', (e) => {
|
|
217
|
+
if (e.key === 'Escape') tooltip.hidden = true;
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Disclosure (show/hide)
|
|
222
|
+
|
|
223
|
+
```html
|
|
224
|
+
<button aria-expanded="false" aria-controls="details-content">
|
|
225
|
+
Show Details
|
|
226
|
+
</button>
|
|
227
|
+
<div id="details-content" hidden>
|
|
228
|
+
<p>Additional details here.</p>
|
|
229
|
+
</div>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
button.addEventListener('click', () => {
|
|
234
|
+
const expanded = button.getAttribute('aria-expanded') === 'true';
|
|
235
|
+
button.setAttribute('aria-expanded', String(!expanded));
|
|
236
|
+
content.hidden = expanded;
|
|
237
|
+
button.textContent = expanded ? 'Show Details' : 'Hide Details';
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## WordPress @wordpress/components
|
|
242
|
+
|
|
243
|
+
WordPress core components are pre-built with accessibility:
|
|
244
|
+
|
|
245
|
+
```js
|
|
246
|
+
import { Modal, TabPanel, Notice } from '@wordpress/components';
|
|
247
|
+
|
|
248
|
+
// Modal — handles focus trap, Escape, aria-modal automatically
|
|
249
|
+
<Modal title="Settings" onRequestClose={closeModal}>
|
|
250
|
+
<p>Modal content</p>
|
|
251
|
+
</Modal>
|
|
252
|
+
|
|
253
|
+
// TabPanel — handles arrow keys, aria-selected automatically
|
|
254
|
+
<TabPanel
|
|
255
|
+
tabs={[
|
|
256
|
+
{ name: 'general', title: 'General' },
|
|
257
|
+
{ name: 'advanced', title: 'Advanced' },
|
|
258
|
+
]}
|
|
259
|
+
>
|
|
260
|
+
{(tab) => <p>{tab.name} content</p>}
|
|
261
|
+
</TabPanel>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Prefer `@wordpress/components` over custom implementations when building for the block editor.
|
|
265
|
+
|
|
266
|
+
## Verification
|
|
267
|
+
|
|
268
|
+
1. Keyboard-only navigation: can you operate the component without a mouse?
|
|
269
|
+
2. Screen reader: does it announce state changes (expanded/collapsed, selected tab)?
|
|
270
|
+
3. Focus management: does focus move to the right place on open/close?
|
|
271
|
+
4. Escape key: does it close overlays and tooltips?
|
|
272
|
+
5. No focus traps: can you escape from every component?
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Media Accessibility
|
|
2
|
+
|
|
3
|
+
Use this file when making images, video, audio, and embedded media accessible in WordPress.
|
|
4
|
+
|
|
5
|
+
## Images
|
|
6
|
+
|
|
7
|
+
### Alt text decision tree
|
|
8
|
+
|
|
9
|
+
1. **Is the image decorative?** → `alt=""`
|
|
10
|
+
2. **Does it contain text?** → alt = the text in the image
|
|
11
|
+
3. **Is it a link/button?** → alt = the link destination or action
|
|
12
|
+
4. **Does it convey information?** → alt = describe the information
|
|
13
|
+
5. **Is it complex (chart/graph)?** → alt = brief summary + detailed description nearby
|
|
14
|
+
|
|
15
|
+
### WordPress implementation
|
|
16
|
+
|
|
17
|
+
```php
|
|
18
|
+
// Featured image with alt text
|
|
19
|
+
if (has_post_thumbnail()) {
|
|
20
|
+
$alt = get_post_meta(
|
|
21
|
+
get_post_thumbnail_id(), '_wp_attachment_image_alt', true
|
|
22
|
+
);
|
|
23
|
+
the_post_thumbnail('large', ['alt' => $alt ?: get_the_title()]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Custom image output
|
|
27
|
+
$image_id = get_field('hero_image'); // ACF example
|
|
28
|
+
$alt = get_post_meta($image_id, '_wp_attachment_image_alt', true);
|
|
29
|
+
echo wp_get_attachment_image($image_id, 'full', false, [
|
|
30
|
+
'alt' => $alt,
|
|
31
|
+
'loading' => 'lazy',
|
|
32
|
+
]);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Decorative images
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<!-- Decorative: empty alt, presentational role -->
|
|
39
|
+
<img src="divider.svg" alt="" role="presentation">
|
|
40
|
+
|
|
41
|
+
<!-- CSS background for purely decorative -->
|
|
42
|
+
<div class="decorative-bg" aria-hidden="true"></div>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Complex images
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<figure>
|
|
49
|
+
<img src="chart.png"
|
|
50
|
+
alt="Q4 revenue chart showing 25% growth"
|
|
51
|
+
aria-describedby="chart-details">
|
|
52
|
+
<figcaption id="chart-details">
|
|
53
|
+
Revenue grew from $2M in Q3 to $2.5M in Q4, driven primarily
|
|
54
|
+
by the European market which accounted for 60% of new sales.
|
|
55
|
+
</figcaption>
|
|
56
|
+
</figure>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### SVG accessibility
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<!-- Informative SVG -->
|
|
63
|
+
<svg role="img" aria-labelledby="svg-title svg-desc">
|
|
64
|
+
<title id="svg-title">Company Logo</title>
|
|
65
|
+
<desc id="svg-desc">Green shield with leaf motif</desc>
|
|
66
|
+
<!-- svg paths -->
|
|
67
|
+
</svg>
|
|
68
|
+
|
|
69
|
+
<!-- Decorative SVG -->
|
|
70
|
+
<svg aria-hidden="true" focusable="false">
|
|
71
|
+
<!-- svg paths -->
|
|
72
|
+
</svg>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Video
|
|
76
|
+
|
|
77
|
+
### WordPress video block
|
|
78
|
+
|
|
79
|
+
The core Video block supports:
|
|
80
|
+
- Captions (`.vtt` files)
|
|
81
|
+
- Poster image (alt text via the poster)
|
|
82
|
+
- Playback controls (native browser controls are accessible)
|
|
83
|
+
|
|
84
|
+
### Captions and subtitles
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<video controls>
|
|
88
|
+
<source src="video.mp4" type="video/mp4">
|
|
89
|
+
<track kind="captions" src="captions-en.vtt"
|
|
90
|
+
srclang="en" label="English" default>
|
|
91
|
+
<track kind="captions" src="captions-it.vtt"
|
|
92
|
+
srclang="it" label="Italiano">
|
|
93
|
+
<track kind="descriptions" src="descriptions-en.vtt"
|
|
94
|
+
srclang="en" label="English audio descriptions">
|
|
95
|
+
</video>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### WebVTT format
|
|
99
|
+
|
|
100
|
+
```vtt
|
|
101
|
+
WEBVTT
|
|
102
|
+
|
|
103
|
+
00:00:01.000 --> 00:00:04.000
|
|
104
|
+
Welcome to our product demonstration.
|
|
105
|
+
|
|
106
|
+
00:00:04.500 --> 00:00:08.000
|
|
107
|
+
Today we'll show you the key features
|
|
108
|
+
of our new application.
|
|
109
|
+
|
|
110
|
+
00:00:08.500 --> 00:00:12.000
|
|
111
|
+
[Music playing in background]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### WCAG video requirements
|
|
115
|
+
|
|
116
|
+
| Level | Requirement |
|
|
117
|
+
|-------|------------|
|
|
118
|
+
| A | Captions for prerecorded video |
|
|
119
|
+
| A | Audio description or media alternative for prerecorded video |
|
|
120
|
+
| AA | Captions for live video |
|
|
121
|
+
| AA | Audio descriptions for prerecorded video |
|
|
122
|
+
| AAA | Sign language interpretation |
|
|
123
|
+
| AAA | Extended audio descriptions |
|
|
124
|
+
|
|
125
|
+
### Autoplay restrictions
|
|
126
|
+
|
|
127
|
+
```html
|
|
128
|
+
<!-- NEVER autoplay with sound -->
|
|
129
|
+
<video autoplay muted playsinline>
|
|
130
|
+
<!-- Muted autoplay is acceptable for decorative/background video -->
|
|
131
|
+
</video>
|
|
132
|
+
|
|
133
|
+
<!-- Provide pause control for autoplay -->
|
|
134
|
+
<div class="video-wrapper">
|
|
135
|
+
<video autoplay muted loop id="bg-video">
|
|
136
|
+
<source src="bg.mp4" type="video/mp4">
|
|
137
|
+
</video>
|
|
138
|
+
<button aria-label="Pause background video"
|
|
139
|
+
onclick="toggleVideo()">
|
|
140
|
+
Pause
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Audio
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<audio controls>
|
|
149
|
+
<source src="podcast.mp3" type="audio/mpeg">
|
|
150
|
+
<a href="podcast.mp3">Download podcast episode</a>
|
|
151
|
+
</audio>
|
|
152
|
+
|
|
153
|
+
<!-- Provide transcript for audio content -->
|
|
154
|
+
<details>
|
|
155
|
+
<summary>Read transcript</summary>
|
|
156
|
+
<div class="transcript">
|
|
157
|
+
<p><strong>Host:</strong> Welcome to the show...</p>
|
|
158
|
+
</div>
|
|
159
|
+
</details>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### WCAG audio requirements
|
|
163
|
+
|
|
164
|
+
| Level | Requirement |
|
|
165
|
+
|-------|------------|
|
|
166
|
+
| A | Transcript for prerecorded audio-only content |
|
|
167
|
+
| AAA | Sign language for prerecorded audio in video |
|
|
168
|
+
|
|
169
|
+
## Embedded media (iframes)
|
|
170
|
+
|
|
171
|
+
```html
|
|
172
|
+
<!-- YouTube/Vimeo embeds need titles -->
|
|
173
|
+
<iframe src="https://www.youtube.com/embed/VIDEO_ID"
|
|
174
|
+
title="Product demonstration video"
|
|
175
|
+
allowfullscreen>
|
|
176
|
+
</iframe>
|
|
177
|
+
|
|
178
|
+
<!-- Maps -->
|
|
179
|
+
<iframe src="https://www.google.com/maps/embed?..."
|
|
180
|
+
title="Store location map - 123 Main Street, Rome"
|
|
181
|
+
allowfullscreen>
|
|
182
|
+
</iframe>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### WordPress oEmbed
|
|
186
|
+
|
|
187
|
+
WordPress auto-embeds URLs. Add accessible wrappers:
|
|
188
|
+
|
|
189
|
+
```php
|
|
190
|
+
add_filter('embed_oembed_html', function($html, $url, $attr) {
|
|
191
|
+
// Add responsive wrapper with accessible title
|
|
192
|
+
return sprintf(
|
|
193
|
+
'<div class="responsive-embed" role="group" aria-label="%s">%s</div>',
|
|
194
|
+
esc_attr__('Embedded media', 'my-theme'),
|
|
195
|
+
$html
|
|
196
|
+
);
|
|
197
|
+
}, 10, 3);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Animations and motion
|
|
201
|
+
|
|
202
|
+
```css
|
|
203
|
+
/* Respect prefers-reduced-motion */
|
|
204
|
+
@media (prefers-reduced-motion: reduce) {
|
|
205
|
+
*,
|
|
206
|
+
*::before,
|
|
207
|
+
*::after {
|
|
208
|
+
animation-duration: 0.01ms !important;
|
|
209
|
+
animation-iteration-count: 1 !important;
|
|
210
|
+
transition-duration: 0.01ms !important;
|
|
211
|
+
scroll-behavior: auto !important;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
video, .animated-element {
|
|
215
|
+
animation: none !important;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
// Check user preference in JavaScript
|
|
222
|
+
const prefersReducedMotion = window.matchMedia(
|
|
223
|
+
'(prefers-reduced-motion: reduce)'
|
|
224
|
+
).matches;
|
|
225
|
+
|
|
226
|
+
if (!prefersReducedMotion) {
|
|
227
|
+
// Run animations
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
WCAG requirements:
|
|
232
|
+
- Content that moves, blinks, or scrolls for more than 5 seconds must have a pause mechanism
|
|
233
|
+
- No content flashes more than 3 times per second
|
|
234
|
+
|
|
235
|
+
## Verification
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# Check for images without alt text
|
|
239
|
+
curl -s https://site.com/ | grep -oP '<img[^>]*>' | grep -v 'alt='
|
|
240
|
+
|
|
241
|
+
# Check for iframes without titles
|
|
242
|
+
curl -s https://site.com/ | grep -oP '<iframe[^>]*>' | grep -v 'title='
|
|
243
|
+
|
|
244
|
+
# Check for video without captions
|
|
245
|
+
curl -s https://site.com/ | grep -oP '<video[^>]*>.*?</video>' | grep -v '<track'
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Manual checks:
|
|
249
|
+
1. All informative images have descriptive alt text
|
|
250
|
+
2. Decorative images have `alt=""`
|
|
251
|
+
3. Videos have captions
|
|
252
|
+
4. Audio has transcripts
|
|
253
|
+
5. No autoplay media with sound
|
|
254
|
+
6. Animations respect `prefers-reduced-motion`
|