React Aria Modal is a powerful library for creating accessible modal dialogs in React applications. It provides comprehensive ARIA support, focus management, and keyboard navigation out of the box, ensuring your modals are usable by all users including those using assistive technologies. This guide walks through implementing advanced, accessible modal dialogs using React Aria Modal with React, covering focus management, keyboard handling, and accessibility patterns. This is part 27 of a series on using React Aria Modal with React.
Prerequisites
Before you begin, ensure you have:
- Node.js version 16.0 or higher
- npm, yarn, or pnpm package manager
- A React project (version 16.8 or higher) with hooks support
- Advanced understanding of React hooks, refs, and accessibility concepts
- Familiarity with ARIA attributes and accessibility best practices
- Knowledge of TypeScript (recommended)
Installation
Install React Aria Modal using your preferred package manager:
npm install react-aria-modal
Or with yarn:
yarn add react-aria-modal
Or with pnpm:
pnpm add react-aria-modal
Your package.json should include:
{
"dependencies": {
"react-aria-modal": "^4.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
Project Setup
React Aria Modal requires minimal setup. No additional configuration is needed beyond installation.
First Example / Basic Usage
Let's create a basic modal component. Create src/Modal.jsx:
// src/Modal.jsx
import React, { useState } from 'react';
import AriaModal from 'react-aria-modal';
function Modal() {
const [modalActive, setModalActive] = useState(false);
const activateModal = () => {
setModalActive(true);
};
const deactivateModal = () => {
setModalActive(false);
};
const getApplicationNode = () => {
return document.getElementById('root');
};
return (
<div style={{ padding: '20px' }}>
<button onClick={activateModal}>
Open Modal
</button>
{modalActive && (
<AriaModal
onExit={deactivateModal}
getApplicationNode={getApplicationNode}
underlayStyle={{ paddingTop: '2em' }}
>
<div style={{ background: 'white', padding: '20px', borderRadius: '8px' }}>
<h2>Modal Title</h2>
<p>This is a basic modal dialog.</p>
<button onClick={deactivateModal}>
Close
</button>
</div>
</AriaModal>
)}
</div>
);
}
export default Modal;
Understanding the Basics
React Aria Modal provides accessible modal functionality:
- AriaModal: Main component that wraps modal content
- onExit: Callback when modal should close
- getApplicationNode: Function that returns the application root node
- underlayStyle: Styles for the modal backdrop
Key concepts for advanced usage:
- Focus Management: Automatically traps and manages focus
- ARIA Attributes: Adds proper ARIA attributes for screen readers
- Keyboard Navigation: Handles Escape key and focus trapping
- Application Node: Used to hide main content from screen readers
Here's an example with custom styling:
// src/StyledModal.jsx
import React, { useState } from 'react';
import AriaModal from 'react-aria-modal';
function StyledModal() {
const [modalActive, setModalActive] = useState(false);
const getApplicationNode = () => document.getElementById('root');
return (
<div>
<button onClick={() => setModalActive(true)}>
Open Styled Modal
</button>
{modalActive && (
<AriaModal
onExit={() => setModalActive(false)}
getApplicationNode={getApplicationNode}
underlayStyle={{
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
right: 0,
zIndex: 1040,
backgroundColor: 'rgba(0, 0, 0, 0.5)'
}}
underlayClass="modal-underlay"
>
<div
style={{
position: 'relative',
backgroundColor: 'white',
borderRadius: '8px',
padding: '24px',
maxWidth: '500px',
margin: '0 auto',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
}}
>
<h2 style={{ marginTop: 0 }}>Styled Modal</h2>
<p>This modal has custom styling.</p>
<button onClick={() => setModalActive(false)}>
Close
</button>
</div>
</AriaModal>
)}
</div>
);
}
export default StyledModal;
Practical Example / Building Something Real
Let's build a comprehensive modal system with form handling and advanced features:
// src/AdvancedModal.jsx
import React, { useState, useRef, useEffect } from 'react';
import AriaModal from 'react-aria-modal';
interface FormData {
name: string;
email: string;
message: string;
}
function AdvancedModal() {
const [modalActive, setModalActive] = useState(false);
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
message: ''
});
const [errors, setErrors] = useState<Partial<FormData>>({});
const firstInputRef = useRef<HTMLInputElement>(null);
const getApplicationNode = () => document.getElementById('root');
useEffect(() => {
if (modalActive && firstInputRef.current) {
firstInputRef.current.focus();
}
}, [modalActive]);
const handleInputChange = (field: keyof FormData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
};
const validateForm = (): boolean => {
const newErrors: Partial<FormData> = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Invalid email format';
}
if (!formData.message.trim()) {
newErrors.message = 'Message is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
console.log('Form submitted:', formData);
setModalActive(false);
setFormData({ name: '', email: '', message: '' });
}
};
const handleClose = () => {
setModalActive(false);
setFormData({ name: '', email: '', message: '' });
setErrors({});
};
return (
<div style={{ padding: '20px' }}>
<button
onClick={() => setModalActive(true)}
style={{
padding: '10px 20px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Open Contact Modal
</button>
{modalActive && (
<AriaModal
onExit={handleClose}
getApplicationNode={getApplicationNode}
underlayStyle={{
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
right: 0,
zIndex: 1040,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
focusDialog={true}
titleId="modal-title"
>
<div
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
style={{
position: 'relative',
backgroundColor: 'white',
borderRadius: '8px',
padding: '24px',
maxWidth: '500px',
width: '90%',
maxHeight: '90vh',
overflow: 'auto',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
}}
>
<h2 id="modal-title" style={{ marginTop: 0 }}>
Contact Us
</h2>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '16px' }}>
<label htmlFor="name" style={{ display: 'block', marginBottom: '4px' }}>
Name *
</label>
<input
ref={firstInputRef}
id="name"
type="text"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: `1px solid ${errors.name ? '#dc3545' : '#ddd'}`,
borderRadius: '4px'
}}
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined}
/>
{errors.name && (
<div id="name-error" style={{ color: '#dc3545', fontSize: '14px', marginTop: '4px' }}>
{errors.name}
</div>
)}
</div>
<div style={{ marginBottom: '16px' }}>
<label htmlFor="email" style={{ display: 'block', marginBottom: '4px' }}>
Email *
</label>
<input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: `1px solid ${errors.email ? '#dc3545' : '#ddd'}`,
borderRadius: '4px'
}}
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<div id="email-error" style={{ color: '#dc3545', fontSize: '14px', marginTop: '4px' }}>
{errors.email}
</div>
)}
</div>
<div style={{ marginBottom: '16px' }}>
<label htmlFor="message" style={{ display: 'block', marginBottom: '4px' }}>
Message *
</label>
<textarea
id="message"
value={formData.message}
onChange={(e) => handleInputChange('message', e.target.value)}
rows={4}
style={{
width: '100%',
padding: '8px',
border: `1px solid ${errors.message ? '#dc3545' : '#ddd'}`,
borderRadius: '4px',
resize: 'vertical'
}}
aria-invalid={!!errors.message}
aria-describedby={errors.message ? 'message-error' : undefined}
/>
{errors.message && (
<div id="message-error" style={{ color: '#dc3545', fontSize: '14px', marginTop: '4px' }}>
{errors.message}
</div>
)}
</div>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<button
type="button"
onClick={handleClose}
style={{
padding: '8px 16px',
border: '1px solid #ddd',
backgroundColor: 'white',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Cancel
</button>
<button
type="submit"
style={{
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Submit
</button>
</div>
</form>
</div>
</AriaModal>
)}
</div>
);
}
export default AdvancedModal;
This advanced example demonstrates:
- Form handling with validation
- Focus management with refs
- Error handling and accessibility
- ARIA attributes for screen readers
- Keyboard navigation support
- Custom styling and layout
- Proper modal structure
Common Issues / Troubleshooting
Modal not displaying: Ensure
modalActivestate is properly managed and the modal is conditionally rendered. Check thatgetApplicationNodereturns a valid DOM element.Focus not trapped: Verify that
focusDialog={true}is set. React Aria Modal automatically handles focus trapping, but this prop ensures proper behavior.Accessibility issues: Always provide
titleIdand usearia-labelledbyon the modal content. Ensure form inputs have proper labels and error messages are associated with inputs usingaria-describedby.Styling issues: The
underlayStyleprop controls the backdrop. For centered modals, use flexbox inunderlayStylewithdisplay: 'flex',alignItems: 'center', andjustifyContent: 'center'.Application node errors: Ensure
getApplicationNodereturns the actual application root element. This is used to hide the main content from screen readers when the modal is open.
Next Steps
Now that you've mastered React Aria Modal:
- Explore advanced focus management patterns
- Implement nested modals and modal stacking
- Add animations and transitions
- Learn about other accessibility libraries (react-aria, reach-ui)
- Implement modal state management with context
- Add analytics and tracking
- Check the official repository: https://github.com/davidtheclark/react-aria-modal
- Look for part 28 of this series for more advanced topics
Summary
You've learned how to implement advanced, accessible modal dialogs with React Aria Modal, including focus management, keyboard navigation, and proper ARIA attributes. The library provides comprehensive accessibility support out of the box, making it easy to build modals that work for all users.
SEO Keywords
react-aria-modal
React accessible modal
react-aria-modal tutorial
React ARIA modal dialog
react-aria-modal installation
React accessibility modal
react-aria-modal example
React focus management
react-aria-modal setup
React keyboard navigation
react-aria-modal accessibility
React modal component
react-aria-modal ARIA
React accessible dialog
react-aria-modal getting started
Top comments (0)