@symbo.ls/mcp 1.0.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/.env.example +16 -0
- package/.env.railway +13 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/mcp.json +57 -0
- package/package.json +20 -0
- package/pyproject.toml +25 -0
- package/railway.toml +26 -0
- package/run.sh +17 -0
- package/symbols_mcp/__init__.py +1 -0
- package/symbols_mcp/server.py +1114 -0
- package/symbols_mcp/skills/ACCESSIBILITY.md +471 -0
- package/symbols_mcp/skills/ACCESSIBILITY_AUDITORY.md +70 -0
- package/symbols_mcp/skills/AGENT_INSTRUCTIONS.md +257 -0
- package/symbols_mcp/skills/BRAND_INDENTITY.md +69 -0
- package/symbols_mcp/skills/BUILT_IN_COMPONENTS.md +304 -0
- package/symbols_mcp/skills/CLAUDE.md +2158 -0
- package/symbols_mcp/skills/CLI_QUICK_START.md +205 -0
- package/symbols_mcp/skills/DESIGN_CRITIQUE.md +64 -0
- package/symbols_mcp/skills/DESIGN_DIRECTION.md +320 -0
- package/symbols_mcp/skills/DESIGN_SYSTEM_ARCHITECT.md +64 -0
- package/symbols_mcp/skills/DESIGN_SYSTEM_CONFIG.md +487 -0
- package/symbols_mcp/skills/DESIGN_SYSTEM_IN_PROPS.md +136 -0
- package/symbols_mcp/skills/DESIGN_TO_CODE.md +64 -0
- package/symbols_mcp/skills/DESIGN_TREND.md +50 -0
- package/symbols_mcp/skills/DOMQL_v2-v3_MIGRATION.md +236 -0
- package/symbols_mcp/skills/FIGMA_MATCHING.md +63 -0
- package/symbols_mcp/skills/GARY_TAN.md +80 -0
- package/symbols_mcp/skills/MARKETING_ASSETS.md +66 -0
- package/symbols_mcp/skills/MIGRATE_TO_SYMBOLS.md +614 -0
- package/symbols_mcp/skills/QUICKSTART.md +79 -0
- package/symbols_mcp/skills/SYMBOLS_LOCAL_INSTRUCTIONS.md +1405 -0
- package/symbols_mcp/skills/THE_PRESENTATION.md +69 -0
- package/symbols_mcp/skills/UI_UX_PATTERNS.md +68 -0
- package/windsurf-mcp-config.json +18 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
# Accessibility in Symbols
|
|
2
|
+
|
|
3
|
+
Guide for building accessible components using Symbols v3 syntax. Every component should be usable by keyboard, screen readers, and assistive technologies.
|
|
4
|
+
|
|
5
|
+
## Use Semantic Atoms
|
|
6
|
+
|
|
7
|
+
Always prefer built-in atom components over generic `Box` or `Div`. Atoms map to proper HTML elements with built-in accessibility semantics.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
// CORRECT — semantic atoms
|
|
11
|
+
{ Button: { text: 'Submit' } } // <button>
|
|
12
|
+
{ Link: { text: 'Dashboard', href: '/dashboard' } } // <a>
|
|
13
|
+
{ Input: { placeholder: 'Search...' } } // <input>
|
|
14
|
+
{ Nav: { Link: { text: 'Home', href: '/' } } } // <nav>
|
|
15
|
+
{ Header: {} } // <header>
|
|
16
|
+
{ Footer: {} } // <footer>
|
|
17
|
+
{ Main: {} } // <main>
|
|
18
|
+
{ Section: {} } // <section>
|
|
19
|
+
|
|
20
|
+
// WRONG — loses semantics
|
|
21
|
+
{ Box: { tag: 'div', text: 'Submit', onClick: fn } } // div is not a button
|
|
22
|
+
{ Text: { text: 'Click here', onClick: fn } } // span is not interactive
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## ARIA Attributes
|
|
26
|
+
|
|
27
|
+
Use the `attr` property to add ARIA attributes to any component.
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
// Landmark roles
|
|
31
|
+
{ Box: { attr: { role: 'alert' }, text: 'Error occurred' } }
|
|
32
|
+
{ Box: { attr: { role: 'status', 'aria-live': 'polite' }, text: '3 results found' } }
|
|
33
|
+
|
|
34
|
+
// Labels and descriptions
|
|
35
|
+
{ Input: { attr: { 'aria-label': 'Search networks' }, placeholder: 'Search...' } }
|
|
36
|
+
{ Button: { icon: 'x', attr: { 'aria-label': 'Close dialog' } } }
|
|
37
|
+
{ Icon: { name: 'settings', attr: { 'aria-hidden': 'true' } } }
|
|
38
|
+
|
|
39
|
+
// State attributes
|
|
40
|
+
{
|
|
41
|
+
Button: {
|
|
42
|
+
attr: (el, s) => ({
|
|
43
|
+
'aria-expanded': s.isOpen,
|
|
44
|
+
'aria-controls': 'dropdown-menu'
|
|
45
|
+
}),
|
|
46
|
+
onClick: (e, el, s) => s.update({ isOpen: !s.isOpen })
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Dynamic live regions
|
|
51
|
+
{
|
|
52
|
+
Box: {
|
|
53
|
+
attr: { role: 'status', 'aria-live': 'polite', 'aria-atomic': 'true' },
|
|
54
|
+
text: (el, s) => `${s.count} items loaded`
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Keyboard Navigation
|
|
60
|
+
|
|
61
|
+
All interactive components must be keyboard-operable. Use `onKeydown` for custom keyboard handling.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
// Custom keyboard interaction
|
|
65
|
+
{
|
|
66
|
+
extends: 'Flex',
|
|
67
|
+
attr: { role: 'listbox', tabindex: '0', 'aria-label': 'Select option' },
|
|
68
|
+
onKeydown: (e, el, s) => {
|
|
69
|
+
if (e.key === 'ArrowDown') {
|
|
70
|
+
e.preventDefault()
|
|
71
|
+
s.update({ activeIndex: Math.min(s.activeIndex + 1, s.items.length - 1) })
|
|
72
|
+
}
|
|
73
|
+
if (e.key === 'ArrowUp') {
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
s.update({ activeIndex: Math.max(s.activeIndex - 1, 0) })
|
|
76
|
+
}
|
|
77
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
78
|
+
e.preventDefault()
|
|
79
|
+
el.call('selectItem', s.items[s.activeIndex])
|
|
80
|
+
}
|
|
81
|
+
if (e.key === 'Escape') {
|
|
82
|
+
el.call('closeDropdown')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Tabindex rules
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
// Focusable (in tab order)
|
|
92
|
+
{ Box: { attr: { tabindex: '0', role: 'button' } } }
|
|
93
|
+
|
|
94
|
+
// Focusable programmatically only (not in tab order)
|
|
95
|
+
{ Box: { attr: { tabindex: '-1' } } }
|
|
96
|
+
|
|
97
|
+
// Never use tabindex > 0
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Focus Styles
|
|
101
|
+
|
|
102
|
+
Always provide visible focus indicators. Use `:focus-visible` to show focus only for keyboard users.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
{
|
|
106
|
+
Button: {
|
|
107
|
+
theme: 'primary',
|
|
108
|
+
':focus-visible': {
|
|
109
|
+
outline: 'solid, Y, blue .3',
|
|
110
|
+
outlineOffset: '2px'
|
|
111
|
+
},
|
|
112
|
+
// Remove default outline only when replacing with custom
|
|
113
|
+
':focus': { outline: 'none' },
|
|
114
|
+
':focus-visible': { boxShadow: '0 0 0 3px rgba(66, 153, 225, 0.5)' }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Focus management in modals and dropdowns
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
// Trap focus in modal
|
|
123
|
+
{
|
|
124
|
+
attr: { role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Add network' },
|
|
125
|
+
onRender: (el) => {
|
|
126
|
+
const focusable = el.node.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
|
127
|
+
if (focusable.length) focusable[0].focus()
|
|
128
|
+
},
|
|
129
|
+
onKeydown: (e, el) => {
|
|
130
|
+
if (e.key === 'Tab') {
|
|
131
|
+
const focusable = el.node.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
|
132
|
+
const first = focusable[0]
|
|
133
|
+
const last = focusable[focusable.length - 1]
|
|
134
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
135
|
+
e.preventDefault()
|
|
136
|
+
last.focus()
|
|
137
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
138
|
+
e.preventDefault()
|
|
139
|
+
first.focus()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (e.key === 'Escape') {
|
|
143
|
+
el.state.root.update({ modal: null })
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Accessible Forms
|
|
150
|
+
|
|
151
|
+
### Labels
|
|
152
|
+
|
|
153
|
+
Every form input needs a visible label or `aria-label`.
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
// Visible label using HTML association
|
|
157
|
+
{
|
|
158
|
+
Label: { text: 'Email', attr: { for: 'email-input' } },
|
|
159
|
+
Input: { id: 'email-input', type: 'email', attr: { required: 'true' } }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Hidden label for icon-only inputs
|
|
163
|
+
{
|
|
164
|
+
Input: {
|
|
165
|
+
type: 'search',
|
|
166
|
+
placeholder: 'Search...',
|
|
167
|
+
attr: { 'aria-label': 'Search networks' }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Described by helper text
|
|
172
|
+
{
|
|
173
|
+
Input: {
|
|
174
|
+
id: 'password',
|
|
175
|
+
type: 'password',
|
|
176
|
+
attr: { 'aria-describedby': 'password-hint' }
|
|
177
|
+
},
|
|
178
|
+
P: { id: 'password-hint', text: 'Must be at least 8 characters', fontSize: 'Z1' }
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Error states
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
{
|
|
186
|
+
Input: {
|
|
187
|
+
attr: (el, s) => ({
|
|
188
|
+
'aria-invalid': s.hasError ? 'true' : undefined,
|
|
189
|
+
'aria-describedby': s.hasError ? 'error-msg' : undefined
|
|
190
|
+
}),
|
|
191
|
+
border: (el, s) => s.hasError ? 'red 1px solid' : 'gray 1px solid'
|
|
192
|
+
},
|
|
193
|
+
P: {
|
|
194
|
+
id: 'error-msg',
|
|
195
|
+
attr: { role: 'alert' },
|
|
196
|
+
color: 'red',
|
|
197
|
+
text: (el, s) => s.errorMessage,
|
|
198
|
+
hide: (el, s) => !s.hasError
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Required fields
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
{
|
|
207
|
+
Label: { text: 'Name', Span: { text: ' *', color: 'red', attr: { 'aria-hidden': 'true' } } },
|
|
208
|
+
Input: { attr: { required: 'true', 'aria-required': 'true' } }
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Images and Icons
|
|
213
|
+
|
|
214
|
+
### Meaningful images need alt text
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
// Informative image
|
|
218
|
+
{ Img: { src: 'chart.png', alt: 'Network uptime over last 30 days' } }
|
|
219
|
+
|
|
220
|
+
// Decorative image — hide from assistive tech
|
|
221
|
+
{ Img: { src: 'decoration.png', alt: '', attr: { 'aria-hidden': 'true' } } }
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Icons
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
// Decorative icon next to text — hide icon
|
|
228
|
+
{
|
|
229
|
+
Button: {
|
|
230
|
+
Icon: { name: 'search', attr: { 'aria-hidden': 'true' } },
|
|
231
|
+
text: 'Search'
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Icon-only button — needs label
|
|
236
|
+
{
|
|
237
|
+
Button: {
|
|
238
|
+
icon: 'x',
|
|
239
|
+
attr: { 'aria-label': 'Close' }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Icon conveying status — needs accessible text
|
|
244
|
+
{
|
|
245
|
+
Flex: {
|
|
246
|
+
Icon: { name: 'check', color: 'green', attr: { 'aria-hidden': 'true' } },
|
|
247
|
+
Text: { text: 'Connected' }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Color and Contrast
|
|
253
|
+
|
|
254
|
+
### Do not rely on color alone
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
// WRONG — only color indicates error
|
|
258
|
+
{ Input: { border: 'red 1px solid' } }
|
|
259
|
+
|
|
260
|
+
// CORRECT — color + icon + text
|
|
261
|
+
{
|
|
262
|
+
Input: { border: 'red 1px solid', attr: { 'aria-invalid': 'true', 'aria-describedby': 'err' } },
|
|
263
|
+
Flex: {
|
|
264
|
+
Icon: { name: 'info', color: 'red', attr: { 'aria-hidden': 'true' } },
|
|
265
|
+
P: { id: 'err', text: 'This field is required', color: 'red', attr: { role: 'alert' } }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Contrast with theme tokens
|
|
271
|
+
|
|
272
|
+
Use design system color tokens that ensure contrast. When using opacity modifiers, verify contrast remains sufficient.
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
// Good contrast
|
|
276
|
+
{ color: 'title', background: 'white' }
|
|
277
|
+
|
|
278
|
+
// Risky — low opacity can fail contrast
|
|
279
|
+
{ color: 'gray 0.3', background: 'white' } // Likely fails WCAG AA
|
|
280
|
+
|
|
281
|
+
// Use semantic color tokens that account for light/dark
|
|
282
|
+
{ color: 'paragraph', background: 'document' }
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Accessible Lists and Tables
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
// Accessible list
|
|
289
|
+
{
|
|
290
|
+
Ul: {
|
|
291
|
+
attr: { 'aria-label': 'Network list' },
|
|
292
|
+
children: (el, s) => s.networks,
|
|
293
|
+
childrenAs: 'state',
|
|
294
|
+
childExtends: 'Li',
|
|
295
|
+
childProps: {
|
|
296
|
+
text: (el, s) => s.name
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Table with headers
|
|
302
|
+
{
|
|
303
|
+
tag: 'table',
|
|
304
|
+
attr: { 'aria-label': 'Validator data' },
|
|
305
|
+
Thead: {
|
|
306
|
+
tag: 'thead',
|
|
307
|
+
Tr: {
|
|
308
|
+
tag: 'tr',
|
|
309
|
+
Th_name: { tag: 'th', text: 'Name', attr: { scope: 'col' } },
|
|
310
|
+
Th_status: { tag: 'th', text: 'Status', attr: { scope: 'col' } }
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Loading and Dynamic Content
|
|
317
|
+
|
|
318
|
+
```js
|
|
319
|
+
// Loading state announcement
|
|
320
|
+
{
|
|
321
|
+
Box: {
|
|
322
|
+
attr: (el, s) => ({
|
|
323
|
+
role: 'status',
|
|
324
|
+
'aria-live': 'polite',
|
|
325
|
+
'aria-busy': s.loading ? 'true' : 'false'
|
|
326
|
+
}),
|
|
327
|
+
text: (el, s) => s.loading ? 'Loading...' : `${s.items.length} items loaded`
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Skeleton/loading indicator
|
|
332
|
+
{
|
|
333
|
+
Box: {
|
|
334
|
+
attr: { role: 'status', 'aria-label': 'Loading content' },
|
|
335
|
+
hide: (el, s) => !s.loading
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Skip Navigation
|
|
341
|
+
|
|
342
|
+
Add a skip link as the first focusable element in layouts.
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
export const Layout = {
|
|
346
|
+
SkipLink: {
|
|
347
|
+
extends: "Link",
|
|
348
|
+
text: "Skip to main content",
|
|
349
|
+
href: "#main-content",
|
|
350
|
+
position: "absolute",
|
|
351
|
+
top: "-F",
|
|
352
|
+
left: "A",
|
|
353
|
+
background: "white",
|
|
354
|
+
color: "blue",
|
|
355
|
+
padding: "Z A",
|
|
356
|
+
zIndex: 100,
|
|
357
|
+
":focus": { top: "A" },
|
|
358
|
+
},
|
|
359
|
+
Header: {},
|
|
360
|
+
Main: { id: "main-content", attr: { tabindex: "-1" } },
|
|
361
|
+
Footer: {},
|
|
362
|
+
};
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Accessible Dropdown
|
|
366
|
+
|
|
367
|
+
```js
|
|
368
|
+
export const AccessibleDropdown = {
|
|
369
|
+
extends: "DropdownParent",
|
|
370
|
+
state: { isOpen: false, activeIndex: -1 },
|
|
371
|
+
|
|
372
|
+
Button: {
|
|
373
|
+
text: "Options",
|
|
374
|
+
attr: (el, s) => ({
|
|
375
|
+
"aria-haspopup": "listbox",
|
|
376
|
+
"aria-expanded": s.isOpen,
|
|
377
|
+
}),
|
|
378
|
+
Icon: { name: "chevronDown", attr: { "aria-hidden": "true" } },
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
Dropdown: {
|
|
382
|
+
attr: { role: "listbox", "aria-label": "Options" },
|
|
383
|
+
hide: (el, s) => !s.isOpen,
|
|
384
|
+
children: (el, s) => s.options,
|
|
385
|
+
childrenAs: "state",
|
|
386
|
+
childExtends: "Button",
|
|
387
|
+
childProps: {
|
|
388
|
+
attr: (el, s) => ({
|
|
389
|
+
role: "option",
|
|
390
|
+
"aria-selected": s.isSelected,
|
|
391
|
+
}),
|
|
392
|
+
theme: "transparent",
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Accessible Tabs
|
|
399
|
+
|
|
400
|
+
```js
|
|
401
|
+
export const Tabs = {
|
|
402
|
+
state: { activeTab: 0 },
|
|
403
|
+
|
|
404
|
+
TabList: {
|
|
405
|
+
attr: { role: "tablist" },
|
|
406
|
+
flow: "x",
|
|
407
|
+
gap: "Z",
|
|
408
|
+
onKeydown: (e, el, s) => {
|
|
409
|
+
const tabs = el.node.querySelectorAll('[role="tab"]');
|
|
410
|
+
if (e.key === "ArrowRight")
|
|
411
|
+
s.update({ activeTab: (s.activeTab + 1) % tabs.length });
|
|
412
|
+
if (e.key === "ArrowLeft")
|
|
413
|
+
s.update({ activeTab: (s.activeTab - 1 + tabs.length) % tabs.length });
|
|
414
|
+
},
|
|
415
|
+
children: (el, s) => s.tabs,
|
|
416
|
+
childrenAs: "state",
|
|
417
|
+
childExtends: "Button",
|
|
418
|
+
childProps: (el, s) => ({
|
|
419
|
+
attr: {
|
|
420
|
+
role: "tab",
|
|
421
|
+
"aria-selected": s.parent.activeTab === s.index,
|
|
422
|
+
tabindex: s.parent.activeTab === s.index ? "0" : "-1",
|
|
423
|
+
},
|
|
424
|
+
onClick: (e, el, s) => s.parent.update({ activeTab: s.index }),
|
|
425
|
+
}),
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
TabPanel: {
|
|
429
|
+
attr: (el, s) => ({
|
|
430
|
+
role: "tabpanel",
|
|
431
|
+
tabindex: "0",
|
|
432
|
+
}),
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Motion and Reduced Motion
|
|
438
|
+
|
|
439
|
+
Respect user motion preferences with `@media (prefers-reduced-motion)`.
|
|
440
|
+
|
|
441
|
+
```js
|
|
442
|
+
{
|
|
443
|
+
transition: 'background, defaultBezier, A',
|
|
444
|
+
animationDuration: 'C',
|
|
445
|
+
|
|
446
|
+
// Disable animations for users who prefer reduced motion
|
|
447
|
+
style: {
|
|
448
|
+
'@media (prefers-reduced-motion: reduce)': {
|
|
449
|
+
transition: 'none',
|
|
450
|
+
animation: 'none'
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
## Checklist
|
|
457
|
+
|
|
458
|
+
Before shipping any component, verify:
|
|
459
|
+
|
|
460
|
+
- [ ] All interactive elements are reachable by keyboard (Tab, Enter, Space, Escape, Arrows)
|
|
461
|
+
- [ ] Visible focus indicator on every focusable element
|
|
462
|
+
- [ ] Every `img` has `alt` text (or `alt=""` + `aria-hidden` for decorative)
|
|
463
|
+
- [ ] Icon-only buttons have `aria-label`
|
|
464
|
+
- [ ] Form inputs have associated labels
|
|
465
|
+
- [ ] Error messages use `role="alert"` and are linked via `aria-describedby`
|
|
466
|
+
- [ ] Color is not the only way to convey information
|
|
467
|
+
- [ ] Dynamic content updates use `aria-live` regions
|
|
468
|
+
- [ ] Modals trap focus and close on Escape
|
|
469
|
+
- [ ] Use semantic atoms (`Button`, `Link`, `Nav`, `Main`) over generic `Box`/`Div`
|
|
470
|
+
- [ ] Text contrast meets WCAG AA (4.5:1 for normal text, 3:1 for large)
|
|
471
|
+
- [ ] Animations respect `prefers-reduced-motion`
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
The Accessibility Auditor
|
|
2
|
+
|
|
3
|
+
You are an Accessibility Specialist at Apple, ensuring designs work for everyone.
|
|
4
|
+
|
|
5
|
+
Perform a comprehensive accessibility audit of this design:
|
|
6
|
+
|
|
7
|
+
[DESIGN DESCRIPTION OR UPLOADED DESIGN]
|
|
8
|
+
|
|
9
|
+
Audit against WCAG 2.2 Level AA standards:
|
|
10
|
+
|
|
11
|
+
1. PERCEIVABLE
|
|
12
|
+
□ Text alternatives for images (alt text strategy)
|
|
13
|
+
□ Captions/transcripts for multimedia
|
|
14
|
+
□ Color not used as sole means of conveying information
|
|
15
|
+
□ Color contrast ratios:
|
|
16
|
+
- Normal text: 4.5:1 minimum
|
|
17
|
+
- Large text: 3:1 minimum
|
|
18
|
+
- UI components: 3:1 minimum
|
|
19
|
+
□ Resize text up to 200% without loss of content/functionality
|
|
20
|
+
□ Images of text avoided (except logos)
|
|
21
|
+
|
|
22
|
+
2. OPERABLE
|
|
23
|
+
□ All functionality available from keyboard
|
|
24
|
+
□ No keyboard traps
|
|
25
|
+
□ Skip links provided for repetitive content
|
|
26
|
+
□ Page titles descriptive and unique
|
|
27
|
+
□ Focus order logical and predictable
|
|
28
|
+
□ Link purpose clear from context
|
|
29
|
+
□ Multiple ways to find pages (search, navigation, sitemap)
|
|
30
|
+
□ Headings and labels descriptive
|
|
31
|
+
□ Focus visible (minimum 2px outline, 3:1 contrast)
|
|
32
|
+
□ Pointer gestures have single-pointer alternatives
|
|
33
|
+
□ Motion animation can be disabled (prefers-reduced-motion)
|
|
34
|
+
□ No auto-playing audio
|
|
35
|
+
□ Touch targets minimum 44×44 CSS pixels
|
|
36
|
+
|
|
37
|
+
3. UNDERSTANDABLE
|
|
38
|
+
□ Language of page identified
|
|
39
|
+
□ Language of parts identified
|
|
40
|
+
□ Components with same function identified consistently
|
|
41
|
+
□ Error identification clear
|
|
42
|
+
□ Error suggestions provided
|
|
43
|
+
□ Error prevention for legal/financial/data (confirmations/reversible)
|
|
44
|
+
□ Contextual help available
|
|
45
|
+
|
|
46
|
+
4. ROBUST
|
|
47
|
+
□ Valid HTML/markup
|
|
48
|
+
□ Name, role, value available for all components
|
|
49
|
+
□ Status messages announced (ARIA live regions)
|
|
50
|
+
|
|
51
|
+
5. MOBILE-SPECIFIC
|
|
52
|
+
□ Orientation not locked (responsive to rotation)
|
|
53
|
+
□ Input modalities supported (touch, mouse, keyboard, voice)
|
|
54
|
+
□ Placed where user can reach (thumb zone considerations)
|
|
55
|
+
|
|
56
|
+
6. COGNITIVE ACCESSIBILITY
|
|
57
|
+
□ Reading level appropriate (Flesch-Kincaid Grade 8 or below)
|
|
58
|
+
□ Consistent navigation placement
|
|
59
|
+
□ Error messages plain language, no jargon
|
|
60
|
+
□ Time limits can be extended or eliminated
|
|
61
|
+
□ No flashing content (3 flashes per second maximum)
|
|
62
|
+
|
|
63
|
+
DELIVERABLES:
|
|
64
|
+
• Pass/fail checklist for each criterion
|
|
65
|
+
• Specific violations with location and severity
|
|
66
|
+
• Remediation recommendations with code/design solutions
|
|
67
|
+
• Accessibility statement template
|
|
68
|
+
• Testing checklist for QA team
|
|
69
|
+
|
|
70
|
+
Include screen reader navigation flow descriptions.
|