@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.
- package/README.md +92 -0
- package/docs/README.md +325 -0
- package/docs/guides/accessibility.md +764 -0
- package/docs/guides/architecture.md +705 -0
- package/docs/guides/composition.md +688 -0
- package/docs/guides/css-variables.md +522 -0
- package/docs/guides/storybook.md +828 -0
- package/docs/guides/testing.md +817 -0
- package/docs/testing/focus-indicator-testing.md +437 -0
- package/libs/{chunk-7XPFW7CB.js → chunk-43TK2ICH.js} +2 -2
- package/libs/chunk-5PJYLVFY.cjs +17 -0
- package/libs/chunk-5PJYLVFY.cjs.map +1 -0
- package/libs/chunk-E4OSROCA.cjs +17 -0
- package/libs/chunk-E4OSROCA.cjs.map +1 -0
- package/libs/chunk-KVKQLRJG.js +10 -0
- package/libs/chunk-KVKQLRJG.js.map +1 -0
- package/libs/{chunk-QVW6W76L.cjs → chunk-MGPWZRBX.cjs} +3 -3
- package/libs/chunk-NNTBIHSD.js +8 -0
- package/libs/chunk-NNTBIHSD.js.map +1 -0
- package/libs/{chunk-X3JCTEPD.js → chunk-QKHPHMG2.js} +2 -2
- package/libs/{chunk-T4T6GWYQ.cjs → chunk-R7NLLZU2.cjs} +3 -3
- package/libs/{chunk-X5LGFCWG.js → chunk-UJAQVHWC.js} +3 -3
- package/libs/{chunk-DKTHCQ5P.cjs → chunk-X5RKCLDC.cjs} +3 -3
- package/libs/components/breadcrumbs/breadcrumb.cjs +5 -5
- package/libs/components/breadcrumbs/breadcrumb.d.cts +1 -1
- package/libs/components/breadcrumbs/breadcrumb.d.ts +1 -1
- package/libs/components/breadcrumbs/breadcrumb.js +2 -2
- package/libs/components/button.cjs +3 -3
- package/libs/components/button.d.cts +1 -1
- package/libs/components/button.d.ts +1 -1
- package/libs/components/button.js +1 -1
- package/libs/components/buttons/button.css +1 -1
- package/libs/components/buttons/button.css.map +1 -1
- package/libs/components/buttons/button.min.css +2 -2
- package/libs/components/dialog/dialog.cjs +4 -4
- package/libs/components/dialog/dialog.js +2 -2
- package/libs/components/icons/icon.d.cts +32 -32
- package/libs/components/icons/icon.d.ts +32 -32
- package/libs/components/link/link.cjs +11 -3
- package/libs/components/link/link.d.cts +131 -3
- package/libs/components/link/link.d.ts +131 -3
- package/libs/components/link/link.js +1 -1
- package/libs/components/list/list.css +1 -1
- package/libs/components/list/list.min.css +1 -1
- package/libs/components/modal.cjs +3 -3
- package/libs/components/modal.js +2 -2
- package/libs/hooks.cjs +3 -3
- package/libs/hooks.d.cts +1 -1
- package/libs/hooks.d.ts +1 -1
- package/libs/hooks.js +2 -2
- package/libs/index.cjs +12 -12
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/libs/index.d.cts +237 -2
- package/libs/index.d.ts +237 -2
- package/libs/index.js +5 -5
- package/package.json +4 -3
- package/src/components/README.mdx +1 -1
- package/src/components/breadcrumbs/breadcrumb.test.tsx +1 -2
- package/src/components/buttons/README.mdx +19 -9
- package/src/components/buttons/button.scss +5 -0
- package/src/components/buttons/button.stories.tsx +8 -5
- package/src/components/buttons/button.tsx +19 -15
- package/src/components/cards/card.stories.tsx +1 -1
- package/src/components/details/details.stories.tsx +1 -1
- package/src/components/form/form.stories.tsx +1 -1
- package/src/components/form/input.stories.tsx +1 -1
- package/src/components/form/select.stories.tsx +1 -1
- package/src/components/heading/README.mdx +292 -0
- package/src/components/icons/icon.stories.tsx +1 -1
- package/src/components/link/link.stories.tsx +205 -8
- package/src/components/link/link.test.tsx +1 -1
- package/src/components/link/link.tsx +22 -0
- package/src/components/link/link.types.ts +11 -3
- package/src/components/list/list.scss +1 -1
- package/src/components/nav/nav.stories.tsx +1 -1
- package/src/components/ui.stories.tsx +53 -19
- package/src/docs/accessibility.mdx +484 -0
- package/src/docs/composition.mdx +549 -0
- package/src/docs/css-variables.mdx +380 -0
- package/src/docs/fpkit-developer.mdx +623 -0
- package/src/introduction.mdx +356 -0
- package/src/styles/buttons/button.css +4 -0
- package/src/styles/buttons/button.css.map +1 -1
- package/src/styles/index.css +9 -3
- package/src/styles/index.css.map +1 -1
- package/src/styles/list/list.css +1 -1
- package/src/styles/utilities/_disabled.scss +5 -4
- package/libs/chunk-33PNJ4LO.cjs +0 -15
- package/libs/chunk-33PNJ4LO.cjs.map +0 -1
- package/libs/chunk-GT77BX4L.cjs +0 -17
- package/libs/chunk-GT77BX4L.cjs.map +0 -1
- package/libs/chunk-OVWLQYMK.js +0 -10
- package/libs/chunk-OVWLQYMK.js.map +0 -1
- package/libs/chunk-UEPAWMDF.js +0 -8
- package/libs/chunk-UEPAWMDF.js.map +0 -1
- package/libs/link-5192f411.d.ts +0 -323
- /package/libs/{chunk-7XPFW7CB.js.map → chunk-43TK2ICH.js.map} +0 -0
- /package/libs/{chunk-QVW6W76L.cjs.map → chunk-MGPWZRBX.cjs.map} +0 -0
- /package/libs/{chunk-X3JCTEPD.js.map → chunk-QKHPHMG2.js.map} +0 -0
- /package/libs/{chunk-T4T6GWYQ.cjs.map → chunk-R7NLLZU2.cjs.map} +0 -0
- /package/libs/{chunk-X5LGFCWG.js.map → chunk-UJAQVHWC.js.map} +0 -0
- /package/libs/{chunk-DKTHCQ5P.cjs.map → chunk-X5RKCLDC.cjs.map} +0 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Guides/Accessibility" />
|
|
4
|
+
|
|
5
|
+
# Accessibility Guide
|
|
6
|
+
|
|
7
|
+
Learn how to maintain **WCAG 2.1 Level AA** compliance when using and composing
|
|
8
|
+
@fpkit/acss components.
|
|
9
|
+
|
|
10
|
+
> **📖 Full Guide:** For comprehensive documentation, see
|
|
11
|
+
> [docs/guides/accessibility.md](https://github.com/shawn-sandy/acss/blob/main/packages/fpkit/docs/guides/accessibility.md)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick Reference
|
|
16
|
+
|
|
17
|
+
fpkit components are **WCAG 2.1 Level AA compliant** by default. Your
|
|
18
|
+
responsibility is to maintain accessibility when:
|
|
19
|
+
|
|
20
|
+
- Composing custom components
|
|
21
|
+
- Customizing with CSS variables
|
|
22
|
+
- Adding custom interactions
|
|
23
|
+
- Creating forms and interactive elements
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Core Principles
|
|
28
|
+
|
|
29
|
+
### 1. Semantic HTML First
|
|
30
|
+
|
|
31
|
+
fpkit components use appropriate semantic elements:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// ✅ Good - uses semantic button
|
|
35
|
+
<Button onClick={handleClick}>Click me</Button>
|
|
36
|
+
// Renders: <button type="button">Click me</button>
|
|
37
|
+
|
|
38
|
+
// ✅ Good - uses semantic link
|
|
39
|
+
<Button as="a" href="/page">Navigate</Button>
|
|
40
|
+
// Renders: <a href="/page">Navigate</a>
|
|
41
|
+
|
|
42
|
+
// ❌ Bad - div as button
|
|
43
|
+
<div onClick={handleClick}>Click me</div>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. Keyboard Navigation
|
|
47
|
+
|
|
48
|
+
All fpkit interactive components support:
|
|
49
|
+
|
|
50
|
+
- **Tab**: Navigate between elements
|
|
51
|
+
- **Enter/Space**: Activate buttons and links
|
|
52
|
+
- **Escape**: Close modals and dialogs
|
|
53
|
+
- **Arrow keys**: Navigate menus and lists
|
|
54
|
+
|
|
55
|
+
**Test:** Try navigating your app using only the keyboard!
|
|
56
|
+
|
|
57
|
+
### 3. Focus Management
|
|
58
|
+
|
|
59
|
+
fpkit provides built-in focus indicators:
|
|
60
|
+
|
|
61
|
+
```css
|
|
62
|
+
/* Customize focus styles */
|
|
63
|
+
:root {
|
|
64
|
+
--btn-focus-outline: 2px solid #0066cc;
|
|
65
|
+
--btn-focus-outline-offset: 2px;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Screen Reader Support
|
|
70
|
+
|
|
71
|
+
fpkit includes proper ARIA attributes. Your job is to add context:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
// ✅ Good - descriptive label
|
|
75
|
+
<Button aria-label="Close dialog">
|
|
76
|
+
<Icon name="close" />
|
|
77
|
+
</Button>
|
|
78
|
+
|
|
79
|
+
// ❌ Bad - no label
|
|
80
|
+
<Button>
|
|
81
|
+
<Icon name="close" />
|
|
82
|
+
</Button>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## ARIA Attributes
|
|
88
|
+
|
|
89
|
+
### Labels
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// Icon-only button needs aria-label
|
|
93
|
+
<Button aria-label="Close dialog">
|
|
94
|
+
<Icon name="close" />
|
|
95
|
+
</Button>
|
|
96
|
+
|
|
97
|
+
// Button with visible text - no aria-label needed
|
|
98
|
+
<Button>
|
|
99
|
+
<Icon name="save" aria-hidden="true" />
|
|
100
|
+
Save
|
|
101
|
+
</Button>
|
|
102
|
+
|
|
103
|
+
// Group with aria-labelledby
|
|
104
|
+
<div role="group" aria-labelledby="filter-heading">
|
|
105
|
+
<h3 id="filter-heading">Filter Options</h3>
|
|
106
|
+
<Button>Apply</Button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
// Additional description
|
|
110
|
+
<Input
|
|
111
|
+
type="email"
|
|
112
|
+
aria-describedby="email-hint"
|
|
113
|
+
/>
|
|
114
|
+
<div id="email-hint">We'll never share your email</div>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### States
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// Expanded state (dropdowns)
|
|
121
|
+
<Button aria-expanded={isOpen} aria-controls="menu-list">
|
|
122
|
+
Menu
|
|
123
|
+
</Button>
|
|
124
|
+
<div id="menu-list" hidden={!isOpen}>
|
|
125
|
+
{/* Menu items */}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
// Toggle state (toolbar)
|
|
129
|
+
<Button aria-pressed={isBold} onClick={toggleBold}>
|
|
130
|
+
<Icon name="bold" aria-hidden="true" />
|
|
131
|
+
Bold
|
|
132
|
+
</Button>
|
|
133
|
+
|
|
134
|
+
// Current page
|
|
135
|
+
<nav aria-label="Pagination">
|
|
136
|
+
<Button aria-current="page">1</Button>
|
|
137
|
+
<Button>2</Button>
|
|
138
|
+
</nav>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Live Regions
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
// Polite announcement (non-urgent)
|
|
145
|
+
<div aria-live="polite" aria-atomic="true">
|
|
146
|
+
{statusMessage}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
// Alert role (urgent messages)
|
|
150
|
+
<Alert variant="error" role="alert">
|
|
151
|
+
Form submission failed.
|
|
152
|
+
</Alert>
|
|
153
|
+
|
|
154
|
+
// Status role (progress updates)
|
|
155
|
+
<div role="status" aria-live="polite">
|
|
156
|
+
Saving... {progress}% complete
|
|
157
|
+
</div>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Button Patterns
|
|
163
|
+
|
|
164
|
+
### Why fpkit Uses aria-disabled
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
// fpkit pattern - stays in tab order
|
|
168
|
+
<Button disabled>Submit</Button>
|
|
169
|
+
// Renders: <button aria-disabled="true">Submit</button>
|
|
170
|
+
|
|
171
|
+
// Benefits:
|
|
172
|
+
// - Keyboard accessible
|
|
173
|
+
// - Can show tooltips
|
|
174
|
+
// - Users understand why disabled
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Button Types
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
// Inside forms - specify type
|
|
181
|
+
<form>
|
|
182
|
+
<Button type="submit">Save</Button>
|
|
183
|
+
<Button type="button">Cancel</Button>
|
|
184
|
+
</form>
|
|
185
|
+
|
|
186
|
+
// Outside forms - defaults to "button"
|
|
187
|
+
<Button onClick={handleAction}>Action</Button>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Form Patterns
|
|
193
|
+
|
|
194
|
+
### Field Labels
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// ✅ Good - explicit association
|
|
198
|
+
<label htmlFor="email">Email Address</label>
|
|
199
|
+
<Input id="email" type="email" />
|
|
200
|
+
|
|
201
|
+
// ✅ Good - implicit association
|
|
202
|
+
<label>
|
|
203
|
+
Email Address
|
|
204
|
+
<Input type="email" />
|
|
205
|
+
</label>
|
|
206
|
+
|
|
207
|
+
// ❌ Bad - no association
|
|
208
|
+
<label>Email Address</label>
|
|
209
|
+
<Input type="email" />
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Error Messages
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
<label htmlFor="password">Password</label>
|
|
216
|
+
<Input
|
|
217
|
+
id="password"
|
|
218
|
+
type="password"
|
|
219
|
+
aria-describedby={hasError ? 'password-error' : undefined}
|
|
220
|
+
aria-invalid={hasError}
|
|
221
|
+
/>
|
|
222
|
+
{hasError && (
|
|
223
|
+
<div id="password-error" role="alert">
|
|
224
|
+
Password must be at least 8 characters
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Required Fields
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
<label htmlFor="name">
|
|
233
|
+
Name <span aria-label="required">*</span>
|
|
234
|
+
</label>
|
|
235
|
+
<Input
|
|
236
|
+
id="name"
|
|
237
|
+
type="text"
|
|
238
|
+
required
|
|
239
|
+
aria-required="true"
|
|
240
|
+
/>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Color Contrast
|
|
246
|
+
|
|
247
|
+
### WCAG AA Requirements
|
|
248
|
+
|
|
249
|
+
- **Normal text** (< 18pt): **4.5:1** contrast ratio
|
|
250
|
+
- **Large text** (≥ 18pt): **3:1** contrast ratio
|
|
251
|
+
- **UI components**: **3:1** contrast ratio
|
|
252
|
+
|
|
253
|
+
### Testing
|
|
254
|
+
|
|
255
|
+
1. **Browser DevTools** → Elements → Accessibility tab
|
|
256
|
+
2. **Storybook a11y addon** → Check violations in Accessibility panel
|
|
257
|
+
3. **[WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)**
|
|
258
|
+
|
|
259
|
+
### Built-in Compliance
|
|
260
|
+
|
|
261
|
+
fpkit components meet requirements by default:
|
|
262
|
+
|
|
263
|
+
```scss
|
|
264
|
+
// Example contrasts
|
|
265
|
+
--btn-primary-bg: #0066cc; // Blue
|
|
266
|
+
--btn-primary-color: #ffffff; // White (7.5:1 - exceeds AA)
|
|
267
|
+
|
|
268
|
+
--alert-error-bg: #f8d7da;
|
|
269
|
+
--alert-error-color: #721c24; // Dark red (9.2:1 - exceeds AA)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**When customizing:** Always test contrast!
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Testing Accessibility
|
|
277
|
+
|
|
278
|
+
### Storybook a11y Addon
|
|
279
|
+
|
|
280
|
+
Storybook includes automatic accessibility testing:
|
|
281
|
+
|
|
282
|
+
1. Open any story in Storybook
|
|
283
|
+
2. Click **Accessibility** tab
|
|
284
|
+
3. Review violations, passes, and incomplete checks
|
|
285
|
+
4. Fix issues before deploying
|
|
286
|
+
|
|
287
|
+
### Automated Testing
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
npm install -D jest-axe
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
import { axe, toHaveNoViolations } from "jest-axe";
|
|
295
|
+
|
|
296
|
+
expect.extend(toHaveNoViolations);
|
|
297
|
+
|
|
298
|
+
it("should not have accessibility violations", async () => {
|
|
299
|
+
const { container } = render(<Button>Click me</Button>);
|
|
300
|
+
const results = await axe(container);
|
|
301
|
+
expect(results).toHaveNoViolations();
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Manual Testing Checklist
|
|
306
|
+
|
|
307
|
+
- [ ] **Keyboard Navigation** - Tab through all interactive elements
|
|
308
|
+
- [ ] **Screen Reader** - Test with VoiceOver (macOS), NVDA (Windows)
|
|
309
|
+
- [ ] **Focus Indicators** - Visible focus states (3:1 contrast ratio)
|
|
310
|
+
- [ ] **Accessible Names** - All interactive elements have labels
|
|
311
|
+
- [ ] **Color Contrast** - Text meets 4.5:1 for normal, 3:1 for large
|
|
312
|
+
- [ ] **Semantic Structure** - Proper heading hierarchy and landmarks
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Interactive Elements
|
|
317
|
+
|
|
318
|
+
### Making Non-Button Elements Clickable
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
import { Card } from '@fpkit/acss'
|
|
322
|
+
|
|
323
|
+
// ❌ Bad - div with onClick
|
|
324
|
+
<Card onClick={handleClick}>Clickable</Card>
|
|
325
|
+
|
|
326
|
+
// ✅ Better - add role and keyboard support
|
|
327
|
+
<Card
|
|
328
|
+
as="article"
|
|
329
|
+
role="button"
|
|
330
|
+
tabIndex={0}
|
|
331
|
+
onClick={handleClick}
|
|
332
|
+
onKeyDown={(e) => {
|
|
333
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
334
|
+
e.preventDefault()
|
|
335
|
+
handleClick(e)
|
|
336
|
+
}
|
|
337
|
+
}}
|
|
338
|
+
aria-label="View article details"
|
|
339
|
+
>
|
|
340
|
+
{/* Card content */}
|
|
341
|
+
</Card>
|
|
342
|
+
|
|
343
|
+
// ✅ Best - use semantic element
|
|
344
|
+
<Card as="a" href="/article/123">
|
|
345
|
+
{/* Card content */}
|
|
346
|
+
</Card>
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Requirements for custom interactive elements:**
|
|
350
|
+
|
|
351
|
+
- `role="button"` - Announces as interactive
|
|
352
|
+
- `tabIndex={0}` - Makes keyboard focusable
|
|
353
|
+
- `onClick` - Mouse interaction
|
|
354
|
+
- `onKeyDown` - Keyboard activation (Enter/Space)
|
|
355
|
+
- `aria-label` - Descriptive label
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Common Mistakes
|
|
360
|
+
|
|
361
|
+
### ❌ Don't
|
|
362
|
+
|
|
363
|
+
- Use `div` or `span` as buttons without proper ARIA
|
|
364
|
+
- Remove focus outlines without alternatives
|
|
365
|
+
- Use `placeholder` as a label replacement
|
|
366
|
+
- Use color alone to convey information
|
|
367
|
+
- Nest interactive elements (`<button>` inside `<a>`)
|
|
368
|
+
- Use positive `tabindex` values (> 0)
|
|
369
|
+
- Auto-play audio/video without controls
|
|
370
|
+
|
|
371
|
+
### ✅ Do
|
|
372
|
+
|
|
373
|
+
- Use semantic HTML elements
|
|
374
|
+
- Provide visible focus indicators
|
|
375
|
+
- Include proper labels for all form controls
|
|
376
|
+
- Use multiple cues (color + icon, color + text)
|
|
377
|
+
- Ensure modals trap focus
|
|
378
|
+
- Use `aria-live="polite"` for non-critical updates
|
|
379
|
+
- Test with keyboard and screen readers
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Composing Accessible Components
|
|
384
|
+
|
|
385
|
+
When composing fpkit components, maintain accessibility:
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
import { Button, Badge } from "@fpkit/acss";
|
|
389
|
+
|
|
390
|
+
// ✅ Good - maintains accessibility
|
|
391
|
+
export const NotificationButton = ({ count, onClick }) => {
|
|
392
|
+
return (
|
|
393
|
+
<Button onClick={onClick} aria-label={`Notifications (${count} unread)`}>
|
|
394
|
+
<Icon name="bell" aria-hidden="true" />
|
|
395
|
+
{count > 0 && <Badge aria-hidden="true">{count}</Badge>}
|
|
396
|
+
</Button>
|
|
397
|
+
);
|
|
398
|
+
};
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Why this works:**
|
|
402
|
+
|
|
403
|
+
- Button is keyboard accessible (inherits from fpkit)
|
|
404
|
+
- `aria-label` provides context for screen readers
|
|
405
|
+
- Visual elements hidden with `aria-hidden`
|
|
406
|
+
- Count announced via `aria-label`
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## WCAG 2.1 Level AA Checklist
|
|
411
|
+
|
|
412
|
+
### Perceivable
|
|
413
|
+
|
|
414
|
+
- [ ] Text alternatives for non-text content
|
|
415
|
+
- [ ] Content presentable in different ways
|
|
416
|
+
- [ ] Sufficient color contrast (4.5:1 normal, 3:1 large)
|
|
417
|
+
- [ ] Text resizable up to 200%
|
|
418
|
+
|
|
419
|
+
### Operable
|
|
420
|
+
|
|
421
|
+
- [ ] All functionality keyboard accessible
|
|
422
|
+
- [ ] No keyboard traps
|
|
423
|
+
- [ ] Users have enough time to interact
|
|
424
|
+
- [ ] No content flashes > 3 times per second
|
|
425
|
+
- [ ] Clear page titles and headings
|
|
426
|
+
- [ ] Visible focus indicators
|
|
427
|
+
- [ ] Multiple navigation methods
|
|
428
|
+
|
|
429
|
+
### Understandable
|
|
430
|
+
|
|
431
|
+
- [ ] Language programmatically determined
|
|
432
|
+
- [ ] Labels for user input
|
|
433
|
+
- [ ] Clear error messages
|
|
434
|
+
- [ ] Consistent navigation
|
|
435
|
+
- [ ] Predictable behavior
|
|
436
|
+
|
|
437
|
+
### Robust
|
|
438
|
+
|
|
439
|
+
- [ ] Valid HTML (no duplicate IDs)
|
|
440
|
+
- [ ] Correct ARIA usage
|
|
441
|
+
- [ ] Compatible with assistive technologies
|
|
442
|
+
- [ ] Status messages announced
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Tools & Resources
|
|
447
|
+
|
|
448
|
+
### Testing Tools
|
|
449
|
+
|
|
450
|
+
| Tool | Type | Use Case |
|
|
451
|
+
| ---------------------------------------------------------------- | ----------------- | ----------------------------- |
|
|
452
|
+
| [axe DevTools](https://www.deque.com/axe/devtools/) | Browser Extension | Real-time violation detection |
|
|
453
|
+
| [WAVE](https://wave.webaim.org/extension/) | Browser Extension | Visual feedback |
|
|
454
|
+
| [Lighthouse](https://developers.google.com/web/tools/lighthouse) | Built-in Chrome | Comprehensive audit |
|
|
455
|
+
| [jest-axe](https://github.com/nickcolley/jest-axe) | Testing Library | Automated unit testing |
|
|
456
|
+
| Storybook a11y addon | Storybook | Interactive testing |
|
|
457
|
+
|
|
458
|
+
### Guidelines
|
|
459
|
+
|
|
460
|
+
- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/)
|
|
461
|
+
- [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
|
|
462
|
+
- [WebAIM Articles](https://webaim.org/articles/)
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Additional Resources
|
|
467
|
+
|
|
468
|
+
- **📖
|
|
469
|
+
[Full Accessibility Guide](https://github.com/shawn-sandy/acss/blob/main/packages/fpkit/docs/guides/accessibility.md)** -
|
|
470
|
+
Comprehensive WCAG patterns
|
|
471
|
+
- **📘
|
|
472
|
+
[Composition Guide](https://github.com/shawn-sandy/acss/blob/main/packages/fpkit/docs/guides/composition.md)** -
|
|
473
|
+
Building accessible compositions
|
|
474
|
+
- **🧪
|
|
475
|
+
[Testing Guide](https://github.com/shawn-sandy/acss/blob/main/packages/fpkit/docs/guides/testing.md)** -
|
|
476
|
+
Accessibility testing strategies
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
**Remember:** Accessibility is not optional. fpkit provides accessible
|
|
481
|
+
components by default - your job is to maintain that accessibility when
|
|
482
|
+
composing and customizing them.
|
|
483
|
+
|
|
484
|
+
**Use the Storybook a11y addon** on every story to catch violations early!
|