gluestack-accessibility

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

gluestack-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: 自定义主题和设计令牌