--- name: gluestack-accessibility description: Use when ensuring accessible gluestack-ui implementations. Covers WAI-ARIA patterns, screen reader support, keyboard navigation, focus management, and WCAG 2.1 AA compliance. allowed-tools: - Read - Write - Edit - Bash - Grep - Glob --- # gluestack-ui - Accessibility Expert knowledge of building accessible user interfaces with gluestack-ui, ensuring WCAG 2.1 AA compliance across React and React Native platforms. ## 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. ## Key Concepts ### Built-in Accessibility gluestack-ui components include accessibility features out of the box: ```tsx // Button automatically has role="button" and handles focus // Modal manages focus trap and escape key handling Content // Form controls link labels to inputs Email ``` ### Accessibility Props React Native accessibility props supported by gluestack-ui: ```tsx ``` ### ARIA Attributes for Web For web platforms, use ARIA attributes: ```tsx import { Platform } from 'react-native'; ``` ## Screen Reader Support ### Meaningful Labels Provide descriptive labels for interactive elements: ```tsx // Bad: No context for screen reader users // Good: Clear accessibility label ``` ### 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 ( {results.map((item) => ( {item.name} ))} ); } ``` ### Image Accessibility Always provide alt text for images: ```tsx import { Image } from '@/components/ui/image'; // Informative image {`${product.name} // Decorative image (hide from screen readers) ``` ## 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(null); useEffect(() => { if (isOpen) { // Focus the search input when modal opens searchInputRef.current?.focus(); } }, [isOpen]); return ( Search ); } ``` ### 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 ( {children} ); } ``` ### 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(null); useKeyboardShortcut('k', () => { inputRef.current?.focus(); }); return ( ); } ``` ## 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 ( {label} {error ? ( {error} ) : helperText ? ( {helperText} ) : null} ); } ``` ### Error Announcement Announce form errors to screen readers: ```tsx import { AccessibilityInfo } from 'react-native'; function FormWithValidation() { const [errors, setErrors] = useState>({}); const validateAndSubmit = () => { const newErrors: Record = {}; 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 ( ); } ``` ### Required Field Indication Clearly indicate required fields: ```tsx function RequiredLabel({ label }: { label: string }) { return ( {label} {' *'} ); } ``` ## Best Practices ### 1. Use Semantic Components Choose appropriate components for their semantic meaning: ```tsx // Good: Semantic components Page Title // Bad: Generic elements for interactive content Submit ``` ### 2. Provide Sufficient Color Contrast Ensure text meets WCAG contrast requirements (4.5:1 for normal text, 3:1 for large text): ```tsx // Good: High contrast Readable text // Bad: Low contrast Hard to read text ``` ### 3. Support Reduced Motion Respect user preferences for reduced motion: ```tsx import { useReducedMotion } from 'react-native-reanimated'; function AnimatedCard({ children }: { children: React.ReactNode }) { const reducedMotion = useReducedMotion(); return ( {children} ); } ``` ### 4. Handle Touch Target Sizes Ensure touch targets are at least 44x44 points: ```tsx // Good: Adequate touch target // Or use Pressable with hitSlop ``` ### 5. Group Related Elements Use accessibility groups for related content: ```tsx {product.name} {product.description} {formatPrice(product.price)} ``` ## 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 ( {items.map((item) => { const isActive = currentPath === item.href; return ( navigate(item.href)} className={cn( 'px-4 py-2 rounded-lg', isActive ? 'bg-primary-500' : 'bg-transparent hover:bg-background-100' )} > {item.label} ); })} ); } ``` ### 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 { key: keyof T; header: string; accessibilityLabel?: string; } interface AccessibleTableProps { columns: Column[]; data: T[]; caption: string; } function AccessibleTable({ columns, data, caption, }: AccessibleTableProps) { return ( {/* Caption for screen readers */} {caption} {/* Header Row */} {columns.map((column) => ( {column.header} ))} {/* Data Rows */} {data.map((row, rowIndex) => ( {columns.map((column) => ( {String(row[column.key])} ))} ))} ); } ``` ### 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 = { 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 ( {title} {message} ); } ``` ## 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 ( { // 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' )} > Skip to main content ); } ``` ### 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 ( {loadingText} ); } ``` ## Anti-Patterns ### Do Not Hide Interactive Elements ```tsx // Bad: Interactive element hidden from accessibility Click me // Good: Interactive element accessible Click me ``` ### Do Not Use Color Alone to Convey Information ```tsx // Bad: Only color indicates error // Good: Color plus icon and text This field is required ``` ### Do Not Remove Focus Indicators ```tsx // Bad: Removing focus outline Click // Good: Visible focus indicator Click ``` ### Do Not Use Placeholder as Label ```tsx // Bad: Placeholder only // Good: Proper label Email ``` ## WCAG 2.1 AA Checklist ### 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 ### 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 ### 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