omgkit 2.1.0 → 2.2.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 (56) hide show
  1. package/package.json +1 -1
  2. package/plugin/skills/SKILL_STANDARDS.md +743 -0
  3. package/plugin/skills/databases/mongodb/SKILL.md +797 -28
  4. package/plugin/skills/databases/postgresql/SKILL.md +494 -18
  5. package/plugin/skills/databases/prisma/SKILL.md +776 -30
  6. package/plugin/skills/databases/redis/SKILL.md +885 -25
  7. package/plugin/skills/devops/aws/SKILL.md +686 -28
  8. package/plugin/skills/devops/docker/SKILL.md +466 -18
  9. package/plugin/skills/devops/github-actions/SKILL.md +684 -29
  10. package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
  11. package/plugin/skills/frameworks/django/SKILL.md +920 -20
  12. package/plugin/skills/frameworks/express/SKILL.md +1361 -35
  13. package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
  14. package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
  15. package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
  16. package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
  17. package/plugin/skills/frameworks/rails/SKILL.md +594 -28
  18. package/plugin/skills/frameworks/react/SKILL.md +1006 -32
  19. package/plugin/skills/frameworks/spring/SKILL.md +528 -35
  20. package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
  21. package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
  22. package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
  23. package/plugin/skills/frontend/responsive/SKILL.md +847 -21
  24. package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
  25. package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
  26. package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
  27. package/plugin/skills/languages/javascript/SKILL.md +935 -31
  28. package/plugin/skills/languages/python/SKILL.md +489 -25
  29. package/plugin/skills/languages/typescript/SKILL.md +379 -30
  30. package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
  31. package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
  32. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
  33. package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
  34. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
  35. package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
  36. package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
  37. package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
  38. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
  39. package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
  40. package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
  41. package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
  42. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
  43. package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
  44. package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
  45. package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
  46. package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
  47. package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
  48. package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
  49. package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
  50. package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
  51. package/plugin/skills/security/better-auth/SKILL.md +1065 -28
  52. package/plugin/skills/security/oauth/SKILL.md +968 -31
  53. package/plugin/skills/security/owasp/SKILL.md +894 -33
  54. package/plugin/skills/testing/playwright/SKILL.md +764 -38
  55. package/plugin/skills/testing/pytest/SKILL.md +873 -36
  56. package/plugin/skills/testing/vitest/SKILL.md +980 -35
@@ -1,52 +1,1126 @@
1
1
  ---
2
2
  name: accessibility
3
- description: Accessibility (a11y). Use for WCAG compliance, screen readers, keyboard navigation.
3
+ description: Web accessibility (a11y) with WCAG compliance, ARIA, keyboard navigation, screen readers, and testing
4
+ category: frontend
5
+ triggers:
6
+ - accessibility
7
+ - a11y
8
+ - wcag
9
+ - screen reader
10
+ - aria
11
+ - keyboard navigation
4
12
  ---
5
13
 
6
- # Accessibility Skill
14
+ # Accessibility
15
+
16
+ Enterprise-grade **web accessibility** following WCAG 2.1 AA guidelines and best practices. This skill covers semantic HTML, ARIA attributes, keyboard navigation, focus management, screen reader support, and accessibility testing patterns used by top engineering teams.
17
+
18
+ ## Purpose
19
+
20
+ Build inclusive web applications:
21
+
22
+ - Implement WCAG 2.1 AA compliance
23
+ - Write semantic, accessible HTML
24
+ - Use ARIA attributes correctly
25
+ - Support keyboard navigation
26
+ - Manage focus appropriately
27
+ - Test with screen readers
28
+ - Build accessible components
29
+
30
+ ## Features
31
+
32
+ ### 1. Semantic HTML Structure
7
33
 
8
- ## Semantic HTML
9
34
  ```html
10
- <header>
11
- <nav aria-label="Main">
12
- <main>
13
- <article>
14
- <aside>
15
- <footer>
35
+ <!-- Accessible page structure -->
36
+ <!DOCTYPE html>
37
+ <html lang="en">
38
+ <head>
39
+ <meta charset="UTF-8">
40
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
41
+ <title>Page Title - Site Name</title>
42
+ </head>
43
+ <body>
44
+ <!-- Skip link for keyboard users -->
45
+ <a href="#main-content" class="skip-link">
46
+ Skip to main content
47
+ </a>
48
+
49
+ <header role="banner">
50
+ <nav aria-label="Main navigation">
51
+ <ul>
52
+ <li><a href="/" aria-current="page">Home</a></li>
53
+ <li><a href="/about">About</a></li>
54
+ <li><a href="/contact">Contact</a></li>
55
+ </ul>
56
+ </nav>
57
+ </header>
58
+
59
+ <main id="main-content" role="main">
60
+ <article>
61
+ <h1>Main Heading</h1>
62
+ <p>Content...</p>
63
+
64
+ <section aria-labelledby="section-heading">
65
+ <h2 id="section-heading">Section Title</h2>
66
+ <p>Section content...</p>
67
+ </section>
68
+ </article>
69
+
70
+ <aside aria-label="Related content">
71
+ <h2>Related Articles</h2>
72
+ <!-- Sidebar content -->
73
+ </aside>
74
+ </main>
75
+
76
+ <footer role="contentinfo">
77
+ <nav aria-label="Footer navigation">
78
+ <!-- Footer links -->
79
+ </nav>
80
+ <p>&copy; 2024 Company Name</p>
81
+ </footer>
82
+ </body>
83
+ </html>
16
84
  ```
