gluestack-accessibility
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesegluestack-ui - Accessibility
gluestack-ui - 无障碍设计
Expert knowledge of building accessible user interfaces with gluestack-ui, ensuring WCAG 2.1 AA compliance across React and React Native platforms.
掌握使用gluestack-ui构建无障碍用户界面的专业知识,确保在React和React Native平台上符合WCAG 2.1 AA标准。
Overview
概述
gluestack-ui components are built with accessibility in mind, following WAI-ARIA guidelines and providing built-in support for screen readers, keyboard navigation, and focus management. This skill covers best practices for maintaining and enhancing accessibility.
gluestack-ui组件从设计之初就考虑了无障碍性,遵循WAI-ARIA指南,内置对屏幕阅读器、键盘导航和焦点管理的支持。本技能涵盖维护和增强无障碍性的最佳实践。
Key Concepts
核心概念
Built-in Accessibility
内置无障碍特性
gluestack-ui components include accessibility features out of the box:
tsx
// Button automatically has role="button" and handles focus
<Button onPress={handlePress}>
<ButtonText>Submit</ButtonText>
</Button>
// Modal manages focus trap and escape key handling
<Modal isOpen={isOpen} onClose={onClose}>
<ModalContent>
<ModalBody>Content</ModalBody>
</ModalContent>
</Modal>
// Form controls link labels to inputs
<FormControl>
<FormControlLabel>
<FormControlLabelText>Email</FormControlLabelText>
</FormControlLabel>
<Input>
<InputField />
</Input>
</FormControl>gluestack-ui组件自带无障碍功能:
tsx
// Button自动拥有role="button"并处理焦点
<Button onPress={handlePress}>
<ButtonText>提交</ButtonText>
</Button>
// Modal管理焦点陷阱和ESC键处理
<Modal isOpen={isOpen} onClose={onClose}>
<ModalContent>
<ModalBody>内容</ModalBody>
</ModalContent>
</Modal>
// 表单控件自动关联标签与输入框
<FormControl>
<FormControlLabel>
<FormControlLabelText>邮箱</FormControlLabelText>
</FormControlLabel>
<Input>
<InputField />
</Input>
</FormControl>Accessibility Props
无障碍属性
React Native accessibility props supported by gluestack-ui:
tsx
<Pressable
accessibilityLabel="Close dialog"
accessibilityHint="Closes the current dialog and returns to the previous screen"
accessibilityRole="button"
accessibilityState={{ disabled: isDisabled }}
accessible={true}
onPress={onClose}
>
<Icon as={CloseIcon} />
</Pressable>gluestack-ui支持React Native的无障碍属性:
tsx
<Pressable
accessibilityLabel="关闭对话框"
accessibilityHint="关闭当前对话框并返回上一页面"
accessibilityRole="button"
accessibilityState={{ disabled: isDisabled }}
accessible={true}
onPress={onClose}
>
<Icon as={CloseIcon} />
</Pressable>ARIA Attributes for Web
Web平台的ARIA属性
For web platforms, use ARIA attributes:
tsx
import { Platform } from 'react-native';
<Button
{...(Platform.OS === 'web' && {
'aria-label': 'Close dialog',
'aria-describedby': 'dialog-description',
'aria-expanded': isExpanded,
})}
onPress={handlePress}
>
<ButtonText>Toggle</ButtonText>
</Button>针对Web平台,可使用ARIA属性:
tsx
import { Platform } from 'react-native';
<Button
{...(Platform.OS === 'web' && {
'aria-label': '关闭对话框',
'aria-describedby': 'dialog-description',
'aria-expanded': isExpanded,
})}
onPress={handlePress}
>
<ButtonText>切换</ButtonText>
</Button>Screen Reader Support
屏幕阅读器支持
Meaningful Labels
有意义的标签
Provide descriptive labels for interactive elements:
tsx
// Bad: No context for screen reader users
<Button onPress={handleDelete}>
<ButtonIcon as={TrashIcon} />
</Button>
// Good: Clear accessibility label
<Button
onPress={handleDelete}
accessibilityLabel="Delete item"
accessibilityHint="Permanently removes this item from your list"
>
<ButtonIcon as={TrashIcon} />
</Button>为交互元素提供描述性标签:
tsx
// 不佳:屏幕阅读器用户无法获取上下文
<Button onPress={handleDelete}>
<ButtonIcon as={TrashIcon} />
</Button>
// 良好:清晰的无障碍标签
<Button
onPress={handleDelete}
accessibilityLabel="删除项目"
accessibilityHint="从列表中永久移除该项目"
>
<ButtonIcon as={TrashIcon} />
</Button>Announcing Dynamic Changes
动态变更通知
Use accessibility live regions for dynamic content:
tsx
import { AccessibilityInfo } from 'react-native';
function SearchResults({ results, isLoading }: {
results: Item[];
isLoading: boolean;
}) {
useEffect(() => {
if (!isLoading) {
AccessibilityInfo.announceForAccessibility(
`${results.length} results found`
);
}
}, [results, isLoading]);
return (
<VStack
accessibilityRole="list"
accessibilityLabel={`Search results, ${results.length} items`}
>
{results.map((item) => (
<Box key={item.id} accessibilityRole="listitem">
<Text>{item.name}</Text>
</Box>
))}
</VStack>
);
}使用无障碍实时区域处理动态内容:
tsx
import { AccessibilityInfo } from 'react-native';
function SearchResults({ results, isLoading }: {
results: Item[];
isLoading: boolean;
}) {
useEffect(() => {
if (!isLoading) {
AccessibilityInfo.announceForAccessibility(
`找到${results.length}个结果`
);
}
}, [results, isLoading]);
return (
<VStack
accessibilityRole="list"
accessibilityLabel={`搜索结果,共${results.length}项`}
>
{results.map((item) => (
<Box key={item.id} accessibilityRole="listitem">
<Text>{item.name}</Text>
</Box>
))}
</VStack>
);
}Image Accessibility
图片无障碍
Always provide alt text for images:
tsx
import { Image } from '@/components/ui/image';
// Informative image
<Image
source={{ uri: product.imageUrl }}
alt={`${product.name} - ${product.color} color option`}
className="w-full h-48 rounded-lg"
/>
// Decorative image (hide from screen readers)
<Image
source={require('@/assets/decorative-pattern.png')}
alt=""
accessibilityElementsHidden={true}
importantForAccessibility="no-hide-descendants"
className="absolute inset-0 opacity-10"
/>始终为图片提供替代文本:
tsx
import { Image } from '@/components/ui/image';
// 信息类图片
<Image
source={{ uri: product.imageUrl }}
alt={`${product.name} - ${product.color}颜色选项`}
className="w-full h-48 rounded-lg"
/>
// 装饰类图片(对屏幕阅读器隐藏)
<Image
source={require('@/assets/decorative-pattern.png')}
alt=""
accessibilityElementsHidden={true}
importantForAccessibility="no-hide-descendants"
className="absolute inset-0 opacity-10"
/>Keyboard Navigation
键盘导航
Focus Management
焦点管理
Ensure proper focus order and visibility:
tsx
import { useRef, useEffect } from 'react';
import { TextInput } from 'react-native';
function SearchModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const searchInputRef = useRef<TextInput>(null);
useEffect(() => {
if (isOpen) {
// Focus the search input when modal opens
searchInputRef.current?.focus();
}
}, [isOpen]);
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalContent>
<ModalHeader>
<Heading>Search</Heading>
<ModalCloseButton />
</ModalHeader>
<ModalBody>
<Input>
<InputField
ref={searchInputRef}
placeholder="Search..."
accessibilityLabel="Search input"
/>
</Input>
</ModalBody>
</ModalContent>
</Modal>
);
}确保焦点顺序合理且可见:
tsx
import { useRef, useEffect } from 'react';
import { TextInput } from 'react-native';
function SearchModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const searchInputRef = useRef<TextInput>(null);
useEffect(() => {
if (isOpen) {
// 模态框打开时聚焦搜索输入框
searchInputRef.current?.focus();
}
}, [isOpen]);
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalContent>
<ModalHeader>
<Heading>搜索</Heading>
<ModalCloseButton />
</ModalHeader>
<ModalBody>
<Input>
<InputField
ref={searchInputRef}
placeholder="搜索..."
accessibilityLabel="搜索输入框"
/>
</Input>
</ModalBody>
</ModalContent>
</Modal>
);
}Focus Trap in Modals
模态框的焦点陷阱
gluestack-ui Modal automatically traps focus, but you can enhance it:
tsx
function AccessibleModal({ isOpen, onClose, children }: {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}) {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
closeOnOverlayClick={true}
// Escape key closes modal (built-in)
>
<ModalBackdrop />
<ModalContent
accessibilityRole="dialog"
accessibilityModal={true}
accessibilityLabel="Dialog"
>
{children}
</ModalContent>
</Modal>
);
}gluestack-ui的Modal自动实现焦点陷阱,你也可以进一步增强:
tsx
function AccessibleModal({ isOpen, onClose, children }: {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}) {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
closeOnOverlayClick={true}
// 内置ESC键关闭模态框功能
>
<ModalBackdrop />
<ModalContent
accessibilityRole="dialog"
accessibilityModal={true}
accessibilityLabel="对话框"
>
{children}
</ModalContent>
</Modal>
);
}Keyboard Shortcuts
键盘快捷键
Implement keyboard shortcuts for web:
tsx
import { useEffect } from 'react';
import { Platform } from 'react-native';
function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
if (Platform.OS !== 'web') return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === key && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
callback();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [key, callback]);
}
// Usage
function SearchBar() {
const inputRef = useRef<TextInput>(null);
useKeyboardShortcut('k', () => {
inputRef.current?.focus();
});
return (
<Input>
<InputField
ref={inputRef}
placeholder="Search (Cmd+K)"
accessibilityKeyShortcuts={['cmd+k']}
/>
</Input>
);
}为Web平台实现键盘快捷键:
tsx
import { useEffect } from 'react';
import { Platform } from 'react-native';
function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
if (Platform.OS !== 'web') return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === key && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
callback();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [key, callback]);
}
// 使用示例
function SearchBar() {
const inputRef = useRef<TextInput>(null);
useKeyboardShortcut('k', () => {
inputRef.current?.focus();
});
return (
<Input>
<InputField
ref={inputRef}
placeholder="搜索 (Cmd+K)"
accessibilityKeyShortcuts={['cmd+k']}
/>
</Input>
);
}Form Accessibility
表单无障碍
Label Association
标签关联
Properly associate labels with form controls:
tsx
import {
FormControl,
FormControlLabel,
FormControlLabelText,
FormControlHelper,
FormControlHelperText,
FormControlError,
FormControlErrorIcon,
FormControlErrorText,
} from '@/components/ui/form-control';
import { Input, InputField } from '@/components/ui/input';
import { AlertCircleIcon } from 'lucide-react-native';
function AccessibleFormField({
label,
placeholder,
helperText,
error,
isRequired,
value,
onChange,
}: {
label: string;
placeholder: string;
helperText?: string;
error?: string;
isRequired?: boolean;
value: string;
onChange: (text: string) => void;
}) {
return (
<FormControl isRequired={isRequired} isInvalid={!!error}>
<FormControlLabel>
<FormControlLabelText>{label}</FormControlLabelText>
</FormControlLabel>
<Input>
<InputField
placeholder={placeholder}
value={value}
onChangeText={onChange}
accessibilityLabel={label}
accessibilityHint={helperText}
/>
</Input>
{error ? (
<FormControlError>
<FormControlErrorIcon as={AlertCircleIcon} />
<FormControlErrorText>{error}</FormControlErrorText>
</FormControlError>
) : helperText ? (
<FormControlHelper>
<FormControlHelperText>{helperText}</FormControlHelperText>
</FormControlHelper>
) : null}
</FormControl>
);
}正确关联标签与表单控件:
tsx
import {
FormControl,
FormControlLabel,
FormControlLabelText,
FormControlHelper,
FormControlHelperText,
FormControlError,
FormControlErrorIcon,
FormControlErrorText,
} from '@/components/ui/form-control';
import { Input, InputField } from '@/components/ui/input';
import { AlertCircleIcon } from 'lucide-react-native';
function AccessibleFormField({
label,
placeholder,
helperText,
error,
isRequired,
value,
onChange,
}: {
label: string;
placeholder: string;
helperText?: string;
error?: string;
isRequired?: boolean;
value: string;
onChange: (text: string) => void;
}) {
return (
<FormControl isRequired={isRequired} isInvalid={!!error}>
<FormControlLabel>
<FormControlLabelText>{label}</FormControlLabelText>
</FormControlLabel>
<Input>
<InputField
placeholder={placeholder}
value={value}
onChangeText={onChange}
accessibilityLabel={label}
accessibilityHint={helperText}
/>
</Input>
{error ? (
<FormControlError>
<FormControlErrorIcon as={AlertCircleIcon} />
<FormControlErrorText>{error}</FormControlErrorText>
</FormControlError>
) : helperText ? (
<FormControlHelper>
<FormControlHelperText>{helperText}</FormControlHelperText>
</FormControlHelper>
) : null}
</FormControl>
);
}Error Announcement
错误通知
Announce form errors to screen readers:
tsx
import { AccessibilityInfo } from 'react-native';
function FormWithValidation() {
const [errors, setErrors] = useState<Record<string, string>>({});
const validateAndSubmit = () => {
const newErrors: Record<string, string> = {};
if (!formData.email) {
newErrors.email = 'Email is required';
}
if (!formData.password) {
newErrors.password = 'Password is required';
}
setErrors(newErrors);
const errorCount = Object.keys(newErrors).length;
if (errorCount > 0) {
// Announce errors to screen readers
AccessibilityInfo.announceForAccessibility(
`Form has ${errorCount} error${errorCount > 1 ? 's' : ''}. ${Object.values(newErrors).join('. ')}`
);
return;
}
submitForm();
};
return (
<VStack space="md">
<AccessibleFormField
label="Email"
error={errors.email}
{...emailProps}
/>
<AccessibleFormField
label="Password"
error={errors.password}
{...passwordProps}
/>
<Button onPress={validateAndSubmit}>
<ButtonText>Submit</ButtonText>
</Button>
</VStack>
);
}向屏幕阅读器通知表单错误:
tsx
import { AccessibilityInfo } from 'react-native';
function FormWithValidation() {
const [errors, setErrors] = useState<Record<string, string>>({});
const validateAndSubmit = () => {
const newErrors: Record<string, string> = {};
if (!formData.email) {
newErrors.email = '邮箱为必填项';
}
if (!formData.password) {
newErrors.password = '密码为必填项';
}
setErrors(newErrors);
const errorCount = Object.keys(newErrors).length;
if (errorCount > 0) {
// 向屏幕阅读器通知错误
AccessibilityInfo.announceForAccessibility(
`表单存在${errorCount}项错误${errorCount > 1 ? 's' : ''}。${Object.values(newErrors).join('。 ')}`
);
return;
}
submitForm();
};
return (
<VStack space="md">
<AccessibleFormField
label="邮箱"
error={errors.email}
{...emailProps}
/>
<AccessibleFormField
label="密码"
error={errors.password}
{...passwordProps}
/>
<Button onPress={validateAndSubmit}>
<ButtonText>提交</ButtonText>
</Button>
</VStack>
);
}Required Field Indication
必填字段标识
Clearly indicate required fields:
tsx
function RequiredLabel({ label }: { label: string }) {
return (
<FormControlLabel>
<FormControlLabelText>
{label}
<Text className="text-error-500" accessibilityLabel="required">
{' *'}
</Text>
</FormControlLabelText>
</FormControlLabel>
);
}清晰标识必填字段:
tsx
function RequiredLabel({ label }: { label: string }) {
return (
<FormControlLabel>
<FormControlLabelText>
{label}
<Text className="text-error-500" accessibilityLabel="必填">
{' *'}
</Text>
</FormControlLabelText>
</FormControlLabel>
);
}Best Practices
最佳实践
1. Use Semantic Components
1. 使用语义化组件
Choose appropriate components for their semantic meaning:
tsx
// Good: Semantic components
<Heading size="xl" accessibilityRole="header">Page Title</Heading>
<Button onPress={handleSubmit}>
<ButtonText>Submit</ButtonText>
</Button>
// Bad: Generic elements for interactive content
<Text onPress={handleSubmit}>Submit</Text>根据语义含义选择合适的组件:
tsx
// 良好:语义化组件
<Heading size="xl" accessibilityRole="header">页面标题</Heading>
<Button onPress={handleSubmit}>
<ButtonText>提交</ButtonText>
</Button>
// 不佳:使用通用元素承载交互内容
<Text onPress={handleSubmit}>提交</Text>2. Provide Sufficient Color Contrast
2. 提供足够的色彩对比度
Ensure text meets WCAG contrast requirements (4.5:1 for normal text, 3:1 for large text):
tsx
// Good: High contrast
<Text className="text-typography-900 dark:text-typography-50">
Readable text
</Text>
// Bad: Low contrast
<Text className="text-typography-300">
Hard to read text
</Text>确保文本符合WCAG对比度要求(普通文本4.5:1,大文本3:1):
tsx
// 良好:高对比度
<Text className="text-typography-900 dark:text-typography-50">
易读文本
</Text>
// 不佳:低对比度
<Text className="text-typography-300">
难以阅读的文本
</Text>3. Support Reduced Motion
3. 支持减少动画
Respect user preferences for reduced motion:
tsx
import { useReducedMotion } from 'react-native-reanimated';
function AnimatedCard({ children }: { children: React.ReactNode }) {
const reducedMotion = useReducedMotion();
return (
<Animated.View
entering={reducedMotion ? undefined : FadeIn.duration(300)}
exiting={reducedMotion ? undefined : FadeOut.duration(300)}
>
{children}
</Animated.View>
);
}尊重用户对减少动画的偏好:
tsx
import { useReducedMotion } from 'react-native-reanimated';
function AnimatedCard({ children }: { children: React.ReactNode }) {
const reducedMotion = useReducedMotion();
return (
<Animated.View
entering={reducedMotion ? undefined : FadeIn.duration(300)}
exiting={reducedMotion ? undefined : FadeOut.duration(300)}
>
{children}
</Animated.View>
);
}4. Handle Touch Target Sizes
4. 处理触摸目标尺寸
Ensure touch targets are at least 44x44 points:
tsx
// Good: Adequate touch target
<Button size="md" className="min-h-[44px] min-w-[44px]">
<ButtonIcon as={MenuIcon} />
</Button>
// Or use Pressable with hitSlop
<Pressable
onPress={handlePress}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
className="p-2"
>
<Icon as={CloseIcon} size="sm" />
</Pressable>确保触摸目标至少为44x44点:
tsx
// 良好:足够的触摸目标
<Button size="md" className="min-h-[44px] min-w-[44px]">
<ButtonIcon as={MenuIcon} />
</Button>
// 或使用带hitSlop的Pressable
<Pressable
onPress={handlePress}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
className="p-2"
>
<Icon as={CloseIcon} size="sm" />
</Pressable>5. Group Related Elements
5. 分组相关元素
Use accessibility groups for related content:
tsx
<Box
accessibilityRole="group"
accessibilityLabel="Product details"
>
<Heading>{product.name}</Heading>
<Text>{product.description}</Text>
<Text>{formatPrice(product.price)}</Text>
</Box>使用无障碍分组管理相关内容:
tsx
<Box
accessibilityRole="group"
accessibilityLabel="产品详情"
>
<Heading>{product.name}</Heading>
<Text>{product.description}</Text>
<Text>{formatPrice(product.price)}</Text>
</Box>Examples
示例
Accessible Navigation Menu
无障碍导航菜单
tsx
import { useState } from 'react';
import { HStack } from '@/components/ui/hstack';
import { Pressable } from '@/components/ui/pressable';
import { Text } from '@/components/ui/text';
interface NavItem {
id: string;
label: string;
href: string;
}
function AccessibleNav({ items, currentPath }: {
items: NavItem[];
currentPath: string;
}) {
return (
<HStack
space="md"
accessibilityRole="navigation"
accessibilityLabel="Main navigation"
>
{items.map((item) => {
const isActive = currentPath === item.href;
return (
<Pressable
key={item.id}
accessibilityRole="link"
accessibilityLabel={item.label}
accessibilityState={{ selected: isActive }}
accessibilityCurrent={isActive ? 'page' : undefined}
onPress={() => navigate(item.href)}
className={cn(
'px-4 py-2 rounded-lg',
isActive
? 'bg-primary-500'
: 'bg-transparent hover:bg-background-100'
)}
>
<Text
className={cn(
isActive ? 'text-typography-0' : 'text-typography-700'
)}
>
{item.label}
</Text>
</Pressable>
);
})}
</HStack>
);
}tsx
import { useState } from 'react';
import { HStack } from '@/components/ui/hstack';
import { Pressable } from '@/components/ui/pressable';
import { Text } from '@/components/ui/text';
interface NavItem {
id: string;
label: string;
href: string;
}
function AccessibleNav({ items, currentPath }: {
items: NavItem[];
currentPath: string;
}) {
return (
<HStack
space="md"
accessibilityRole="navigation"
accessibilityLabel="主导航"
>
{items.map((item) => {
const isActive = currentPath === item.href;
return (
<Pressable
key={item.id}
accessibilityRole="link"
accessibilityLabel={item.label}
accessibilityState={{ selected: isActive }}
accessibilityCurrent={isActive ? 'page' : undefined}
onPress={() => navigate(item.href)}
className={cn(
'px-4 py-2 rounded-lg',
isActive
? 'bg-primary-500'
: 'bg-transparent hover:bg-background-100'
)}
>
<Text
className={cn(
isActive ? 'text-typography-0' : 'text-typography-700'
)}
>
{item.label}
</Text>
</Pressable>
);
})}
</HStack>
);
}Accessible Data Table
无障碍数据表格
tsx
import { VStack } from '@/components/ui/vstack';
import { HStack } from '@/components/ui/hstack';
import { Box } from '@/components/ui/box';
import { Text } from '@/components/ui/text';
interface Column<T> {
key: keyof T;
header: string;
accessibilityLabel?: string;
}
interface AccessibleTableProps<T> {
columns: Column<T>[];
data: T[];
caption: string;
}
function AccessibleTable<T extends { id: string }>({
columns,
data,
caption,
}: AccessibleTableProps<T>) {
return (
<VStack
accessibilityRole="table"
accessibilityLabel={caption}
>
{/* Caption for screen readers */}
<Text
accessibilityRole="summary"
className="sr-only"
>
{caption}
</Text>
{/* Header Row */}
<HStack
accessibilityRole="row"
className="bg-background-100 dark:bg-background-800 rounded-t-lg"
>
{columns.map((column) => (
<Box
key={String(column.key)}
accessibilityRole="columnheader"
className="flex-1 p-3"
>
<Text className="font-semibold text-typography-700 dark:text-typography-200">
{column.header}
</Text>
</Box>
))}
</HStack>
{/* Data Rows */}
{data.map((row, rowIndex) => (
<HStack
key={row.id}
accessibilityRole="row"
accessibilityLabel={`Row ${rowIndex + 1}`}
className={cn(
'border-b border-outline-200 dark:border-outline-700',
rowIndex % 2 === 0 ? 'bg-background-0' : 'bg-background-50'
)}
>
{columns.map((column) => (
<Box
key={String(column.key)}
accessibilityRole="cell"
accessibilityLabel={`${column.header}: ${String(row[column.key])}`}
className="flex-1 p-3"
>
<Text className="text-typography-900 dark:text-typography-50">
{String(row[column.key])}
</Text>
</Box>
))}
</HStack>
))}
</VStack>
);
}tsx
import { VStack } from '@/components/ui/vstack';
import { HStack } from '@/components/ui/hstack';
import { Box } from '@/components/ui/box';
import { Text } from '@/components/ui/text';
interface Column<T> {
key: keyof T;
header: string;
accessibilityLabel?: string;
}
interface AccessibleTableProps<T> {
columns: Column<T>[];
data: T[];
caption: string;
}
function AccessibleTable<T extends { id: string }>({
columns,
data,
caption,
}: AccessibleTableProps<T>) {
return (
<VStack
accessibilityRole="table"
accessibilityLabel={caption}
>
{/* 屏幕阅读器专用标题 */}
<Text
accessibilityRole="summary"
className="sr-only"
>
{caption}
</Text>
{/* 表头行 */}
<HStack
accessibilityRole="row"
className="bg-background-100 dark:bg-background-800 rounded-t-lg"
>
{columns.map((column) => (
<Box
key={String(column.key)}
accessibilityRole="columnheader"
className="flex-1 p-3"
>
<Text className="font-semibold text-typography-700 dark:text-typography-200">
{column.header}
</Text>
</Box>
))}
</HStack>
{/* 数据行 */}
{data.map((row, rowIndex) => (
<HStack
key={row.id}
accessibilityRole="row"
accessibilityLabel={`第${rowIndex + 1}行`}
className={cn(
'border-b border-outline-200 dark:border-outline-700',
rowIndex % 2 === 0 ? 'bg-background-0' : 'bg-background-50'
)}
>
{columns.map((column) => (
<Box
key={String(column.key)}
accessibilityRole="cell"
accessibilityLabel={`${column.header}:${String(row[column.key])}`}
className="flex-1 p-3"
>
<Text className="text-typography-900 dark:text-typography-50">
{String(row[column.key])}
</Text>
</Box>
))}
</HStack>
))}
</VStack>
);
}Accessible Alert Component
无障碍提示组件
tsx
import { HStack } from '@/components/ui/hstack';
import { VStack } from '@/components/ui/vstack';
import { Box } from '@/components/ui/box';
import { Text } from '@/components/ui/text';
import { Icon } from '@/components/ui/icon';
import {
AlertCircleIcon,
CheckCircleIcon,
InfoIcon,
AlertTriangleIcon,
} from 'lucide-react-native';
type AlertType = 'info' | 'success' | 'warning' | 'error';
interface AccessibleAlertProps {
type: AlertType;
title: string;
message: string;
}
const alertConfig: Record<AlertType, {
icon: typeof InfoIcon;
containerClass: string;
iconClass: string;
role: 'alert' | 'status';
}> = {
info: {
icon: InfoIcon,
containerClass: 'bg-info-50 dark:bg-info-900 border-info-200',
iconClass: 'text-info-500',
role: 'status',
},
success: {
icon: CheckCircleIcon,
containerClass: 'bg-success-50 dark:bg-success-900 border-success-200',
iconClass: 'text-success-500',
role: 'status',
},
warning: {
icon: AlertTriangleIcon,
containerClass: 'bg-warning-50 dark:bg-warning-900 border-warning-200',
iconClass: 'text-warning-500',
role: 'alert',
},
error: {
icon: AlertCircleIcon,
containerClass: 'bg-error-50 dark:bg-error-900 border-error-200',
iconClass: 'text-error-500',
role: 'alert',
},
};
export function AccessibleAlert({ type, title, message }: AccessibleAlertProps) {
const config = alertConfig[type];
return (
<Box
accessibilityRole={config.role}
accessibilityLiveRegion={type === 'error' || type === 'warning' ? 'assertive' : 'polite'}
accessibilityLabel={`${type} alert: ${title}. ${message}`}
className={cn(
'p-4 rounded-lg border',
config.containerClass
)}
>
<HStack space="sm" alignItems="flex-start">
<Icon
as={config.icon}
className={cn('w-5 h-5 mt-0.5', config.iconClass)}
accessibilityElementsHidden={true}
/>
<VStack space="xs" flex={1}>
<Text className="font-semibold text-typography-900 dark:text-typography-50">
{title}
</Text>
<Text className="text-typography-700 dark:text-typography-200">
{message}
</Text>
</VStack>
</HStack>
</Box>
);
}tsx
import { HStack } from '@/components/ui/hstack';
import { VStack } from '@/components/ui/vstack';
import { Box } from '@/components/ui/box';
import { Text } from '@/components/ui/text';
import { Icon } from '@/components/ui/icon';
import {
AlertCircleIcon,
CheckCircleIcon,
InfoIcon,
AlertTriangleIcon,
} from 'lucide-react-native';
type AlertType = 'info' | 'success' | 'warning' | 'error';
interface AccessibleAlertProps {
type: AlertType;
title: string;
message: string;
}
const alertConfig: Record<AlertType, {
icon: typeof InfoIcon;
containerClass: string;
iconClass: string;
role: 'alert' | 'status';
}> = {
info: {
icon: InfoIcon,
containerClass: 'bg-info-50 dark:bg-info-900 border-info-200',
iconClass: 'text-info-500',
role: 'status',
},
success: {
icon: CheckCircleIcon,
containerClass: 'bg-success-50 dark:bg-success-900 border-success-200',
iconClass: 'text-success-500',
role: 'status',
},
warning: {
icon: AlertTriangleIcon,
containerClass: 'bg-warning-50 dark:bg-warning-900 border-warning-200',
iconClass: 'text-warning-500',
role: 'alert',
},
error: {
icon: AlertCircleIcon,
containerClass: 'bg-error-50 dark:bg-error-900 border-error-200',
iconClass: 'text-error-500',
role: 'alert',
},
};
export function AccessibleAlert({ type, title, message }: AccessibleAlertProps) {
const config = alertConfig[type];
return (
<Box
accessibilityRole={config.role}
accessibilityLiveRegion={type === 'error' || type === 'warning' ? 'assertive' : 'polite'}
accessibilityLabel={`${type}提示:${title}。${message}`}
className={cn(
'p-4 rounded-lg border',
config.containerClass
)}
>
<HStack space="sm" alignItems="flex-start">
<Icon
as={config.icon}
className={cn('w-5 h-5 mt-0.5', config.iconClass)}
accessibilityElementsHidden={true}
/>
<VStack space="xs" flex={1}>
<Text className="font-semibold text-typography-900 dark:text-typography-50">
{title}
</Text>
<Text className="text-typography-700 dark:text-typography-200">
{message}
</Text>
</VStack>
</HStack>
</Box>
);
}Common Patterns
常见模式
Skip Navigation Link
跳过导航链接
tsx
import { useState } from 'react';
import { Pressable } from '@/components/ui/pressable';
import { Text } from '@/components/ui/text';
function SkipLink() {
const [isFocused, setIsFocused] = useState(false);
return (
<Pressable
onPress={() => {
// Focus main content
document.getElementById('main-content')?.focus();
}}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
accessibilityRole="link"
accessibilityLabel="Skip to main content"
className={cn(
'absolute left-4 z-50 px-4 py-2 bg-primary-500 rounded-md',
'transition-all duration-200',
isFocused ? 'top-4' : '-top-20'
)}
>
<Text className="text-typography-0 font-semibold">
Skip to main content
</Text>
</Pressable>
);
}tsx
import { useState } from 'react';
import { Pressable } from '@/components/ui/pressable';
import { Text } from '@/components/ui/text';
function SkipLink() {
const [isFocused, setIsFocused] = useState(false);
return (
<Pressable
onPress={() => {
// 聚焦主内容
document.getElementById('main-content')?.focus();
}}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
accessibilityRole="link"
accessibilityLabel="跳转到主内容"
className={cn(
'absolute left-4 z-50 px-4 py-2 bg-primary-500 rounded-md',
'transition-all duration-200',
isFocused ? 'top-4' : '-top-20'
)}
>
<Text className="text-typography-0 font-semibold">
跳转到主内容
</Text>
</Pressable>
);
}Loading State Announcement
加载状态通知
tsx
import { useEffect } from 'react';
import { AccessibilityInfo } from 'react-native';
import { Spinner } from '@/components/ui/spinner';
import { Text } from '@/components/ui/text';
import { VStack } from '@/components/ui/vstack';
function LoadingState({ isLoading, loadingText = 'Loading...' }: {
isLoading: boolean;
loadingText?: string;
}) {
useEffect(() => {
if (isLoading) {
AccessibilityInfo.announceForAccessibility(loadingText);
}
}, [isLoading, loadingText]);
if (!isLoading) return null;
return (
<VStack
space="sm"
alignItems="center"
accessibilityRole="progressbar"
accessibilityLabel={loadingText}
accessibilityLiveRegion="polite"
>
<Spinner size="large" />
<Text className="text-typography-500">{loadingText}</Text>
</VStack>
);
}tsx
import { useEffect } from 'react';
import { AccessibilityInfo } from 'react-native';
import { Spinner } from '@/components/ui/spinner';
import { Text } from '@/components/ui/text';
import { VStack } from '@/components/ui/vstack';
function LoadingState({ isLoading, loadingText = '加载中...' }: {
isLoading: boolean;
loadingText?: string;
}) {
useEffect(() => {
if (isLoading) {
AccessibilityInfo.announceForAccessibility(loadingText);
}
}, [isLoading, loadingText]);
if (!isLoading) return null;
return (
<VStack
space="sm"
alignItems="center"
accessibilityRole="progressbar"
accessibilityLabel={loadingText}
accessibilityLiveRegion="polite"
>
<Spinner size="large" />
<Text className="text-typography-500">{loadingText}</Text>
</VStack>
);
}Anti-Patterns
反模式
Do Not Hide Interactive Elements
不要隐藏交互元素
tsx
// Bad: Interactive element hidden from accessibility
<Pressable
onPress={handlePress}
importantForAccessibility="no"
>
<Text>Click me</Text>
</Pressable>
// Good: Interactive element accessible
<Pressable
onPress={handlePress}
accessibilityRole="button"
accessibilityLabel="Perform action"
>
<Text>Click me</Text>
</Pressable>tsx
// 不佳:交互元素对无障碍功能隐藏
<Pressable
onPress={handlePress}
importantForAccessibility="no"
>
<Text>点击我</Text>
</Pressable>
// 良好:交互元素支持无障碍
<Pressable
onPress={handlePress}
accessibilityRole="button"
accessibilityLabel="执行操作"
>
<Text>点击我</Text>
</Pressable>Do Not Use Color Alone to Convey Information
不要仅通过颜色传递信息
tsx
// Bad: Only color indicates error
<Input>
<InputField className="border-error-500" />
</Input>
// Good: Color plus icon and text
<FormControl isInvalid>
<Input>
<InputField />
</Input>
<FormControlError>
<FormControlErrorIcon as={AlertCircleIcon} />
<FormControlErrorText>This field is required</FormControlErrorText>
</FormControlError>
</FormControl>tsx
// 不佳:仅通过颜色标识错误
<Input>
<InputField className="border-error-500" />
</Input>
// 良好:颜色+图标+文本
<FormControl isInvalid>
<Input>
<InputField />
</Input>
<FormControlError>
<FormControlErrorIcon as={AlertCircleIcon} />
<FormControlErrorText>此字段为必填项</FormControlErrorText>
</FormControlError>
</FormControl>Do Not Remove Focus Indicators
不要移除焦点指示器
tsx
// Bad: Removing focus outline
<Pressable className="focus:outline-none">
<Text>Click</Text>
</Pressable>
// Good: Visible focus indicator
<Pressable className="focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded-lg">
<Text>Click</Text>
</Pressable>tsx
// 不佳:移除焦点轮廓
<Pressable className="focus:outline-none">
<Text>点击</Text>
</Pressable>
// 良好:可见的焦点指示器
<Pressable className="focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded-lg">
<Text>点击</Text>
</Pressable>Do Not Use Placeholder as Label
不要将占位符作为标签
tsx
// Bad: Placeholder only
<Input>
<InputField placeholder="Email" />
</Input>
// Good: Proper label
<FormControl>
<FormControlLabel>
<FormControlLabelText>Email</FormControlLabelText>
</FormControlLabel>
<Input>
<InputField placeholder="name@example.com" />
</Input>
</FormControl>tsx
// 不佳:仅使用占位符
<Input>
<InputField placeholder="邮箱" />
</Input>
// 良好:使用正确的标签
<FormControl>
<FormControlLabel>
<FormControlLabelText>邮箱</FormControlLabelText>
</FormControlLabel>
<Input>
<InputField placeholder="name@example.com" />
</Input>
</FormControl>WCAG 2.1 AA Checklist
WCAG 2.1 AA检查清单
Perceivable
可感知性
- Text has 4.5:1 contrast ratio (3:1 for large text)
- Images have alt text
- Form inputs have visible labels
- Content is readable when zoomed to 200%
- Color is not the only means of conveying information
- 文本对比度达到4.5:1(大文本3:1)
- 图片包含替代文本
- 表单输入框有可见标签
- 内容放大到200%时仍可读
- 不单独使用颜色传递信息
Operable
可操作性
- All functionality available via keyboard
- Focus order is logical
- Focus indicators are visible
- Touch targets are at least 44x44 points
- Users have enough time to read and interact
- 所有功能可通过键盘访问
- 焦点顺序符合逻辑
- 焦点指示器可见
- 触摸目标至少为44x44点
- 用户有足够时间阅读和交互
Understandable
可理解性
- Language is specified
- Navigation is consistent
- Form errors are identified and described
- Labels and instructions are provided
- 指定页面语言
- 导航方式一致
- 表单错误被标识并描述
- 提供标签和说明
Robust
健壮性
- Valid markup/component structure
- Name, role, and value are programmatically determined
- Status messages are announced to screen readers
- 标记/组件结构有效
- 名称、角色和值可通过程序获取
- 状态变更会通知屏幕阅读器
Related Skills
相关技能
- gluestack-components: Building UI with gluestack-ui components
- gluestack-theming: Customizing themes and design tokens
- gluestack-components: 使用gluestack-ui组件构建UI
- gluestack-theming: 自定义主题和设计令牌