@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,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.
|