17
85
 
18
- ## ARIA
19
- ```html
20
- <button aria-label="Close" aria-pressed="false">
21
- <div role="alert" aria-live="polite">
22
- <input aria-describedby="hint-id">
86
+ ```css
87
+ /* Skip link styles */
88
+ .skip-link {
89
+ position: absolute;
90
+ top: -40px;
91
+ left: 0;
92
+ background: #000;
93
+ color: #fff;
94
+ padding: 8px 16px;
95
+ z-index: 100;
96
+ transition: top 0.3s;
97
+ }
98
+
99
+ .skip-link:focus {
100
+ top: 0;
101
+ }
102
+
103
+ /* Focus styles - never remove, customize instead */
104
+ :focus {
105
+ outline: 2px solid #005fcc;
106
+ outline-offset: 2px;
107
+ }
108
+
109
+ :focus:not(:focus-visible) {
110
+ outline: none;
111
+ }
112
+
113
+ :focus-visible {
114
+ outline: 2px solid #005fcc;
115
+ outline-offset: 2px;
116
+ }
117
+
118
+ /* Visually hidden but accessible to screen readers */
119
+ .sr-only {
120
+ position: absolute;
121
+ width: 1px;
122
+ height: 1px;
123
+ padding: 0;
124
+ margin: -1px;
125
+ overflow: hidden;
126
+ clip: rect(0, 0, 0, 0);
127
+ white-space: nowrap;
128
+ border: 0;
129
+ }
130
+
131
+ /* Show when focused (for skip links) */
132
+ .sr-only-focusable:focus {
133
+ position: static;
134
+ width: auto;
135
+ height: auto;
136
+ padding: inherit;
137
+ margin: inherit;
138
+ overflow: visible;
139
+ clip: auto;
140
+ white-space: normal;
141
+ }
142
+ ```
143
+
144
+ ### 2. ARIA Attributes
145
+
146
+ ```tsx
147
+ // components/accessible/Alert.tsx
148
+ import React from "react";
149
+
150
+ interface AlertProps {
151
+ type: "info" | "success" | "warning" | "error";
152
+ children: React.ReactNode;
153
+ dismissible?: boolean;
154
+ onDismiss?: () => void;
155
+ }
156
+
157
+ export function Alert({ type, children, dismissible, onDismiss }: AlertProps) {
158
+ // Map type to appropriate ARIA role
159
+ const role = type === "error" ? "alert" : "status";
160
+ const ariaLive = type === "error" ? "assertive" : "polite";
161
+
162
+ return (
163
+ <div
164
+ role={role}
165
+ aria-live={ariaLive}
166
+ aria-atomic="true"
167
+ className={`alert alert-${type}`}
168
+ >
169
+ <span className="sr-only">{type}:</span>
170
+ <div className="alert-content">{children}</div>
171
+ {dismissible && (
172
+ <button
173
+ type="button"
174
+ aria-label={`Dismiss ${type} message`}
175
+ onClick={onDismiss}
176
+ className="alert-dismiss"
177
+ >
178
+ <span aria-hidden="true">&times;</span>
179
+ </button>
180
+ )}
181
+ </div>
182
+ );
183
+ }
184
+
185
+ // components/accessible/Dialog.tsx
186
+ import React, { useRef, useEffect } from "react";
187
+ import { createPortal } from "react-dom";
188
+
189
+ interface DialogProps {
190
+ isOpen: boolean;
191
+ onClose: () => void;
192
+ title: string;
193
+ children: React.ReactNode;
194
+ }
195
+
196
+ export function Dialog({ isOpen, onClose, title, children }: DialogProps) {
197
+ const dialogRef = useRef<HTMLDivElement>(null);
198
+ const previousFocusRef = useRef<HTMLElement | null>(null);
199
+ const titleId = `dialog-title-${React.useId()}`;
200
+ const descId = `dialog-desc-${React.useId()}`;
201
+
202
+ useEffect(() => {
203
+ if (isOpen) {
204
+ // Store current focus
205
+ previousFocusRef.current = document.activeElement as HTMLElement;
206
+
207
+ // Focus the dialog
208
+ dialogRef.current?.focus();
209
+
210
+ // Prevent body scroll
211
+ document.body.style.overflow = "hidden";
212
+ }
213
+
214
+ return () => {
215
+ document.body.style.overflow = "";
216
+ // Return focus on close
217
+ previousFocusRef.current?.focus();
218
+ };
219
+ }, [isOpen]);
220
+
221
+ // Trap focus within dialog
222
+ const handleKeyDown = (e: React.KeyboardEvent) => {
223
+ if (e.key === "Escape") {
224
+ onClose();
225
+ return;
226
+ }
227
+
228
+ if (e.key !== "Tab") return;
229
+
230
+ const focusableElements = dialogRef.current?.querySelectorAll(
231
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
232
+ );
233
+
234
+ if (!focusableElements?.length) return;
235
+
236
+ const firstElement = focusableElements[0] as HTMLElement;
237
+ const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
238
+
239
+ if (e.shiftKey && document.activeElement === firstElement) {
240
+ e.preventDefault();
241
+ lastElement.focus();
242
+ } else if (!e.shiftKey && document.activeElement === lastElement) {
243
+ e.preventDefault();
244
+ firstElement.focus();
245
+ }
246
+ };
247
+
248
+ if (!isOpen) return null;
249
+
250
+ return createPortal(
251
+ <>
252
+ <div
253
+ className="dialog-backdrop"
254
+ aria-hidden="true"
255
+ onClick={onClose}
256
+ />
257
+ <div
258
+ ref={dialogRef}
259
+ role="dialog"
260
+ aria-modal="true"
261
+ aria-labelledby={titleId}
262
+ aria-describedby={descId}
263
+ tabIndex={-1}
264
+ onKeyDown={handleKeyDown}
265
+ className="dialog"
266
+ >
267
+ <h2 id={titleId}>{title}</h2>
268
+ <div id={descId}>{children}</div>
269
+ <button
270
+ type="button"
271
+ aria-label="Close dialog"
272
+ onClick={onClose}
273
+ className="dialog-close"
274
+ >
275
+ &times;
276
+ </button>
277
+ </div>
278
+ </>,
279
+ document.body
280
+ );
281
+ }
282
+
283
+ // components/accessible/Tabs.tsx
284
+ import React, { useState, useRef } from "react";
285
+
286
+ interface Tab {
287
+ id: string;
288
+ label: string;
289
+ content: React.ReactNode;
290
+ }
291
+
292
+ interface TabsProps {
293
+ tabs: Tab[];
294
+ defaultTab?: string;
295
+ }
296
+
297
+ export function Tabs({ tabs, defaultTab }: TabsProps) {
298
+ const [activeTab, setActiveTab] = useState(defaultTab || tabs[0]?.id);
299
+ const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
300
+
301
+ const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
302
+ let newIndex: number | null = null;
303
+
304
+ switch (e.key) {
305
+ case "ArrowLeft":
306
+ newIndex = index === 0 ? tabs.length - 1 : index - 1;
307
+ break;
308
+ case "ArrowRight":
309
+ newIndex = index === tabs.length - 1 ? 0 : index + 1;
310
+ break;
311
+ case "Home":
312
+ newIndex = 0;
313
+ break;
314
+ case "End":
315
+ newIndex = tabs.length - 1;
316
+ break;
317
+ default:
318
+ return;
319
+ }
320
+
321
+ e.preventDefault();
322
+ const newTab = tabs[newIndex];
323
+ setActiveTab(newTab.id);
324
+ tabRefs.current.get(newTab.id)?.focus();
325
+ };
326
+
327
+ return (
328
+ <div className="tabs">
329
+ <div role="tablist" aria-label="Content tabs">
330
+ {tabs.map((tab, index) => (
331
+ <button
332
+ key={tab.id}
333
+ ref={(el) => el && tabRefs.current.set(tab.id, el)}
334
+ role="tab"
335
+ id={`tab-${tab.id}`}
336
+ aria-selected={activeTab === tab.id}
337
+ aria-controls={`panel-${tab.id}`}
338
+ tabIndex={activeTab === tab.id ? 0 : -1}
339
+ onClick={() => setActiveTab(tab.id)}
340
+ onKeyDown={(e) => handleKeyDown(e, index)}
341
+ className={`tab ${activeTab === tab.id ? "active" : ""}`}
342
+ >
343
+ {tab.label}
344
+ </button>
345
+ ))}
346
+ </div>
347
+ {tabs.map((tab) => (
348
+ <div
349
+ key={tab.id}
350
+ role="tabpanel"
351
+ id={`panel-${tab.id}`}
352
+ aria-labelledby={`tab-${tab.id}`}
353
+ hidden={activeTab !== tab.id}
354
+ tabIndex={0}
355
+ className="tab-panel"
356
+ >
357
+ {tab.content}
358
+ </div>
359
+ ))}
360
+ </div>
361
+ );
362
+ }
363
+ ```
364
+
365
+ ### 3. Keyboard Navigation
366
+
367
+ ```tsx
368
+ // components/accessible/Menu.tsx
369
+ import React, { useState, useRef, useEffect } from "react";
370
+
371
+ interface MenuItem {
372
+ id: string;
373
+ label: string;
374
+ onClick: () => void;
375
+ disabled?: boolean;
376
+ }
377
+
378
+ interface MenuProps {
379
+ trigger: React.ReactNode;
380
+ items: MenuItem[];
381
+ label: string;
382
+ }
383
+
384
+ export function Menu({ trigger, items, label }: MenuProps) {
385
+ const [isOpen, setIsOpen] = useState(false);
386
+ const [activeIndex, setActiveIndex] = useState(-1);
387
+ const menuRef = useRef<HTMLUListElement>(null);
388
+ const triggerRef = useRef<HTMLButtonElement>(null);
389
+ const menuId = `menu-${React.useId()}`;
390
+
391
+ useEffect(() => {
392
+ if (isOpen && activeIndex >= 0) {
393
+ const item = menuRef.current?.children[activeIndex] as HTMLElement;
394
+ item?.focus();
395
+ }
396
+ }, [isOpen, activeIndex]);
397
+
398
+ // Close on outside click
399
+ useEffect(() => {
400
+ const handleClickOutside = (e: MouseEvent) => {
401
+ if (
402
+ menuRef.current &&
403
+ !menuRef.current.contains(e.target as Node) &&
404
+ !triggerRef.current?.contains(e.target as Node)
405
+ ) {
406
+ setIsOpen(false);
407
+ }
408
+ };
409
+
410
+ document.addEventListener("mousedown", handleClickOutside);
411
+ return () => document.removeEventListener("mousedown", handleClickOutside);
412
+ }, []);
413
+
414
+ const handleTriggerKeyDown = (e: React.KeyboardEvent) => {
415
+ switch (e.key) {
416
+ case "Enter":
417
+ case " ":
418
+ case "ArrowDown":
419
+ e.preventDefault();
420
+ setIsOpen(true);
421
+ setActiveIndex(0);
422
+ break;
423
+ case "ArrowUp":
424
+ e.preventDefault();
425
+ setIsOpen(true);
426
+ setActiveIndex(items.length - 1);
427
+ break;
428
+ }
429
+ };
430
+
431
+ const handleMenuKeyDown = (e: React.KeyboardEvent) => {
432
+ switch (e.key) {
433
+ case "Escape":
434
+ setIsOpen(false);
435
+ triggerRef.current?.focus();
436
+ break;
437
+ case "ArrowDown":
438
+ e.preventDefault();
439
+ setActiveIndex((prev) =>
440
+ prev === items.length - 1 ? 0 : prev + 1
441
+ );
442
+ break;
443
+ case "ArrowUp":
444
+ e.preventDefault();
445
+ setActiveIndex((prev) =>
446
+ prev === 0 ? items.length - 1 : prev - 1
447
+ );
448
+ break;
449
+ case "Home":
450
+ e.preventDefault();
451
+ setActiveIndex(0);
452
+ break;
453
+ case "End":
454
+ e.preventDefault();
455
+ setActiveIndex(items.length - 1);
456
+ break;
457
+ case "Tab":
458
+ setIsOpen(false);
459
+ break;
460
+ }
461
+ };
462
+
463
+ const handleItemClick = (item: MenuItem) => {
464
+ if (!item.disabled) {
465
+ item.onClick();
466
+ setIsOpen(false);
467
+ triggerRef.current?.focus();
468
+ }
469
+ };
470
+
471
+ return (
472
+ <div className="menu-container">
473
+ <button
474
+ ref={triggerRef}
475
+ aria-haspopup="menu"
476
+ aria-expanded={isOpen}
477
+ aria-controls={menuId}
478
+ onClick={() => setIsOpen(!isOpen)}
479
+ onKeyDown={handleTriggerKeyDown}
480
+ className="menu-trigger"
481
+ >
482
+ {trigger}
483
+ <span className="sr-only">{label}</span>
484
+ </button>
485
+
486
+ {isOpen && (
487
+ <ul
488
+ ref={menuRef}
489
+ id={menuId}
490
+ role="menu"
491
+ aria-label={label}
492
+ onKeyDown={handleMenuKeyDown}
493
+ className="menu-list"
494
+ >
495
+ {items.map((item, index) => (
496
+ <li
497
+ key={item.id}
498
+ role="menuitem"
499
+ tabIndex={activeIndex === index ? 0 : -1}
500
+ aria-disabled={item.disabled}
501
+ onClick={() => handleItemClick(item)}
502
+ onKeyDown={(e) => {
503
+ if (e.key === "Enter" || e.key === " ") {
504
+ e.preventDefault();
505
+ handleItemClick(item);
506
+ }
507
+ }}
508
+ className={`menu-item ${item.disabled ? "disabled" : ""}`}
509
+ >
510
+ {item.label}
511
+ </li>
512
+ ))}
513
+ </ul>
514
+ )}
515
+ </div>
516
+ );
517
+ }
518
+
519
+ // hooks/useRovingTabIndex.ts
520
+ import { useState, useCallback } from "react";
521
+
522
+ export function useRovingTabIndex<T>(items: T[]) {
523
+ const [activeIndex, setActiveIndex] = useState(0);
524
+
525
+ const handleKeyDown = useCallback(
526
+ (e: React.KeyboardEvent, index: number) => {
527
+ let newIndex: number | null = null;
528
+
529
+ switch (e.key) {
530
+ case "ArrowDown":
531
+ case "ArrowRight":
532
+ newIndex = index === items.length - 1 ? 0 : index + 1;
533
+ break;
534
+ case "ArrowUp":
535
+ case "ArrowLeft":
536
+ newIndex = index === 0 ? items.length - 1 : index - 1;
537
+ break;
538
+ case "Home":
539
+ newIndex = 0;
540
+ break;
541
+ case "End":
542
+ newIndex = items.length - 1;
543
+ break;
544
+ default:
545
+ return;
546
+ }
547
+
548
+ e.preventDefault();
549
+ setActiveIndex(newIndex);
550
+ },
551
+ [items.length]
552
+ );
553
+
554
+ const getTabIndex = useCallback(
555
+ (index: number) => (index === activeIndex ? 0 : -1),
556
+ [activeIndex]
557
+ );
558
+
559
+ return { activeIndex, setActiveIndex, handleKeyDown, getTabIndex };
560
+ }
561
+ ```
562
+
563
+ ### 4. Form Accessibility
564
+
565
+ ```tsx
566
+ // components/accessible/FormField.tsx
567
+ import React from "react";
568
+
569
+ interface FormFieldProps {
570
+ id: string;
571
+ label: string;
572
+ type?: string;
573
+ required?: boolean;
574
+ error?: string;
575
+ hint?: string;
576
+ children?: React.ReactNode;
577
+ }
578
+
579
+ export function FormField({
580
+ id,
581
+ label,
582
+ type = "text",
583
+ required,
584
+ error,
585
+ hint,
586
+ children,
587
+ }: FormFieldProps) {
588
+ const hintId = hint ? `${id}-hint` : undefined;
589
+ const errorId = error ? `${id}-error` : undefined;
590
+ const describedBy = [hintId, errorId].filter(Boolean).join(" ") || undefined;
591
+
592
+ return (
593
+ <div className={`form-field ${error ? "has-error" : ""}`}>
594
+ <label htmlFor={id}>
595
+ {label}
596
+ {required && (
597
+ <span aria-hidden="true" className="required">*</span>
598
+ )}
599
+ {required && <span className="sr-only">(required)</span>}
600
+ </label>
601
+
602
+ {hint && (
603
+ <p id={hintId} className="form-hint">
604
+ {hint}
605
+ </p>
606
+ )}
607
+
608
+ {children || (
609
+ <input
610
+ id={id}
611
+ type={type}
612
+ required={required}
613
+ aria-required={required}
614
+ aria-invalid={!!error}
615
+ aria-describedby={describedBy}
616
+ />
617
+ )}
618
+
619
+ {error && (
620
+ <p id={errorId} role="alert" className="form-error">
621
+ <span className="sr-only">Error: </span>
622
+ {error}
623
+ </p>
624
+ )}
625
+ </div>
626
+ );
627
+ }
628
+
629
+ // components/accessible/Checkbox.tsx
630
+ interface CheckboxProps {
631
+ id: string;
632
+ label: string;
633
+ checked: boolean;
634
+ onChange: (checked: boolean) => void;
635
+ description?: string;
636
+ }
637
+
638
+ export function Checkbox({
639
+ id,
640
+ label,
641
+ checked,
642
+ onChange,
643
+ description,
644
+ }: CheckboxProps) {
645
+ const descId = description ? `${id}-desc` : undefined;
646
+
647
+ return (
648
+ <div className="checkbox-field">
649
+ <input
650
+ type="checkbox"
651
+ id={id}
652
+ checked={checked}
653
+ onChange={(e) => onChange(e.target.checked)}
654
+ aria-describedby={descId}
655
+ />
656
+ <label htmlFor={id}>{label}</label>
657
+ {description && (
658
+ <p id={descId} className="checkbox-description">
659
+ {description}
660
+ </p>
661
+ )}
662
+ </div>
663
+ );
664
+ }
665
+
666
+ // components/accessible/RadioGroup.tsx
667
+ interface RadioOption {
668
+ value: string;
669
+ label: string;
670
+ description?: string;
671
+ }
672
+
673
+ interface RadioGroupProps {
674
+ name: string;
675
+ legend: string;
676
+ options: RadioOption[];
677
+ value: string;
678
+ onChange: (value: string) => void;
679
+ required?: boolean;
680
+ error?: string;
681
+ }
682
+
683
+ export function RadioGroup({
684
+ name,
685
+ legend,
686
+ options,
687
+ value,
688
+ onChange,
689
+ required,
690
+ error,
691
+ }: RadioGroupProps) {
692
+ const errorId = error ? `${name}-error` : undefined;
693
+
694
+ return (
695
+ <fieldset
696
+ aria-required={required}
697
+ aria-invalid={!!error}
698
+ aria-describedby={errorId}
699
+ >
700
+ <legend>
701
+ {legend}
702
+ {required && <span className="sr-only">(required)</span>}
703
+ </legend>
704
+
705
+ {options.map((option) => {
706
+ const optionId = `${name}-${option.value}`;
707
+ const descId = option.description ? `${optionId}-desc` : undefined;
708
+
709
+ return (
710
+ <div key={option.value} className="radio-option">
711
+ <input
712
+ type="radio"
713
+ id={optionId}
714
+ name={name}
715
+ value={option.value}
716
+ checked={value === option.value}
717
+ onChange={(e) => onChange(e.target.value)}
718
+ aria-describedby={descId}
719
+ />
720
+ <label htmlFor={optionId}>{option.label}</label>
721
+ {option.description && (
722
+ <p id={descId} className="radio-description">
723
+ {option.description}
724
+ </p>
725
+ )}
726
+ </div>
727
+ );
728
+ })}
729
+
730
+ {error && (
731
+ <p id={errorId} role="alert" className="form-error">
732
+ {error}
733
+ </p>
734
+ )}
735
+ </fieldset>
736
+ );
737
+ }
23
738
  ```
24
739
 
25
- ## Keyboard Navigation
740
+ ### 5. Live Regions
741
+
26
742
  ```tsx
