@fpkit/acss 1.0.0-beta.1 → 2.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.
Files changed (103) hide show
  1. package/README.md +92 -0
  2. package/docs/README.md +325 -0
  3. package/docs/guides/accessibility.md +764 -0
  4. package/docs/guides/architecture.md +705 -0
  5. package/docs/guides/composition.md +688 -0
  6. package/docs/guides/css-variables.md +522 -0
  7. package/docs/guides/storybook.md +828 -0
  8. package/docs/guides/testing.md +817 -0
  9. package/docs/testing/focus-indicator-testing.md +437 -0
  10. package/libs/{chunk-7XPFW7CB.js → chunk-43TK2ICH.js} +2 -2
  11. package/libs/chunk-5PJYLVFY.cjs +17 -0
  12. package/libs/chunk-5PJYLVFY.cjs.map +1 -0
  13. package/libs/chunk-E4OSROCA.cjs +17 -0
  14. package/libs/chunk-E4OSROCA.cjs.map +1 -0
  15. package/libs/chunk-KVKQLRJG.js +10 -0
  16. package/libs/chunk-KVKQLRJG.js.map +1 -0
  17. package/libs/{chunk-QVW6W76L.cjs → chunk-MGPWZRBX.cjs} +3 -3
  18. package/libs/chunk-NNTBIHSD.js +8 -0
  19. package/libs/chunk-NNTBIHSD.js.map +1 -0
  20. package/libs/{chunk-X3JCTEPD.js → chunk-QKHPHMG2.js} +2 -2
  21. package/libs/{chunk-T4T6GWYQ.cjs → chunk-R7NLLZU2.cjs} +3 -3
  22. package/libs/{chunk-X5LGFCWG.js → chunk-UJAQVHWC.js} +3 -3
  23. package/libs/{chunk-DKTHCQ5P.cjs → chunk-X5RKCLDC.cjs} +3 -3
  24. package/libs/components/breadcrumbs/breadcrumb.cjs +5 -5
  25. package/libs/components/breadcrumbs/breadcrumb.d.cts +1 -1
  26. package/libs/components/breadcrumbs/breadcrumb.d.ts +1 -1
  27. package/libs/components/breadcrumbs/breadcrumb.js +2 -2
  28. package/libs/components/button.cjs +3 -3
  29. package/libs/components/button.d.cts +1 -1
  30. package/libs/components/button.d.ts +1 -1
  31. package/libs/components/button.js +1 -1
  32. package/libs/components/buttons/button.css +1 -1
  33. package/libs/components/buttons/button.css.map +1 -1
  34. package/libs/components/buttons/button.min.css +2 -2
  35. package/libs/components/dialog/dialog.cjs +4 -4
  36. package/libs/components/dialog/dialog.js +2 -2
  37. package/libs/components/icons/icon.d.cts +32 -32
  38. package/libs/components/icons/icon.d.ts +32 -32
  39. package/libs/components/link/link.cjs +11 -3
  40. package/libs/components/link/link.d.cts +131 -3
  41. package/libs/components/link/link.d.ts +131 -3
  42. package/libs/components/link/link.js +1 -1
  43. package/libs/components/list/list.css +1 -1
  44. package/libs/components/list/list.min.css +1 -1
  45. package/libs/components/modal.cjs +3 -3
  46. package/libs/components/modal.js +2 -2
  47. package/libs/hooks.cjs +3 -3
  48. package/libs/hooks.d.cts +1 -1
  49. package/libs/hooks.d.ts +1 -1
  50. package/libs/hooks.js +2 -2
  51. package/libs/index.cjs +12 -12
  52. package/libs/index.css +1 -1
  53. package/libs/index.css.map +1 -1
  54. package/libs/index.d.cts +237 -2
  55. package/libs/index.d.ts +237 -2
  56. package/libs/index.js +5 -5
  57. package/package.json +4 -3
  58. package/src/components/README.mdx +1 -1
  59. package/src/components/breadcrumbs/breadcrumb.test.tsx +1 -2
  60. package/src/components/buttons/README.mdx +19 -9
  61. package/src/components/buttons/button.scss +5 -0
  62. package/src/components/buttons/button.stories.tsx +8 -5
  63. package/src/components/buttons/button.tsx +19 -15
  64. package/src/components/cards/card.stories.tsx +1 -1
  65. package/src/components/details/details.stories.tsx +1 -1
  66. package/src/components/form/form.stories.tsx +1 -1
  67. package/src/components/form/input.stories.tsx +1 -1
  68. package/src/components/form/select.stories.tsx +1 -1
  69. package/src/components/heading/README.mdx +292 -0
  70. package/src/components/icons/icon.stories.tsx +1 -1
  71. package/src/components/link/link.stories.tsx +205 -8
  72. package/src/components/link/link.test.tsx +1 -1
  73. package/src/components/link/link.tsx +22 -0
  74. package/src/components/link/link.types.ts +11 -3
  75. package/src/components/list/list.scss +1 -1
  76. package/src/components/nav/nav.stories.tsx +1 -1
  77. package/src/components/ui.stories.tsx +53 -19
  78. package/src/docs/accessibility.mdx +484 -0
  79. package/src/docs/composition.mdx +549 -0
  80. package/src/docs/css-variables.mdx +380 -0
  81. package/src/docs/fpkit-developer.mdx +623 -0
  82. package/src/introduction.mdx +356 -0
  83. package/src/styles/buttons/button.css +4 -0
  84. package/src/styles/buttons/button.css.map +1 -1
  85. package/src/styles/index.css +9 -3
  86. package/src/styles/index.css.map +1 -1
  87. package/src/styles/list/list.css +1 -1
  88. package/src/styles/utilities/_disabled.scss +5 -4
  89. package/libs/chunk-33PNJ4LO.cjs +0 -15
  90. package/libs/chunk-33PNJ4LO.cjs.map +0 -1
  91. package/libs/chunk-GT77BX4L.cjs +0 -17
  92. package/libs/chunk-GT77BX4L.cjs.map +0 -1
  93. package/libs/chunk-OVWLQYMK.js +0 -10
  94. package/libs/chunk-OVWLQYMK.js.map +0 -1
  95. package/libs/chunk-UEPAWMDF.js +0 -8
  96. package/libs/chunk-UEPAWMDF.js.map +0 -1
  97. package/libs/link-5192f411.d.ts +0 -323
  98. /package/libs/{chunk-7XPFW7CB.js.map → chunk-43TK2ICH.js.map} +0 -0
  99. /package/libs/{chunk-QVW6W76L.cjs.map → chunk-MGPWZRBX.cjs.map} +0 -0
  100. /package/libs/{chunk-X3JCTEPD.js.map → chunk-QKHPHMG2.js.map} +0 -0
  101. /package/libs/{chunk-T4T6GWYQ.cjs.map → chunk-R7NLLZU2.cjs.map} +0 -0
  102. /package/libs/{chunk-X5LGFCWG.js.map → chunk-UJAQVHWC.js.map} +0 -0
  103. /package/libs/{chunk-DKTHCQ5P.cjs.map → chunk-X5RKCLDC.cjs.map} +0 -0
