| name | accessibility-standards |
| description | Implement WCAG 2.1 accessibility standards for Vue 3 apps. Use when adding ARIA labels, keyboard navigation, screen reader support, or checking color contrast. Mentions "accessibility", "ARIA", "keyboard nav", "screen reader", or "color contrast". |
| allowed-tools | Read, Edit, Grep, Glob |
Accessibility Standards
WCAG 2.1 AA compliance guidelines for Vue 3 applications.
When to Activate
Use this skill when the user:
- Says "make it accessible" or "add ARIA labels"
- Asks "keyboard navigation" or "tab order"
- Mentions "screen reader", "WCAG", or "color contrast"
- Wants to "support assistive technology"
Core Principles
- Perceivable: Content must be presentable to users
- Operable: UI must be navigable via keyboard
- Understandable: Information must be clear
- Robust: Compatible with assistive technologies
1. Keyboard Navigation
Tab Order
Proper Focus Flow: Left → Right, Top → Bottom
<template>
<!-- Header actions -->
<button tabindex="0" @click="refresh">刷新</button>
<button tabindex="0" @click="settings">设置</button>
<!-- Filter panel -->
<select tabindex="0" v-model="selectedInstitution">...</select>
<button tabindex="0" @click="applyFilters">应用筛选</button>
<!-- Main content -->
<div tabindex="0" role="main">...</div>
</template>
Skip Links
<template>
<a href="#main-content" class="skip-link">跳至主内容</a>
<header>...</header>
<main id="main-content" tabindex="-1">
<!-- Main content -->
</main>
</template>
<style>
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
position: static;
left: 0;
}
</style>
Keyboard Event Handling
<template>
<button
@click="handleAction"
@keydown.enter="handleAction"
@keydown.space.prevent="handleAction"
>
操作按钮
</button>
<!-- Custom component -->
<FilterPanel
tabindex="0"
@keydown.esc="closePanel"
aria-label="筛选面板"
/>
</template>
2. ARIA Labels
Button ARIA
<!-- Icon button needs aria-label -->
<button aria-label="刷新数据" @click="refresh">
<RefreshIcon />
</button>
<!-- Text button doesn't need it -->
<button @click="save">保存</button>
<!-- Disabled button -->
<button disabled aria-disabled="true">已禁用</button>
Form ARIA
<template>
<div class="form-field">
<label for="institution-select">三级机构</label>
<select
id="institution-select"
v-model="selectedInstitution"
aria-describedby="institution-help"
aria-required="true"
>
<option>达州</option>
<option>德阳</option>
</select>
<span id="institution-help" class="help-text">
选择业务员所属机构
</span>
</div>
</template>
Live Region (Status Updates)
<template>
<!-- Announce status changes to screen readers -->
<div
role="status"
aria-live="polite"
aria-atomic="true"
class="sr-only"
>
{{ statusMessage }}
</div>
</template>
<script setup>
const statusMessage = ref('')
watch(isLoading, (loading) => {
statusMessage.value = loading ? '正在加载数据' : '数据加载完成'
})
</script>
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
</style>
Dialog ARIA
<template>
<div
v-if="visible"
role="dialog"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc"
aria-modal="true"
>
<h2 id="dialog-title">确认操作</h2>
<p id="dialog-desc">您确定要删除此项吗?</p>
<button @click="confirm">确认</button>
<button @click="cancel">取消</button>
</div>
</template>
3. Color Contrast
WCAG 2.1 AA Requirements
- Normal text: Contrast ratio ≥ 4.5:1
- Large text (18pt+): Contrast ratio ≥ 3:1
Current Platform Colors (Verified)
| Combination | Ratio | Status |
|---|---|---|
| Primary text (#2C3E50) / White | 12.6:1 | ✅ Excellent |
| Secondary text (#8B95A5) / White | 4.8:1 | ✅ Pass |
| Primary color (#5B8DEF) / White | 4.2:1 | ⚠️ Borderline |
| Error color (#EF4444) / White | 5.1:1 | ✅ Pass |
Improvements
/* Use primary color with bold text for better readability */
.link-primary {
color: var(--primary-500);
font-weight: 600; /* Bold improves perceived contrast */
}
/* Add icon support for color-blind users */
.status-success {
color: var(--success-600);
}
.status-success::before {
content: '✓'; /* Icon doesn't rely on color alone */
}
4. Focus Indicators
Visible Focus
/* Default browser focus */
*:focus {
outline: 2px solid var(--primary-500);
outline-offset: 2px;
}
/* Custom focus for buttons */
.btn:focus-visible {
outline: 2px solid var(--primary-500);
outline-offset: 2px;
box-shadow: 0 0 0 4px var(--primary-100);
}
/* Remove outline for mouse users */
.btn:focus:not(:focus-visible) {
outline: none;
}
Focus Management
// Focus first interactive element in modal
const focusFirstElement = () => {
nextTick(() => {
const firstInput = modalRef.value?.querySelector('button, input, select')
firstInput?.focus()
})
}
5. Screen Reader Support
Image Alt Text
<!-- Decorative image -->
<img src="icon.svg" alt="" role="presentation" />
<!-- Informative image -->
<img src="chart.png" alt="周对比保费趋势图,显示最近3周保费上升" />
Chart Accessibility
<template>
<div
class="chart-container"
role="img"
:aria-label="chartDescription"
>
<ECharts :option="chartOption" />
</div>
<!-- Provide data table alternative -->
<details class="chart-data">
<summary>查看数据表格</summary>
<table>
<caption>周对比保费数据</caption>
<thead>
<tr>
<th>日期</th>
<th>保费(万元)</th>
</tr>
</thead>
<tbody>
<tr v-for="item in chartData" :key="item.date">
<td>{{ item.date }}</td>
<td>{{ item.value }}</td>
</tr>
</tbody>
</table>
</details>
</template>
<script setup>
const chartDescription = computed(() =>
`周对比保费趋势图,显示${chartData.length}个数据点,保费范围从${minValue}到${maxValue}万元`
)
</script>
Loading Announcements
<template>
<div aria-busy="true" aria-live="polite">
<Loading v-if="isLoading" />
<span class="sr-only">{{ loadingMessage }}</span>
</div>
</template>
<script setup>
const loadingMessage = computed(() =>
isLoading.value ? '正在加载数据,请稍候' : '数据加载完成'
)
</script>
6. Motion and Animation
Respect User Preferences
/* Disable animations for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Safe Defaults
/* Use subtle animations by default */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
/* No flashing or rapid movements */
Accessibility Checklist
Before Shipping
- All interactive elements are keyboard accessible
- Focus indicators are visible
- All images have appropriate alt text
- Forms have associated labels
- Color contrast meets WCAG AA (4.5:1)
- ARIA roles and properties are correct
- Screen reader tested (NVDA/JAWS/VoiceOver)
- Keyboard navigation tested (Tab/Shift+Tab/Arrow keys/Enter/Esc)
- Reduced motion preference respected
- Error messages are announced to screen readers
Testing Tools
Browser Extensions
- axe DevTools - Automated accessibility testing
- WAVE - Visual accessibility evaluation
- Lighthouse - Built into Chrome DevTools
Screen Readers
- NVDA (Windows, free)
- JAWS (Windows, paid)
- VoiceOver (macOS, built-in)
Keyboard Testing
- Use only keyboard (no mouse)
- Tab through entire interface
- Verify all actions are accessible
- Check focus indicators are visible
Troubleshooting
"Screen reader doesn't announce changes"
Add aria-live region:
<div role="status" aria-live="polite">{{ message }}</div>
"Keyboard navigation skips elements"
Check tabindex:
0= Normal tab order-1= Not in tab order, but focusable programmatically1+= Avoid (disrupts natural order)
"Focus indicator not visible"
Don't remove :focus styles. Customize instead:
*:focus-visible {
outline: 2px solid var(--primary-500);
}
Related Files
Component examples:
Create:
utils/accessibility.js- Helper functionscomposables/useFocusTrap.js- Modal focus management
Related Skills:
vue-component-dev- Component developmentuser-guidance-flows- Help text and guidance
Skill Version: v1.0 Created: 2025-11-09 Focuses On: Accessibility standards only