27
- function handleKeyDown(e: KeyboardEvent) {
28
- if (e.key === 'Enter' || e.key === ' ') {
29
- activate();
30
- }
31
- if (e.key === 'Escape') {
32
- close();
33
- }
743
+ // components/accessible/LiveRegion.tsx
744
+ import React from "react";
745
+
746
+ interface LiveRegionProps {
747
+ message: string;
748
+ type?: "polite" | "assertive";
749
+ atomic?: boolean;
750
+ }
751
+
752
+ export function LiveRegion({
753
+ message,
754
+ type = "polite",
755
+ atomic = true,
756
+ }: LiveRegionProps) {
757
+ return (
758
+ <div
759
+ role={type === "assertive" ? "alert" : "status"}
760
+ aria-live={type}
761
+ aria-atomic={atomic}
762
+ className="sr-only"
763
+ >
764
+ {message}
765
+ </div>
766
+ );
767
+ }
768
+
769
+ // hooks/useAnnounce.ts
770
+ import { useState, useCallback, useRef, useEffect } from "react";
771
+
772
+ export function useAnnounce() {
773
+ const [announcement, setAnnouncement] = useState("");
774
+ const timeoutRef = useRef<NodeJS.Timeout>();
775
+
776
+ const announce = useCallback((message: string, duration = 1000) => {
777
+ // Clear previous announcement
778
+ setAnnouncement("");
779
+
780
+ // Small delay to ensure screen reader picks up change
781
+ requestAnimationFrame(() => {
782
+ setAnnouncement(message);
783
+ });
784
+
785
+ // Clear after duration
786
+ if (timeoutRef.current) {
787
+ clearTimeout(timeoutRef.current);
788
+ }
789
+ timeoutRef.current = setTimeout(() => {
790
+ setAnnouncement("");
791
+ }, duration);
792
+ }, []);
793
+
794
+ useEffect(() => {
795
+ return () => {
796
+ if (timeoutRef.current) {
797
+ clearTimeout(timeoutRef.current);
798
+ }
799
+ };
800
+ }, []);
801
+
802
+ return { announcement, announce };
803
+ }
804
+
805
+ // Usage with loading states
806
+ function SearchResults() {
807
+ const { announcement, announce } = useAnnounce();
808
+ const [results, setResults] = useState([]);
809
+ const [loading, setLoading] = useState(false);
810
+
811
+ const handleSearch = async (query: string) => {
812
+ setLoading(true);
813
+ announce("Searching...");
814
+
815
+ const data = await fetchResults(query);
816
+ setResults(data);
817
+ setLoading(false);
818
+
819
+ announce(`Found ${data.length} results`);
820
+ };
821
+
822
+ return (
823
+ <div>
824
+ <LiveRegion message={announcement} />
825
+ {/* Search UI */}
826
+ </div>
827
+ );
34
828
  }
35
829
  ```
36
830
 
37
- ## Focus Management
831
+ ### 6. Color and Contrast
832
+
833
+ ```css
834
+ /* Color contrast guidelines */
835
+
836
+ /* Text colors - minimum 4.5:1 for normal text, 3:1 for large text */
837
+ :root {
838
+ --color-text-primary: #1a1a1a; /* High contrast */
839
+ --color-text-secondary: #4a4a4a; /* 4.5:1 minimum */
840
+ --color-text-disabled: #767676; /* 4.5:1 on white */
841
+ --color-background: #ffffff;
842
+
843
+ /* Focus indicators - minimum 3:1 against adjacent colors */
844
+ --color-focus: #005fcc;
845
+ --color-focus-offset: #ffffff;
846
+
847
+ /* Error states - don't rely on color alone */
848
+ --color-error: #d32f2f;
849
+ --color-error-bg: #ffebee;
850
+
851
+ /* Success states */
852
+ --color-success: #2e7d32;
853
+ --color-success-bg: #e8f5e9;
854
+ }
855
+
856
+ /* Don't rely on color alone - use icons and text */
857
+ .status-indicator {
858
+ display: flex;
859
+ align-items: center;
860
+ gap: 0.5rem;
861
+ }
862
+
863
+ .status-indicator::before {
864
+ content: "";
865
+ width: 8px;
866
+ height: 8px;
867
+ border-radius: 50%;
868
+ }
869
+
870
+ .status-indicator.success::before {
871
+ background-color: var(--color-success);
872
+ }
873
+
874
+ .status-indicator.error::before {
875
+ background-color: var(--color-error);
876
+ }
877
+
878
+ /* Link styling - don't rely on color alone */
879
+ a {
880
+ color: var(--color-focus);
881
+ text-decoration: underline;
882
+ }
883
+
884
+ a:hover,
885
+ a:focus {
886
+ text-decoration-thickness: 2px;
887
+ }
888
+
889
+ /* Form validation - use icons alongside color */
890
+ .input-error {
891
+ border-color: var(--color-error);
892
+ border-width: 2px;
893
+ }
894
+
895
+ .input-error + .error-icon {
896
+ display: block;
897
+ color: var(--color-error);
898
+ }
899
+ ```
900
+
901
+ ### 7. Testing Accessibility
902
+
903
+ ```tsx
904
+ // tests/accessibility.test.tsx
905
+ import { render, screen } from "@testing-library/react";
906
+ import userEvent from "@testing-library/user-event";
907
+ import { axe, toHaveNoViolations } from "jest-axe";
908
+
909
+ expect.extend(toHaveNoViolations);
910
+
911
+ describe("Accessibility Tests", () => {
912
+ describe("Button", () => {
913
+ it("should have no accessibility violations", async () => {
914
+ const { container } = render(
915
+ <button onClick={() => {}}>Click me</button>
916
+ );
917
+ const results = await axe(container);
918
+ expect(results).toHaveNoViolations();
919
+ });
920
+
921
+ it("should be keyboard accessible", async () => {
922
+ const user = userEvent.setup();
923
+ const handleClick = jest.fn();
924
+
925
+ render(<button onClick={handleClick}>Click me</button>);
926
+
927
+ const button = screen.getByRole("button");
928
+ button.focus();
929
+ expect(button).toHaveFocus();
930
+
931
+ await user.keyboard("{Enter}");
932
+ expect(handleClick).toHaveBeenCalledTimes(1);
933
+
934
+ await user.keyboard(" ");
935
+ expect(handleClick).toHaveBeenCalledTimes(2);
936
+ });
937
+ });
938
+
939
+ describe("Form", () => {
940
+ it("should have proper labels", () => {
941
+ render(
942
+ <form>
943
+ <label htmlFor="email">Email</label>
944
+ <input id="email" type="email" />
945
+ </form>
946
+ );
947
+
948
+ expect(screen.getByLabelText("Email")).toBeInTheDocument();
949
+ });
950
+
951
+ it("should announce errors to screen readers", async () => {
952
+ render(
953
+ <div>
954
+ <input aria-invalid="true" aria-describedby="error" />
955
+ <p id="error" role="alert">Invalid email</p>
956
+ </div>
957
+ );
958
+
959
+ expect(screen.getByRole("alert")).toHaveTextContent("Invalid email");
960
+ });
961
+ });
962
+
963
+ describe("Dialog", () => {
964
+ it("should trap focus", async () => {
965
+ const user = userEvent.setup();
966
+
967
+ render(
968
+ <div role="dialog" aria-modal="true">
969
+ <button>First</button>
970
+ <button>Second</button>
971
+ <button>Third</button>
972
+ </div>
973
+ );
974
+
975
+ const buttons = screen.getAllByRole("button");
976
+ buttons[0].focus();
977
+
978
+ // Tab through all buttons
979
+ await user.tab();
980
+ expect(buttons[1]).toHaveFocus();
981
+
982
+ await user.tab();
983
+ expect(buttons[2]).toHaveFocus();
984
+ });
985
+ });
986
+ });
987
+
988
+ // Cypress accessibility tests
989
+ // cypress/e2e/accessibility.cy.ts
990
+ describe("Accessibility", () => {
991
+ beforeEach(() => {
992
+ cy.visit("/");
993
+ cy.injectAxe();
994
+ });
995
+
996
+ it("should have no accessibility violations on homepage", () => {
997
+ cy.checkA11y();
998
+ });
999
+
1000
+ it("should have no violations after interaction", () => {
1001
+ cy.get("button").first().click();
1002
+ cy.checkA11y();
1003
+ });
1004
+
1005
+ it("should be navigable by keyboard", () => {
1006
+ cy.get("body").tab();
1007
+ cy.focused().should("have.attr", "href");
1008
+ });
1009
+ });
1010
+ ```
1011
+
1012
+ ## Use Cases
1013
+
1014
+ ### Accessible Data Table
1015
+
38
1016
  ```tsx
39
- const ref = useRef<HTMLButtonElement>(null);
1017
+ // components/accessible/DataTable.tsx
1018
+ interface Column<T> {
1019
+ key: keyof T;
1020
+ header: string;
1021
+ render?: (value: T[keyof T], row: T) => React.ReactNode;
1022
+ }
1023
+
1024
+ interface DataTableProps<T> {
1025
+ data: T[];
1026
+ columns: Column<T>[];
1027
+ caption: string;
1028
+ sortable?: boolean;
1029
+ }
1030
+
1031
+ export function DataTable<T extends { id: string }>({
1032
+ data,
1033
+ columns,
1034
+ caption,
1035
+ sortable,
1036
+ }: DataTableProps<T>) {
1037
+ const [sortColumn, setSortColumn] = useState<keyof T | null>(null);
1038
+ const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
40
1039
 
41
- useEffect(() => {
42
- if (isOpen) ref.current?.focus();
43
- }, [isOpen]);
1040
+ return (
1041
+ <table>
1042
+ <caption>{caption}</caption>
1043
+ <thead>
1044
+ <tr>
1045
+ {columns.map((col) => (
1046
+ <th
1047
+ key={String(col.key)}
1048
+ scope="col"
1049
+ aria-sort={
1050
+ sortable && sortColumn === col.key
1051
+ ? sortDirection === "asc"
1052
+ ? "ascending"
1053
+ : "descending"
1054
+ : undefined
1055
+ }
1056
+ >
1057
+ {sortable ? (
1058
+ <button
1059
+ onClick={() => handleSort(col.key)}
1060
+ aria-label={`Sort by ${col.header}`}
1061
+ >
1062
+ {col.header}
1063
+ </button>
1064
+ ) : (
1065
+ col.header
1066
+ )}
1067
+ </th>
1068
+ ))}
1069
+ </tr>
1070
+ </thead>
1071
+ <tbody>
1072
+ {data.map((row) => (
1073
+ <tr key={row.id}>
1074
+ {columns.map((col, index) => (
1075
+ <td
1076
+ key={String(col.key)}
1077
+ headers={`header-${String(col.key)}`}
1078
+ >
1079
+ {col.render
1080
+ ? col.render(row[col.key], row)
1081
+ : String(row[col.key])}
1082
+ </td>
1083
+ ))}
1084
+ </tr>
1085
+ ))}
1086
+ </tbody>
1087
+ </table>
1088
+ );
1089
+ }
44
1090
  ```
45
1091
 
46
- ## Checklist
47
- - [ ] Color contrast 4.5:1+
48
- - [ ] Keyboard accessible
49
- - [ ] Focus visible
50
- - [ ] Alt text on images
51
- - [ ] Form labels
52
- - [ ] Skip links
1092
+ ## Best Practices
1093
+
1094
+ ### Do's
1095
+
1096
+ - Use semantic HTML elements
1097
+ - Provide text alternatives for images
1098
+ - Ensure keyboard accessibility
1099
+ - Use sufficient color contrast (4.5:1)
1100
+ - Associate labels with form controls
1101
+ - Provide skip links for navigation
1102
+ - Test with screen readers
1103
+ - Use ARIA only when needed
1104
+ - Manage focus appropriately
1105
+ - Announce dynamic content changes
1106
+
1107
+ ### Don'ts
1108
+
1109
+ - Don't remove focus outlines without replacement
1110
+ - Don't rely on color alone to convey information
1111
+ - Don't use placeholder as label
1112
+ - Don't trap keyboard focus unintentionally
1113
+ - Don't auto-play media with sound
1114
+ - Don't use ARIA when HTML suffices
1115
+ - Don't hide content from screen readers unnecessarily
1116
+ - Don't create keyboard traps
1117
+ - Don't use very small touch targets
1118
+ - Don't ignore accessibility in testing
1119
+
1120
+ ## References
1121
+
1122
+ - [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
1123
+ - [WAI-ARIA Practices](https://www.w3.org/WAI/ARIA/apg/)
1124
+ - [MDN Accessibility](https://developer.mozilla.org/en-US/docs/Web/Accessibility)
1125
+ - [A11y Project](https://www.a11yproject.com/)
1126
+ - [Inclusive Components](https://inclusive-components.design/)