This skill activates when implementing accessible React applications, ensuring WCAG 2.1 AA compliance, managing keyboard navigation, adding ARIA attributes, or leveraging Chakra UI's built-in accessibility features. It provides comprehensive guidance on focus management, screen reader support, semantic HTML, and testing for accessibility.
/plugin marketplace add Lobbi-Docs/claude/plugin install chakra-react-toolkit@claude-orchestrationThis skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill activates when implementing accessible React applications, ensuring WCAG 2.1 AA compliance, managing keyboard navigation, adding ARIA attributes, or leveraging Chakra UI's built-in accessibility features. It provides comprehensive guidance on focus management, screen reader support, semantic HTML, and testing for accessibility.
Ensure your application meets Web Content Accessibility Guidelines Level AA:
Perceivable: Information must be presentable to users in ways they can perceive.
Operable: User interface components must be operable.
Understandable: Information and operation must be understandable.
Robust: Content must be robust enough to work with assistive technologies.
Use semantic HTML elements for proper document structure and screen reader navigation:
import { Box, Container, Heading, Text } from '@chakra-ui/react';
// Use semantic HTML with 'as' prop
<Box as="main" role="main">
<Container maxW="container.lg">
<Box as="article">
<Heading as="h1" size="2xl" mb={4}>
Article Title
</Heading>
<Box as="section" mb={8}>
<Heading as="h2" size="xl" mb={3}>
Section Heading
</Heading>
<Text as="p">
Section content with proper paragraph structure.
</Text>
</Box>
<Box as="aside" borderLeft="4px" borderColor="blue.500" pl={4}>
<Text>Complementary information</Text>
</Box>
</Box>
</Container>
</Box>
// Navigation with proper semantics
<Box as="nav" role="navigation" aria-label="Main navigation">
<Box as="ul" listStyleType="none" display="flex" gap={4}>
<Box as="li">
<Link href="/">Home</Link>
</Box>
<Box as="li">
<Link href="/about">About</Link>
</Box>
</Box>
</Box>
// Skip to content link
<Link
href="#main-content"
position="absolute"
left="-999px"
_focus={{
left: 4,
top: 4,
zIndex: 9999,
bg: 'white',
p: 2,
}}
>
Skip to main content
</Link>
<Box id="main-content" as="main" tabIndex={-1}>
{/* Main content */}
</Box>
Implement proper keyboard navigation and focus management:
import { useRef, useEffect } from 'react';
import {
Box,
Button,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
useDisclosure,
} from '@chakra-ui/react';
// Focus trap in modal (Chakra handles this automatically)
function AccessibleModal() {
const { isOpen, onOpen, onClose } = useDisclosure();
const initialRef = useRef(null);
const finalRef = useRef(null);
return (
<>
<Button ref={finalRef} onClick={onOpen}>
Open Modal
</Button>
<Modal
isOpen={isOpen}
onClose={onClose}
initialFocusRef={initialRef}
finalFocusRef={finalRef}
>
<ModalOverlay />
<ModalContent>
<ModalHeader>Modal Title</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Input
ref={initialRef}
placeholder="Focus moves here on open"
/>
</ModalBody>
</ModalContent>
</Modal>
</>
);
}
// Focus management on route change
function FocusOnRouteChange() {
const mainRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Focus main content after route change
mainRef.current?.focus();
}, [location.pathname]);
return (
<Box
ref={mainRef}
as="main"
tabIndex={-1}
outline="none"
>
{/* Page content */}
</Box>
);
}
// Custom focus visible styles
<Button
_focusVisible={{
outline: '2px solid',
outlineColor: 'blue.500',
outlineOffset: '2px',
}}
>
Accessible Button
</Button>
Implement keyboard shortcuts with proper ARIA announcements:
import { useEffect } from 'react';
import { useToast } from '@chakra-ui/react';
function KeyboardShortcuts() {
const toast = useToast();
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Command/Ctrl + K for search
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
// Open search modal
toast({
title: 'Search opened',
description: 'Press Escape to close',
status: 'info',
duration: 2000,
});
}
// Escape to close
if (e.key === 'Escape') {
// Close modal/dialog
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [toast]);
return (
<Box>
{/* Include keyboard shortcuts guide */}
<Box
as="section"
aria-label="Keyboard shortcuts"
display="none"
_focus={{ display: 'block' }}
>
<Heading size="md">Keyboard Shortcuts</Heading>
<Box as="dl">
<Box as="dt">Cmd/Ctrl + K</Box>
<Box as="dd">Open search</Box>
<Box as="dt">Escape</Box>
<Box as="dd">Close modal</Box>
</Box>
</Box>
</Box>
);
}
Manage tab order and implement focus traps:
import { useRef, useEffect } from 'react';
function FocusTrap({ children, isActive }: { children: React.ReactNode; isActive: boolean }) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isActive) return;
const container = containerRef.current;
if (!container) return;
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
container.addEventListener('keydown', handleTabKey);
firstElement?.focus();
return () => container.removeEventListener('keydown', handleTabKey);
}, [isActive]);
return (
<Box ref={containerRef}>
{children}
</Box>
);
}
// Usage in drawer or sidebar
<FocusTrap isActive={isOpen}>
<Box>
<Button onClick={onClose}>Close</Button>
<Input placeholder="First focusable element" />
<Button>Submit</Button>
</Box>
</FocusTrap>
Use ARIA attributes to provide context for assistive technologies:
import { IconButton, Button, Box, Input } from '@chakra-ui/react';
import { SearchIcon, CloseIcon } from '@chakra-ui/icons';
// Icon buttons need aria-label
<IconButton
aria-label="Search"
icon={<SearchIcon />}
onClick={handleSearch}
/>
<IconButton
aria-label="Close modal"
icon={<CloseIcon />}
onClick={onClose}
/>
// Buttons with visible text don't need aria-label
<Button onClick={handleSubmit}>
Submit Form
</Button>
// Descriptive text with aria-describedby
<Box>
<Input
id="email"
aria-describedby="email-helper"
placeholder="Email address"
/>
<Text id="email-helper" fontSize="sm" color="gray.600">
We'll never share your email with anyone else.
</Text>
</Box>
// Loading state
<Button
isLoading
loadingText="Submitting"
aria-busy="true"
aria-live="polite"
>
Submit
</Button>
// Error messages
<Box>
<Input
id="username"
isInvalid={!!error}
aria-invalid={!!error}
aria-describedby="username-error"
/>
{error && (
<Text
id="username-error"
color="red.500"
role="alert"
aria-live="assertive"
>
{error}
</Text>
)}
</Box>
Announce dynamic content changes to screen readers:
import { Box, useToast } from '@chakra-ui/react';
// Status messages
<Box
role="status"
aria-live="polite"
aria-atomic="true"
position="absolute"
left="-10000px"
width="1px"
height="1px"
overflow="hidden"
>
{statusMessage}
</Box>
// Alert messages (urgent)
<Box
role="alert"
aria-live="assertive"
aria-atomic="true"
>
{errorMessage}
</Box>
// Use Chakra's toast for announcements
function AccessibleToast() {
const toast = useToast();
const showToast = () => {
toast({
title: 'Action completed',
description: 'Your changes have been saved.',
status: 'success',
duration: 5000,
isClosable: true,
// Toast automatically includes proper ARIA
});
};
return <Button onClick={showToast}>Save</Button>;
}
// Loading spinner with announcement
<Box>
<Spinner />
<Box
as="span"
position="absolute"
left="-10000px"
aria-live="polite"
aria-busy="true"
>
Loading content...
</Box>
</Box>
Manage disclosure widgets and expandable content:
import { useState } from 'react';
import { Box, Button, Collapse } from '@chakra-ui/react';
// Accordion pattern
function AccessibleAccordion() {
const [isOpen, setIsOpen] = useState(false);
return (
<Box>
<Button
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-controls="accordion-content"
rightIcon={isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
>
Toggle Section
</Button>
<Collapse in={isOpen}>
<Box id="accordion-content" p={4}>
Collapsible content
</Box>
</Collapse>
</Box>
);
}
// Dropdown menu
function AccessibleMenu() {
const { isOpen, onToggle } = useDisclosure();
return (
<Box position="relative">
<Button
onClick={onToggle}
aria-haspopup="true"
aria-expanded={isOpen}
aria-controls="dropdown-menu"
>
Menu
</Button>
{isOpen && (
<Box
id="dropdown-menu"
role="menu"
position="absolute"
bg="white"
shadow="lg"
borderRadius="md"
p={2}
>
<Button
role="menuitem"
variant="ghost"
w="full"
justifyContent="flex-start"
>
Option 1
</Button>
<Button
role="menuitem"
variant="ghost"
w="full"
justifyContent="flex-start"
>
Option 2
</Button>
</Box>
)}
</Box>
);
}
Leverage Chakra's built-in form accessibility:
import {
FormControl,
FormLabel,
FormErrorMessage,
FormHelperText,
Input,
Select,
Checkbox,
Radio,
RadioGroup,
Stack,
} from '@chakra-ui/react';
// Form with proper labeling
<FormControl id="email" isRequired isInvalid={!!emailError}>
<FormLabel>Email address</FormLabel>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<FormHelperText>We'll never share your email.</FormHelperText>
{emailError && (
<FormErrorMessage>{emailError}</FormErrorMessage>
)}
</FormControl>
// Select with proper labeling
<FormControl id="country">
<FormLabel>Country</FormLabel>
<Select placeholder="Select country">
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="mx">Mexico</option>
</Select>
</FormControl>
// Radio group
<FormControl as="fieldset">
<FormLabel as="legend">Notification preference</FormLabel>
<RadioGroup defaultValue="email">
<Stack spacing={4}>
<Radio value="email">Email</Radio>
<Radio value="sms">SMS</Radio>
<Radio value="both">Both</Radio>
</Stack>
</RadioGroup>
</FormControl>
// Checkbox with description
<Checkbox defaultChecked>
I agree to the{' '}
<Link href="/terms" textDecoration="underline">
terms and conditions
</Link>
</Checkbox>
Use appropriate components for actions:
import { Button, Link, IconButton } from '@chakra-ui/react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
// Button for actions (no href)
<Button onClick={handleSubmit} isLoading={isSubmitting}>
Submit
</Button>
// Link for navigation (has href)
<Button as="a" href="/page">
Go to Page
</Button>
// External link
<Link href="https://example.com" isExternal>
External Site <ExternalLinkIcon mx="2px" />
</Link>
// Disabled state with explanation
<Tooltip label="Complete the form to enable">
<Button isDisabled>
Submit
</Button>
</Tooltip>
// Icon button with label
<IconButton
aria-label="Delete item"
icon={<DeleteIcon />}
colorScheme="red"
variant="ghost"
/>
Use Chakra's modal components with proper ARIA:
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
ModalFooter,
AlertDialog,
AlertDialogOverlay,
AlertDialogContent,
AlertDialogHeader,
AlertDialogBody,
AlertDialogFooter,
} from '@chakra-ui/react';
// Standard modal
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Modal Title</ModalHeader>
<ModalCloseButton />
<ModalBody>
Modal content
</ModalBody>
<ModalFooter>
<Button onClick={onClose}>Close</Button>
</ModalFooter>
</ModalContent>
</Modal>
// Alert dialog for destructive actions
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader>Delete Account</AlertDialogHeader>
<AlertDialogBody>
Are you sure? This action cannot be undone.
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}>
Cancel
</Button>
<Button colorScheme="red" onClick={handleDelete} ml={3}>
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
Ensure sufficient color contrast (4.5:1 for normal text, 3:1 for large text):
import { Box, Text, useColorModeValue } from '@chakra-ui/react';
// High contrast text
<Box bg="blue.600">
<Text color="white" fontSize="md">
{/* 7:1 contrast ratio - AAA compliant */}
High contrast text
</Text>
</Box>
// Use Chakra's color tokens for accessible combinations
<Box bg="gray.100">
<Text color="gray.900">
{/* Meets WCAG AA */}
Dark text on light background
</Text>
</Box>
// Avoid low contrast
// ❌ Bad: gray.400 on white (2.6:1)
// ✅ Good: gray.600 on white (4.7:1)
<Text color="gray.600">Accessible text</Text>
// Test contrast programmatically
const bgColor = useColorModeValue('white', 'gray.800');
const textColor = useColorModeValue('gray.900', 'white');
<Box bg={bgColor}>
<Text color={textColor}>
Auto-adjusting for color mode
</Text>
</Box>
Don't rely solely on color to convey information:
// Use icons with color
<HStack spacing={2}>
<CheckIcon color="green.500" />
<Text>Success</Text>
</HStack>
<HStack spacing={2}>
<WarningIcon color="red.500" />
<Text>Error</Text>
</HStack>
// Use patterns with color
<Box
bg="green.500"
border="2px solid"
borderColor="green.700"
p={4}
>
<HStack>
<CheckIcon />
<Text color="white">Success message</Text>
</HStack>
</Box>
// Accessible status indicators
<Badge
colorScheme="green"
leftIcon={<CheckIcon />}
>
Active
</Badge>
<Badge
colorScheme="red"
leftIcon={<WarningIcon />}
>
Inactive
</Badge>
Perform these manual accessibility tests:
Keyboard Navigation
Screen Reader Testing
Zoom Testing
Color Contrast
Use jest-axe for automated accessibility testing:
// Install: npm install --save-dev jest-axe
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { ChakraProvider } from '@chakra-ui/react';
expect.extend(toHaveNoViolations);
describe('Accessibility', () => {
it('should have no accessibility violations', async () => {
const { container } = render(
<ChakraProvider>
<YourComponent />
</ChakraProvider>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('form should be accessible', async () => {
const { container } = render(
<ChakraProvider>
<FormControl id="email">
<FormLabel>Email</FormLabel>
<Input type="email" />
</FormControl>
</ChakraProvider>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
Test keyboard navigation and focus:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('Focus Management', () => {
it('should trap focus in modal', async () => {
const user = userEvent.setup();
render(
<Modal isOpen onClose={onClose}>
<ModalContent>
<Button id="first">First</Button>
<Button id="last">Last</Button>
</ModalContent>
</Modal>
);
const firstButton = screen.getByRole('button', { name: /first/i });
const lastButton = screen.getByRole('button', { name: /last/i });
// Focus should start on first element
expect(firstButton).toHaveFocus();
// Tab to last element
await user.tab();
expect(lastButton).toHaveFocus();
// Tab should cycle back to first
await user.tab();
expect(firstButton).toHaveFocus();
});
});
Use these accessibility patterns consistently to create inclusive applications that work for all users, including those using assistive technologies.