@@ -0,0 +1,764 @@
1
+ # Accessibility Guide
2
+
3
+ ## Overview
4
+
5
+ @fpkit/acss components follow **WCAG 2.1 Level AA** standards. This guide explains accessibility patterns, ARIA attributes, keyboard navigation, and focus management to help you maintain accessibility when using and composing fpkit components.
6
+
7
+ ---
8
+
9
+ ## Core Principles
10
+
11
+ ### 1. Semantic HTML First
12
+
13
+ fpkit components use the most appropriate HTML elements by default:
14
+
15
+ ```tsx
16
+ // fpkit Button renders as <button>
17
+ import { Button } from '@fpkit/acss'
18
+ <Button>Click me</Button>
19
+ // Renders: <button type="button">Click me</button>
20
+
21
+ // fpkit Card renders as <article>
22
+ import { Card } from '@fpkit/acss'
23
+ <Card>Content</Card>
24
+ // Renders: <article>Content</article>
25
+ ```
26
+
27
+ **Polymorphic Components**: Many fpkit components support the `as` prop for semantic flexibility:
28
+
29
+ ```tsx
30
+ // Button as link
31
+ <Button as="a" href="/page">Navigate</Button>
32
+
33
+ // Card as section
34
+ <Card as="section">...</Card>
35
+ ```
36
+
37
+ ### 2. Keyboard Navigation
38
+
39
+ All fpkit interactive components are keyboard accessible:
40
+
41
+ - **Tab**: Navigate between focusable elements
42
+ - **Enter/Space**: Activate buttons and links
43
+ - **Arrow keys**: Navigate within menus, tabs, and lists
44
+ - **Escape**: Close modals and dialogs
45
+
46
+ **Testing**: Try navigating your app using only the keyboard:
47
+ ```bash
48
+ # Tab through interactive elements
49
+ # Enter/Space to activate
50
+ # Escape to close
51
+ ```
52
+
53
+ ### 3. Focus Management
54
+
55
+ fpkit components include built-in focus management:
56
+
57
+ - **Visible focus indicators**: `:focus-visible` for keyboard users only
58
+ - **Focus trapping**: Modals keep focus within the dialog
59
+ - **Focus restoration**: Focus returns to trigger element when closing overlays
60
+
61
+ **Customizing focus styles**:
62
+ ```css
63
+ /* Override focus indicator globally */
64
+ :root {
65
+ --btn-focus-outline: 2px solid #0066cc;
66
+ --btn-focus-outline-offset: 2px;
67
+ }
68
+ ```
69
+
70
+ ### 4. Screen Reader Support
71
+
72
+ fpkit components include appropriate ARIA attributes:
73
+
74
+ - **Descriptive labels**: Every interactive element has a label
75
+ - **Alternative text**: Icons have `aria-label` or `aria-hidden`
76
+ - **ARIA attributes**: States and properties for complex widgets
77
+ - **Live regions**: Alerts and notifications use proper ARIA roles
78
+
79
+ ---
80
+
81
+ ## ARIA Attributes
82
+
83
+ ### Labels and Descriptions
84
+
85
+ fpkit components handle basic labeling, but you may need to add context:
86
+
87
+ ```tsx
88
+ // Icon-only button needs aria-label
89
+ <Button aria-label="Close dialog">
90
+ <Icon name="close" />
91
+ </Button>
92
+
93
+ // Button with visible text - no aria-label needed
94
+ <Button>
95
+ <Icon name="save" aria-hidden="true" />
96
+ Save
97
+ </Button>
98
+
99
+ // Grouping with aria-labelledby
100
+ <div role="group" aria-labelledby="filter-heading">
101
+ <h3 id="filter-heading">Filter Options</h3>
102
+ <Button>Apply</Button>
103
+ </div>
104
+
105
+ // Additional description
106
+ <Input
107
+ type="email"
108
+ aria-describedby="email-hint"
109
+ />
110
+ <div id="email-hint">We'll never share your email</div>
111
+ ```
112
+
113
+ ### States and Properties
114
+
115
+ ```tsx
116
+ // Expanded state (dropdowns, accordions)
117
+ <Button aria-expanded={isOpen} aria-controls="menu-list">
118
+ Menu
119
+ </Button>
120
+ <div id="menu-list" hidden={!isOpen}>
121
+ {/* Menu items */}
122
+ </div>
123
+
124
+ // Toggle state (toolbar buttons)
125
+ <Button aria-pressed={isBold} onClick={toggleBold}>
126
+ <Icon name="bold" aria-hidden="true" />
127
+ Bold
128
+ </Button>
129
+
130
+ // Current page in navigation
131
+ <nav aria-label="Pagination">
132
+ <Button aria-current="page">1</Button>
133
+ <Button>2</Button>
134
+ <Button>3</Button>
135
+ </nav>
136
+
137
+ // Hide decorative elements
138
+ <Badge>
139
+ New
140
+ <span aria-hidden="true">✨</span>
141
+ </Badge>
142
+ ```
143
+
144
+ ### Live Regions
145
+
146
+ Announce dynamic content changes:
147
+
148
+ ```tsx
149
+ // Polite announcement (non-urgent)
150
+ <div aria-live="polite" aria-atomic="true">
151
+ {statusMessage}
152
+ </div>
153
+
154
+ // Alert role (urgent messages)
155
+ <Alert variant="error" role="alert">
156
+ Form submission failed. Please check your input.
157
+ </Alert>
158
+
159
+ // Status role (progress updates)
160
+ <div role="status" aria-live="polite">
161
+ Saving... {progress}% complete
162
+ </div>
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Button Patterns
168
+
169
+ ### Why fpkit Uses aria-disabled
170
+
171
+ fpkit buttons use `aria-disabled` instead of native `disabled`:
172
+
173
+ ```tsx
174
+ // fpkit Button with aria-disabled
175
+ <Button disabled>Submit</Button>
176
+ // Renders: <button aria-disabled="true">Submit</button>
177
+
178
+ // NOT: <button disabled>Submit</button>
179
+ ```
180
+
181
+ **Benefits:**
182
+ - **Keyboard accessible**: Disabled buttons remain in tab order
183
+ - **Screen reader context**: Users can discover why it's disabled
184
+ - **Tooltip compatible**: Can show explanation tooltips
185
+ - **Consistent styling**: Easier to style with CSS variables
186
+
187
+ **Adding tooltips to disabled buttons**:
188
+ ```tsx
189
+ <Tooltip content="Complete all required fields first">
190
+ <Button disabled>Submit</Button>
191
+ </Tooltip>
192
+ ```
193
+
194
+ ### Button Types
195
+
196
+ Always specify button type when inside forms:
197
+
198
+ ```tsx
199
+ // Inside forms
200
+ <form>
201
+ <Button type="submit">Save</Button>
202
+ <Button type="reset">Clear</Button>
203
+ <Button type="button">Cancel</Button>
204
+ </form>
205
+
206
+ // Outside forms - defaults to type="button"
207
+ <Button onClick={handleAction}>Action</Button>
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Interactive Elements
213
+
214
+ ### Making Non-Button Elements Clickable
215
+
216
+ When you need to make a non-button element interactive (e.g., clickable card):
217
+
218
+ ```tsx
219
+ import { Card } from '@fpkit/acss'
220
+
221
+ <Card
222
+ as="article"
223
+ role="button"
224
+ tabIndex={0}
225
+ onClick={handleClick}
226
+ onKeyDown={(e) => {
227
+ if (e.key === 'Enter' || e.key === ' ') {
228
+ e.preventDefault()
229
+ handleClick(e)
230
+ }
231
+ }}
232
+ aria-label="View article details"
233
+ >
234
+ {/* Card content */}
235
+ </Card>
236
+ ```
237
+
238
+ **Requirements:**
239
+ - `role="button"`: Announces as interactive
240
+ - `tabIndex={0}`: Makes keyboard focusable
241
+ - `onClick`: Mouse/touch interaction
242
+ - `onKeyDown`: Keyboard activation (Enter/Space)
243
+ - `aria-label` or `aria-labelledby`: Descriptive label
244
+
245
+ **Better alternative**: Wrap in an actual button or link when possible:
246
+ ```tsx
247
+ // Better - uses semantic <a> element
248
+ <Card as="a" href="/article/123">
249
+ {/* Card content */}
250
+ </Card>
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Focus Visible Pattern
256
+
257
+ fpkit components use `:focus-visible` to show focus only for keyboard users:
258
+
259
+ ```scss
260
+ // Already built into fpkit components
261
+ button {
262
+ outline: none; // Removes default for mouse users
263
+
264
+ &:focus-visible {
265
+ outline: 2px solid var(--btn-focus-outline);
266
+ outline-offset: var(--btn-focus-outline-offset, 2px);
267
+ }
268
+ }
269
+ ```
270
+
271
+ **Customizing focus indicators**:
272
+ ```css
273
+ /* Global override */
274
+ :root {
275
+ --btn-focus-outline: 3px solid #ff6b6b;
276
+ --btn-focus-outline-offset: 4px;
277
+ }
278
+
279
+ /* Component-specific */
280
+ .primary-button {
281
+ --btn-focus-outline: 3px solid #0066cc;
282
+ }
283
+ ```
284
+
285
+ **WCAG 2.4.7: Focus Visible** - Any keyboard operable interface has a mode where the focus indicator is visible.
286
+
287
+ ---
288
+
289
+ ## Link Patterns
290
+
291
+ ### External Links
292
+
293
+ Inform users when links open in new tabs:
294
+
295
+ ```tsx
296
+ import { Link } from '@fpkit/acss'
297
+
298
+ // External link pattern
299
+ <Link
300
+ href="https://example.com"
301
+ target="_blank"
302
+ rel="noopener noreferrer"
303
+ >
304
+ External Site
305
+ <span className="sr-only">(opens in new tab)</span>
306
+ </Link>
307
+ ```
308
+
309
+ ### Visited Link State
310
+
311
+ Maintain visited link styling:
312
+
313
+ ```css
314
+ /* Customize visited state */
315
+ :root {
316
+ --link-color: #0066cc;
317
+ --link-visited-color: #551a8b;
318
+ --link-hover-color: #004499;
319
+ }
320
+ ```
321
+
322
+ ---
323
+
324
+ ## Form Patterns
325
+
326
+ ### Field Labels
327
+
328
+ Always associate labels with inputs:
329
+
330
+ ```tsx
331
+ // ✅ Explicit association (recommended)
332
+ <label htmlFor="email">Email Address</label>
333
+ <Input id="email" type="email" name="email" />
334
+
335
+ // ✅ Implicit association
336
+ <label>
337
+ Email Address
338
+ <Input type="email" name="email" />
339
+ </label>
340
+
341
+ // ❌ No association - inaccessible
342
+ <label>Email Address</label>
343
+ <Input type="email" name="email" />
344
+ ```
345
+
346
+ ### Error Messages
347
+
348
+ Link error messages to inputs:
349
+
350
+ ```tsx
351
+ <label htmlFor="password">Password</label>
352
+ <Input
353
+ id="password"
354
+ type="password"
355
+ aria-describedby={hasError ? 'password-error' : undefined}
356
+ aria-invalid={hasError}
357
+ />
358
+ {hasError && (
359
+ <div id="password-error" role="alert">
360
+ Password must be at least 8 characters
361
+ </div>
362
+ )}
363
+ ```
364
+
365
+ ### Required Fields
366
+
367
+ ```tsx
368
+ <label htmlFor="name">
369
+ Name <span aria-label="required">*</span>
370
+ </label>
371
+ <Input
372
+ id="name"
373
+ type="text"
374
+ required
375
+ aria-required="true"
376
+ />
377
+ ```
378
+
379
+ ### Field Hints
380
+
381
+ ```tsx
382
+ <label htmlFor="username">Username</label>
383
+ <Input
384
+ id="username"
385
+ type="text"
386
+ aria-describedby="username-hint"
387
+ />
388
+ <div id="username-hint">
389
+ 3-20 characters, letters and numbers only
390
+ </div>
391
+ ```
392
+
393
+ ---
394
+
395
+ ## Modal/Dialog Patterns
396
+
397
+ fpkit Dialog components handle focus management automatically:
398
+
399
+ ```tsx
400
+ import { Dialog, Button } from '@fpkit/acss'
401
+
402
+ <Dialog
403
+ isOpen={isOpen}
404
+ onClose={handleClose}
405
+ aria-labelledby="dialog-title"
406
+ >
407
+ <h2 id="dialog-title">Confirm Action</h2>
408
+ <p>Are you sure you want to proceed?</p>
409
+ <Button onClick={handleClose}>Cancel</Button>
410
+ <Button onClick={handleConfirm}>Confirm</Button>
411
+ </Dialog>
412
+ ```
413
+
414
+ **Built-in features:**
415
+ - ✅ Focus trap (keyboard stays within dialog)
416
+ - ✅ Escape key closes dialog
417
+ - ✅ Focus restoration (returns to trigger element)
418
+ - ✅ Backdrop click closes (with `closeOnBackdrop` prop)
419
+ - ✅ `aria-modal="true"` attribute
420
+
421
+ **Custom focus management**:
422
+ ```tsx
423
+ import { useEffect, useRef } from 'react'
424
+
425
+ const CustomDialog = ({ isOpen, onClose }) => {
426
+ const firstFocusRef = useRef(null)
427
+
428
+ useEffect(() => {
429
+ if (isOpen) {
430
+ // Focus specific element when opening
431
+ firstFocusRef.current?.focus()
432
+ }
433
+ }, [isOpen])
434
+
435
+ return (
436
+ <Dialog isOpen={isOpen} onClose={onClose}>
437
+ <Button ref={firstFocusRef}>Primary Action</Button>
438
+ </Dialog>
439
+ )
440
+ }
441
+ ```
442
+
443
+ ---
444
+
445
+ ## Screen Reader Only Content
446
+
447
+ ### Visually Hidden Text
448
+
449
+ Use the `sr-only` utility class for screen reader only content:
450
+
451
+ ```css
452
+ /* Add to your global CSS */
453
+ .sr-only {
454
+ position: absolute;
455
+ width: 1px;
456
+ height: 1px;
457
+ padding: 0;
458
+ margin: -1px;
459
+ overflow: hidden;
460
+ clip: rect(0, 0, 0, 0);
461
+ white-space: nowrap;
462
+ border-width: 0;
463
+ }
464
+ ```
465
+
466
+ ```tsx
467
+ // Icon-only button
468
+ <Button>
469
+ <Icon name="close" aria-hidden="true" />
470
+ <span className="sr-only">Close dialog</span>
471
+ </Button>
472
+
473
+ // Loading indicator
474
+ <div>
475
+ <Spinner aria-hidden="true" />
476
+ <span className="sr-only">Loading content...</span>
477
+ </div>
478
+ ```
479
+
480
+ ---
481
+
482
+ ## Color Contrast
483
+
484
+ ### WCAG AA Requirements
485
+
486
+ - **Normal text** (< 18pt): 4.5:1 contrast ratio minimum
487
+ - **Large text** (≥ 18pt or ≥ 14pt bold): 3:1 contrast ratio minimum
488
+ - **UI components**: 3:1 for interactive elements
489
+
490
+ fpkit components meet these requirements by default:
491
+
492
+ ```scss
493
+ // Example built-in contrasts
494
+ --btn-primary-bg: #0066cc; // Blue
495
+ --btn-primary-color: #ffffff; // White (7.5:1 - exceeds AA)
496
+
497
+ --alert-error-bg: #f8d7da;
498
+ --alert-error-color: #721c24; // Dark red (9.2:1 - exceeds AA)
499
+ ```
500
+
501
+ **Testing color contrast**:
502
+ 1. Browser DevTools → Elements → Accessibility
503
+ 2. [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
504
+ 3. Lighthouse accessibility audit
505
+
506
+ **Custom colors**:
507
+ ```css
508
+ /* Ensure sufficient contrast when overriding */
509
+ :root {
510
+ --btn-custom-bg: #your-color;
511
+ --btn-custom-color: #text-color; /* Test contrast! */
512
+ }
513
+ ```
514
+
515
+ ---
516
+
517
+ ## Landmarks and Regions
518
+
519
+ ### Semantic HTML5 Elements
520
+
521
+ ```tsx
522
+ <header>
523
+ <nav aria-label="Main navigation">
524
+ {/* Navigation links */}
525
+ </nav>
526
+ </header>
527
+
528
+ <main>
529
+ <article>
530
+ <header>
531
+ <h1>Article Title</h1>
532
+ </header>
533
+ <section>
534
+ {/* Article content */}
535
+ </section>
536
+ </article>
537
+
538
+ <aside aria-label="Related articles">
539
+ {/* Sidebar content */}
540
+ </aside>
541
+ </main>
542
+
543
+ <footer>
544
+ <nav aria-label="Footer navigation">
545
+ {/* Footer links */}
546
+ </nav>
547
+ </footer>
548
+ ```
549
+
550
+ ### Multiple Landmarks of Same Type
551
+
552
+ Use `aria-label` to differentiate:
553
+
554
+ ```tsx
555
+ <nav aria-label="Main navigation">...</nav>
556
+ <nav aria-label="Footer navigation">...</nav>
557
+
558
+ <aside aria-label="Related articles">...</aside>
559
+ <aside aria-label="Advertisements">...</aside>
560
+ ```
561
+
562
+ ---
563
+
564
+ ## Testing Accessibility
565
+
566
+ ### Manual Testing
567
+
568
+ #### 1. Keyboard Navigation
569
+ - Tab through all interactive elements
570
+ - Activate with Enter/Space
571
+ - Navigate menus with arrow keys
572
+ - Close modals with Escape
573
+ - Ensure focus order is logical
574
+
575
+ #### 2. Screen Reader Testing
576
+
577
+ **macOS - VoiceOver**:
578
+ ```bash
579
+ # Enable VoiceOver
580
+ Cmd + F5
581
+
582
+ # Navigate
583
+ VO + Right Arrow (next)
584
+ VO + Left Arrow (previous)
585
+ ```
586
+
587
+ **Windows - NVDA** (free):
588
+ - Download from [nvaccess.org](https://www.nvaccess.org/)
589
+ - Navigate with arrow keys
590
+ - Read with Insert + Down Arrow
591
+
592
+ #### 3. Browser DevTools
593
+
594
+ **Chrome/Edge**:
595
+ 1. DevTools → Lighthouse → Accessibility audit
596
+ 2. DevTools → Elements → Accessibility pane
597
+ 3. Inspect accessibility tree
598
+
599
+ **Firefox**:
600
+ - DevTools → Accessibility inspector
601
+
602
+ ### Automated Testing
603
+
604
+ ```tsx
605
+ import { render } from '@testing-library/react'
606
+ import { axe, toHaveNoViolations } from 'jest-axe'
607
+
608
+ expect.extend(toHaveNoViolations)
609
+
610
+ describe('Button accessibility', () => {
611
+ it('should not have accessibility violations', async () => {
612
+ const { container } = render(<Button>Click me</Button>)
613
+ const results = await axe(container)
614
+ expect(results).toHaveNoViolations()
615
+ })
616
+ })
617
+ ```
618
+
619
+ ### Accessibility Testing Tools
620
+
621
+ | Tool | Type | Use Case |
622
+ |------|------|----------|
623
+ | [axe DevTools](https://www.deque.com/axe/devtools/) | Browser Extension | Real-time violation detection |
624
+ | [WAVE](https://wave.webaim.org/extension/) | Browser Extension | Visual feedback on issues |
625
+ | [Lighthouse](https://developers.google.com/web/tools/lighthouse) | Built-in Chrome | Comprehensive audit |
626
+ | [jest-axe](https://github.com/nickcolley/jest-axe) | Testing Library | Automated unit testing |
627
+ | [pa11y](https://pa11y.org/) | CLI | CI/CD integration |
628
+
629
+ ---
630
+
631
+ ## WCAG 2.1 Level AA Checklist
632
+
633
+ ### Perceivable
634
+
635
+ - [ ] Text alternatives for non-text content (images, icons)
636
+ - [ ] Captions/transcripts for audio/video
637
+ - [ ] Content can be presented in different ways without losing information
638
+ - [ ] Sufficient color contrast (4.5:1 for normal, 3:1 for large text)
639
+ - [ ] Text can be resized up to 200% without loss of functionality
640
+
641
+ ### Operable
642
+
643
+ - [ ] All functionality available via keyboard
644
+ - [ ] No keyboard traps (can navigate away from all elements)
645
+ - [ ] Users have enough time to read and interact with content
646
+ - [ ] No content flashes more than 3 times per second
647
+ - [ ] Clear page titles and headings
648
+ - [ ] Visible focus indicator for keyboard navigation
649
+ - [ ] Multiple ways to navigate (search, sitemap, nav)
650
+
651
+ ### Understandable
652
+
653
+ - [ ] Language of page is programmatically determined
654
+ - [ ] Labels and instructions provided for user input
655
+ - [ ] Error messages are clear and helpful
656
+ - [ ] Consistent navigation and identification
657
+ - [ ] Components behave predictably
658
+
659
+ ### Robust
660
+
661
+ - [ ] Valid HTML (no duplicate IDs, proper nesting)
662
+ - [ ] ARIA attributes used correctly
663
+ - [ ] Compatible with current and future assistive technologies
664
+ - [ ] Status messages announced to screen readers
665
+
666
+ ---
667
+
668
+ ## Common Mistakes to Avoid
669
+
670
+ ### ❌ Don't
671
+
672
+ - Use `div` or `span` as buttons without proper ARIA
673
+ - Remove focus outlines without providing alternatives
674
+ - Use `placeholder` as a label replacement
675
+ - Use color alone to convey information
676
+ - Create keyboard traps unintentionally
677
+ - Use positive `tabindex` values (> 0) - disrupts natural tab order
678
+ - Announce every state change - overwhelming for screen readers
679
+ - Nest interactive elements (`<button>` inside `<a>`)
680
+ - Use `alt=""` on informative images
681
+ - Auto-play audio/video without controls
682
+
683
+ ### ✅ Do
684
+
685
+ - Use semantic HTML elements (button, nav, main, etc.)
686
+ - Provide visible focus indicators with `:focus-visible`
687
+ - Include proper labels for all form controls
688
+ - Use multiple cues (color + icon, color + text)
689
+ - Ensure modals trap focus intentionally
690
+ - Use `tabindex="0"` for custom interactive elements, `tabindex="-1"` to remove from tab order
691
+ - Use `aria-live="polite"` for non-critical updates, `"assertive"` for urgent ones
692
+ - Use `<button>` or `<a>` appropriately (buttons for actions, links for navigation)
693
+ - Provide meaningful `alt` text for images
694
+ - Provide controls and don't auto-play media
695
+
696
+ ---
697
+
698
+ ## Composing Accessible Components
699
+
700
+ When composing fpkit components, maintain accessibility:
701
+
702
+ ```tsx
703
+ import { Button, Badge } from '@fpkit/acss'
704
+
705
+ // ✅ Good - maintains accessibility
706
+ export const NotificationButton = ({ count, onClick }) => {
707
+ return (
708
+ <Button onClick={onClick} aria-label={`Notifications (${count} unread)`}>
709
+ <Icon name="bell" aria-hidden="true" />
710
+ {count > 0 && (
711
+ <Badge aria-hidden="true">{count}</Badge>
712
+ )}
713
+ </Button>
714
+ )
715
+ }
716
+ ```
717
+
718
+ **Why this works:**
719
+ - Button is keyboard accessible (inherits from fpkit Button)
720
+ - `aria-label` provides context for screen readers
721
+ - Visual elements (icon, badge) are hidden from screen readers with `aria-hidden`
722
+ - Count is announced via `aria-label`
723
+
724
+ ---
725
+
726
+ ## Resources
727
+
728
+ ### WCAG Guidelines
729
+
730
+ - [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/)
731
+ - [WCAG 2.1 Understanding Docs](https://www.w3.org/WAI/WCAG21/Understanding/)
732
+ - [WebAIM Articles](https://webaim.org/articles/)
733
+
734
+ ### ARIA
735
+
736
+ - [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
737
+ - [ARIA in HTML](https://www.w3.org/TR/html-aria/)
738
+ - [ARIA Roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)
739
+
740
+ ### Testing Tools
741
+
742
+ - [axe DevTools](https://www.deque.com/axe/devtools/)
743
+ - [WAVE Browser Extension](https://wave.webaim.org/extension/)
744
+ - [Lighthouse](https://developers.google.com/web/tools/lighthouse)
745
+ - [jest-axe](https://github.com/nickcolley/jest-axe)
746
+ - [Pa11y](https://pa11y.org/)
747
+
748
+ ### Learning Resources
749
+
750
+ - [Web Accessibility by Google](https://www.udacity.com/course/web-accessibility--ud891)
751
+ - [A11ycasts with Rob Dodson](https://www.youtube.com/playlist?list=PLNYkxOF6rcICWx0C9LVWWVqvHlYJyqw7g)
752
+ - [The A11Y Project](https://www.a11yproject.com/)
753
+
754
+ ---
755
+
756
+ ## Additional Guides
757
+
758
+ - **[CSS Variables Guide](./css-variables.md)** - Customize components accessibly
759
+ - **[Composition Guide](./composition.md)** - Build accessible compositions
760
+ - **[Testing Guide](./testing.md)** - Test accessibility in your components
761
+
762
+ ---
763
+
764
+ **Remember**: Accessibility is not optional. It ensures your application is usable by everyone, including people with disabilities. fpkit provides accessible components by default - your job is to maintain that accessibility when composing and customizing them.