with
Example
(❌ Bad vs ✅ Good):
<
div
class
=
"
header
"
>
<
span
class
=
"
title
"
>
My App
span
>
<
div
class
=
"
nav
"
>
<
div
class
=
"
nav-item
"
onclick
=
"
navigate
(
)
"
>
Home
div
>
<
div
class
=
"
nav-item
"
onclick
=
"
navigate
(
)
"
>
About
div
>
div
>
div
>
<
header
>
<
h1
>
My App
h1
>
<
nav
aria-label
=
"
Main navigation
"
>
<
ul
>
<
li
>
<
a
href
=
"
/
"
>
Home
a
>
li
>
<
li
>
<
a
href
=
"
/about
"
>
About
a
>
li
>
ul
>
nav
>
</
header
Form Example
:
<
input
type
=
"
text
"
placeholder
=
"
Enter your name
"
<
label
for
=
"
name
"
Name:
</
label
<
input
type
=
"
text
"
id
=
"
name
"
name
=
"
name
"
required
<
label
>
Email:
<
input
type
=
"
email
"
name
=
"
email
"
required
>
</
label
>
Step 2: Implement Keyboard Navigation
Ensure all features are usable without a mouse.
Tasks
:
Move focus with Tab and Shift+Tab
Activate buttons with Enter/Space
Navigate lists/menus with arrow keys
Close modals/dropdowns with ESC
Use
tabindex
appropriately
Decision Criteria
:
Interactive elements →
tabindex="0"
(focusable)
Exclude from focus order →
tabindex="-1"
(programmatic focus only)
Do not change focus order → avoid using
tabindex="1+"
Example
(React Dropdown):
import
React
,
{
useState
,
useRef
,
useEffect
}
from
'react'
;
interface
DropdownProps
{
label
:
string
;
options
:
{
value
:
string
;
label
:
string
}
[
]
;
onChange
:
(
value
:
string
)
=>
void
;
}
function
AccessibleDropdown
(
{
label
,
options
,
onChange
}
:
DropdownProps
)
{
const
[
isOpen
,
setIsOpen
]
=
useState
(
false
)
;
const
[
selectedIndex
,
setSelectedIndex
]
=
useState
(
0
)
;
const
buttonRef
=
useRef
<
HTMLButtonElement
>
(
null
)
;
const
listRef
=
useRef
<
HTMLUListElement
>
(
null
)
;
// Keyboard handler
const
handleKeyDown
=
(
e
:
React
.
KeyboardEvent
)
=>
{
switch
(
e
.
key
)
{
case
'ArrowDown'
:
e
.
preventDefault
(
)
;
if
(
!
isOpen
)
{
setIsOpen
(
true
)
;
}
else
{
setSelectedIndex
(
(
prev
)
=>
(
prev
+
1
)
%
options
.
length
)
;
}
break
;
case
'ArrowUp'
:
e
.
preventDefault
(
)
;
if
(
!
isOpen
)
{
setIsOpen
(
true
)
;
}
else
{
setSelectedIndex
(
(
prev
)
=>
(
prev
-
1
+
options
.
length
)
%
options
.
length
)
;
}
break
;
case
'Enter'
:
case
' '
:
e
.
preventDefault
(
)
;
if
(
isOpen
)
{
onChange
(
options
[
selectedIndex
]
.
value
)
;
setIsOpen
(
false
)
;
buttonRef
.
current
?.
focus
(
)
;
}
else
{
setIsOpen
(
true
)
;
}
break
;
case
'Escape'
:
e
.
preventDefault
(
)
;
setIsOpen
(
false
)
;
buttonRef
.
current
?.
focus
(
)
;
break
;
}
}
;
return
(
<
div className
=
"dropdown"
>
<
button
ref
=
{
buttonRef
}
onClick
=
{
(
)
=>
setIsOpen
(
!
isOpen
)
}
onKeyDown
=
{
handleKeyDown
}
aria
-
haspopup
=
"listbox"
aria
-
expanded
=
{
isOpen
}
aria
-
labelledby
=
"dropdown-label"
>
{
label
}
<
/
button
>
{
isOpen
&&
(
<
ul
ref
=
{
listRef
}
role
=
"listbox"
aria
-
labelledby
=
"dropdown-label"
onKeyDown
=
{
handleKeyDown
}
tabIndex
=
{
-
1
}
>
{
options
.
map
(
(
option
,
index
)
=>
(
<
li
key
=
{
option
.
value
}
role
=
"option"
aria
-
selected
=
{
index
===
selectedIndex
}
onClick
=
{
(
)
=>
{
onChange
(
option
.
value
)
;
setIsOpen
(
false
)
;
}
}
>
{
option
.
label
}
<
/
li
>
)
)
}
<
/
ul
>
)
}
<
/
div
>
)
;
}
Step 3: Add ARIA Attributes
Provide additional context for screen readers.
Tasks
:
aria-label
Define the element's name
aria-labelledby
Reference another element as a label
aria-describedby
Provide additional description
aria-live
Announce dynamic content changes
aria-hidden
Hide from screen readers
Checklist
:
All interactive elements have clear labels
Button purpose is clear (e.g., "Submit form" not "Click")
State change announcements (aria-live)
Decorative images use alt="" or aria-hidden="true"
Example
(Modal):
function
AccessibleModal
(
{
isOpen
,
onClose
,
title
,
children
}
)
{
const
modalRef
=
useRef
<
HTMLDivElement
(
null
)
;
// Focus trap when modal opens
useEffect
(
(
)
=>
{
if
(
isOpen
)
{
modalRef
.
current
?.
focus
(
)
;
}
}
,
[
isOpen
]
)
;
if
(
!
isOpen
)
return
null
;
return
(
<
div
role
=
"
dialog
"
aria-modal
=
"
true
"
aria-labelledby
=
"
modal-title
"
aria-describedby
=
"
modal-description
"
ref
=
{
modalRef
}
tabIndex
=
{
-
1
}
onKeyDown
=
{
(
e
)
=>
{
if
(
e
.
key
===
'Escape'
)
{
onClose
(
)
;
}
}
}
<
div
className
=
"
modal-overlay
"
onClick
=
{
onClose
}
aria-hidden
=
"
true
"
/>
<
div
className
=
"
modal-content
"
<
h2
id
=
"
modal-title
"
{
title
}
</
h2
<
div
id
=
"
modal-description
"
{
children
}
</
div
<
button
onClick
=
{
onClose
}
aria-label
=
"
Close modal
"
<
span
aria-hidden
=
"
true
"
×
</
span
</
button
</
div
</
div
)
;
}
aria-live Example
(Notifications):
function
Notification
(
{
message
,
type
}
:
{
message
:
string
;
type
:
'success'
|
'error'
}
)
{
return
(
<
div
role
=
"
alert
"
aria-live
=
"
assertive
"
// Immediate announcement (error), "polite" announces in turn
aria-atomic
=
"
true
"
// Read the entire content
className
=
{
notification notification-
${
type
}
}
{
type
===
'error'
&&
<
span
aria-label
=
"
Error
"
⚠️
</
span
}
{
type
===
'success'
&&
<
span
aria-label
=
"
Success
"
✅
</
span
}
{
message
}
</
div
)
;
}
Step 4: Color Contrast and Visual Accessibility
Ensure sufficient contrast ratios for users with visual impairments.
Tasks
:
WCAG AA: text 4.5:1, large text 3:1
WCAG AAA: text 7:1, large text 4.5:1
Do not convey information by color alone (use icons, patterns alongside)
Clearly indicate focus (outline)
Example
(CSS):
/ ✅ Sufficient contrast (text #000 on #FFF = 21:1) /
.button
{
background-color
:
0066cc
;
color
:
ffffff
;
/ contrast ratio 7.7:1 /
}
/ ✅ Focus indicator /
button
:focus
,
a
:focus
{
outline
:
3
px
solid
0066cc
;
outline-offset
:
2
px
;
}
/ ❌ outline: none is forbidden! /
button
:focus
{
outline
:
none
;
/ Never use this /
}
/ ✅ Indicate state with color + icon /
.error-message
{
color
:
d32f2f
;
border-left
:
4
px
solid
d32f2f
;
}
.error-message
::before
{
content
:
'⚠️'
;
margin-right
:
8
px
;
}
Step 5: Accessibility Testing
Validate accessibility with automated and manual testing.
Tasks
:
Automated scan with axe DevTools
Check Lighthouse Accessibility score
Test all features with keyboard only
Screen reader testing (NVDA, VoiceOver)
Example
(Jest + axe-core):
import
{
render
}
from
'@testing-library/react'
;
import
{
axe
,
toHaveNoViolations
}
from
'jest-axe'
;
import
AccessibleButton
from
'./AccessibleButton'
;
expect
.
extend
(
toHaveNoViolations
)
;
describe
(
'AccessibleButton'
,
(
)
=>
{
it
(
'should have no accessibility violations'
,
async
(
)
=>
{
const
{
container
}
=
render
(
<
AccessibleButton onClick
=
{
(
)
=>
{
}
}
Click Me
<
/
AccessibleButton
)
;
const
results
=
await
axe
(
container
)
;
expect
(
results
)
.
toHaveNoViolations
(
)
;
}
)
;
it
(
'should be keyboard accessible'
,
(
)
=>
{
const
handleClick
=
jest
.
fn
(
)
;
const
{
getByRole
}
=
render
(
<
AccessibleButton onClick
=
{
handleClick
}
Click Me
<
/
AccessibleButton
)
;
const
button
=
getByRole
(
'button'
)
;
// Enter key
button
.
focus
(
)
;
fireEvent
.
keyDown
(
button
,
{
key
:
'Enter'
}
)
;
expect
(
handleClick
)
.
toHaveBeenCalled
(
)
;
// Space key
fireEvent
.
keyDown
(
button
,
{
key
:
' '
}
)
;
expect
(
handleClick
)
.
toHaveBeenCalledTimes
(
2
)
;
}
)
;
}
)
;
Output format
Basic Checklist
Accessibility Checklist
Semantic HTML
[x] Use semantic HTML tags (
<button>
,
<nav>
,
<main>
, etc.)
-
[x] Heading hierarchy is correct (h1 → h2 → h3)
-
[x] All form labels are connected
Keyboard Navigation
[x] All interactive elements accessible via Tab
[x] Modals/dropdowns closed with ESC
[x] Focus indicator is clear (outline)
ARIA
[x]
role
used appropriately
-
[x]
aria-label
or
aria-labelledby
provided
-
[x]
aria-live
used for dynamic content
-
[x] Decorative elements use
aria-hidden="true"
Visual
[x] Color contrast meets WCAG AA (4.5:1)
[x] Text size can be adjusted
[x] Responsive design
Testing
[x] Lighthouse Accessibility score 90+
[x] Keyboard test passed
[x] Screen reader test completed
Constraints
Mandatory Rules (MUST)
Keyboard Accessibility
All features must be usable without a mouse
Support Tab, Enter, Space, arrow keys, and ESC
Implement focus trap (for modals)
Alternative Text
All images must have an
alt
attribute
Meaningful images: descriptive alt text
Decorative images:
alt=""
(screen reader ignores)
Clear Labels
All form inputs must have an associated label
or
aria-label
Do not use placeholder alone as a substitute for a label
Prohibited Actions (MUST NOT)
Do Not Remove Outline
Never use
outline: none
Disastrous for keyboard users
Must provide a custom focus style instead
Do Not Use tabindex > 0
Avoid changing focus order
Keep DOM order logical
Exception: only when there is a special reason
Do Not Convey Information by Color Alone
Accompany with icons or text
Consider users with color blindness
e.g., "Click red item" → "Click ⚠️ Error item"
Examples
Example 1: Accessible Form
function
AccessibleContactForm
(
)
{
const
[
errors
,
setErrors
]
=
useState
<
Record
<
string
,
string
>>
(
{
}
)
;
const
[
submitStatus
,
setSubmitStatus
]
=
useState
<
'idle'
|
'success'
|
'error'
>
(
'idle'
)
;
return
(
<
form
onSubmit
=
{
handleSubmit
}
noValidate
>
<
h2
id
=
"
form-title
"
>
Contact Us
</
h2
>
<
p
id
=
"
form-description
"
>
Please fill out the form below to get in touch.
</
p
>
{
/ Name /
}
<
div
className
=
"
form-group
"
>
<
label
htmlFor
=
"
name
"
>
Name
<
span
aria-label
=
"
required
"
>
*
</
span
>
</
label
>
<
input
type
=
"
text
"
id
=
"
name
"
name
=
"
name
"
required
aria-required
=
"
true
"
aria-invalid
=
{
!
!
errors
.
name
}
aria-describedby
=
{
errors
.
name
?
'name-error'
:
undefined
}
/>
{
errors
.
name
&&
(
<
span
id
=
"
name-error
"
role
=
"
alert
"
className
=
"
error
"
>
{
errors
.
name
}
</
span
>
)
}
</
div
>
{
/ Email /
}
<
div
className
=
"
form-group
"
>
<
label
htmlFor
=
"
email
"
>
Email
<
span
aria-label
=
"
required
"
>
*
</
span
>
</
label
>
<
input
type
=
"
email
"
id
=
"
email
"
name
=
"
email
"
required
aria-required
=
"
true
"
aria-invalid
=
{
!
!
errors
.
email
}
aria-describedby
=
{
errors
.
email
?
'email-error'
:
'email-hint'
}
/>
<
span
id
=
"
email-hint
"
className
=
"
hint
"
>
We'll never share your email.
</
span
>
{
errors
.
email
&&
(
<
span
id
=
"
email-error
"
role
=
"
alert
"
className
=
"
error
"
>
{
errors
.
email
}
</
span
>
)
}
</
div
>
{
/ Submit button /
}
<
button
type
=
"
submit
"
disabled
=
{
submitStatus
===
'loading'
}
>
{
submitStatus
===
'loading'
?
'Submitting...'
:
'Submit'
}
</
button
>
{
/ Success/failure messages /
}
{
submitStatus
===
'success'
&&
(
<
div
role
=
"
alert
"
aria-live
=
"
polite
"
className
=
"
success
"
>
✅ Form submitted successfully!
</
div
>
)
}
{
submitStatus
===
'error'
&&
(
<
div
role
=
"
alert
"
aria-live
=
"
assertive
"
className
=
"
error
"
>
⚠️ An error occurred. Please try again.
</
div
>
)
}
</
form
>
)
;
}
Example 2: Accessible Tab UI
function
AccessibleTabs
(
{
tabs
}
:
{
tabs
:
{
id
:
string
;
label
:
string
;
content
:
React
.
ReactNode
}
[
]
}
)
{
const
[
activeTab
,
setActiveTab
]
=
useState
(
0
)
;
const
handleKeyDown
=
(
e
:
React
.
KeyboardEvent
,
index
:
number
)
=>
{
switch
(
e
.
key
)
{
case
'ArrowRight'
:
e
.
preventDefault
(
)
;
setActiveTab
(
(
index
+
1
)
%
tabs
.
length
)
;
break
;
case
'ArrowLeft'
:
e
.
preventDefault
(
)
;
setActiveTab
(
(
index
-
1
+
tabs
.
length
)
%
tabs
.
length
)
;
break
;
case
'Home'
:
e
.
preventDefault
(
)
;
setActiveTab
(
0
)
;
break
;
case
'End'
:
e
.
preventDefault
(
)
;
setActiveTab
(
tabs
.
length
-
1
)
;
break
;
}
}
;
return
(
<
div
>
{
/ Tab List /
}
<
div
role
=
"
tablist
"
aria-label
=
"
Content sections
"
>
{
tabs
.
map
(
(
tab
,
index
)
=>
(
<
button
key
=
{
tab
.
id
}
role
=
"
tab
"
id
=
{
`
tab-
${
tab
.
id
}
`
}
aria-selected
=
{
activeTab
===
index
}
aria-controls
=
{
`
panel-
${
tab
.
id
}
`
}
tabIndex
=
{
activeTab
===
index
?
0
:
-
1
}
onClick
=
{
(
)
=>
setActiveTab
(
index
)
}
onKeyDown
=
{
(
e
)
=>
handleKeyDown
(
e
,
index
)
}
>
{
tab
.
label
}
</
button
>
)
)
}
</
div
>
{
/ Tab Panels /
}
{
tabs
.
map
(
(
tab
,
index
)
=>
(
<
div
key
=
{
tab
.
id
}
role
=
"
tabpanel
"
id
=
{
`
panel-
${
tab
.
id
}
`
}
aria-labelledby
=
{
`
tab-
${
tab
.
id
}
`
}
hidden
=
{
activeTab
!==
index
}
tabIndex
=
{
0
}
>
{
tab
.
content
}
</
div
>
)
)
}
</
div
>
)
;
}
Best practices
Semantic HTML First
ARIA is a last resort
Using the correct HTML element makes ARIA unnecessary
e.g.,
vs
Focus Management
: Manage focus on page transitions in SPAs
Move focus to main content on new page load
Provide skip links ("Skip to main content")
Error Messages
: Clear and helpful error messages
"Invalid input" ❌ → "Email must be in format:
example@domain.com
" ✅
References
WCAG 2.1 Guidelines
MDN ARIA
WebAIM
axe DevTools
A11y Project
Metadata
Version
Current Version
: 1.0.0
Last Updated
: 2025-01-01
Compatible Platforms
: Claude, ChatGPT, Gemini
← 返回排行榜