Fix: React Modal App Element Not Defined
When using the popular react-modal library, you may encounter this warning that prevents your modal from working correctly with screen readers. This guide explains why this warning occurs and provides solutions for different React environments including Create React App, Next.js, Vite, and testing scenarios.
The Warning Message
After installing or upgrading react-modal, you see this warning in your console:
Warning: react-modal: App element is not defined. Please use
Modal.setAppElement(el) or set appElement={el}. This is needed
so screen readers don't see main content when modal is opened.
It is not recommended, but you can opt-out by setting
ariaHideApp={false}.
Why This Warning Occurs
The react-modal library requires knowing your app's root element for accessibility purposes. When a modal opens, react-modal:
- Adds
aria-hidden="true"to the root element - This tells screen readers to ignore the main content
- Screen reader users only hear the modal content
Without setting the app element, screen readers would read both the modal AND the background content simultaneously, creating a confusing experience for visually impaired users.
Solution 1: Set App Element Globally (Recommended)
The best approach is to set the app element once when your app initializes.
Create React App (CRA)
In your src/index.js or src/index.tsx:
import React from 'react';
import ReactDOM from 'react-dom/client';
import Modal from 'react-modal';
import App from './App';
// Set the app element for react-modal
Modal.setAppElement('#root');
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Vite
In your src/main.jsx or src/main.tsx:
import React from 'react';
import ReactDOM from 'react-dom/client';
import Modal from 'react-modal';
import App from './App';
Modal.setAppElement('#root');
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Next.js (App Router)
In Next.js with the App Router, create a client component wrapper:
// components/ModalProvider.jsx
'use client';
import { useEffect } from 'react';
import Modal from 'react-modal';
export default function ModalProvider({ children }) {
useEffect(() => {
Modal.setAppElement('#__next');
}, []);
return <>{children}</>;
}
Then use it in your root layout:
// app/layout.jsx
import ModalProvider from '@/components/ModalProvider';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<ModalProvider>
<div id="__next">{children}</div>
</ModalProvider>
</body>
</html>
);
}
Next.js (Pages Router)
In pages/_app.js:
import Modal from 'react-modal';
import { useEffect } from 'react';
// For pages router, the app element is #__next
if (typeof window !== 'undefined') {
Modal.setAppElement('#__next');
}
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;
Solution 2: Set App Element Per Modal
If you can't set it globally, pass the appElement prop to each Modal:
import Modal from 'react-modal';
function MyComponent() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
<Modal
isOpen={isOpen}
onRequestClose={() => setIsOpen(false)}
appElement={document.getElementById('root')}
>
<h2>Modal Content</h2>
<button onClick={() => setIsOpen(false)}>Close</button>
</Modal>
</>
);
}
Solution 3: Disable Accessibility Feature (Not Recommended)
If you understand the accessibility implications and choose to opt-out:
<Modal
isOpen={isOpen}
onRequestClose={() => setIsOpen(false)}
ariaHideApp={false}
>
<h2>Modal Content</h2>
</Modal>
⚠️ Warning: This approach: - Makes your app less accessible to screen reader users - Should only be used as a temporary fix - Is not recommended for production apps
Solution 4: Handling Server-Side Rendering (SSR)
For SSR environments where document isn't available during initial render:
import Modal from 'react-modal';
import { useEffect, useState } from 'react';
function MyModal({ isOpen, onClose, children }) {
const [isBrowser, setIsBrowser] = useState(false);
useEffect(() => {
setIsBrowser(true);
Modal.setAppElement('#root');
}, []);
if (!isBrowser) {
return null;
}
return (
<Modal isOpen={isOpen} onRequestClose={onClose}>
{children}
</Modal>
);
}
Solution 5: Handling in Tests
When testing components that use react-modal, you need to set up the app element in your test environment.
Jest Setup
Create or update src/setupTests.js:
import '@testing-library/jest-dom';
import Modal from 'react-modal';
// Create a div for react-modal
const modalRoot = document.createElement('div');
modalRoot.setAttribute('id', 'root');
document.body.appendChild(modalRoot);
Modal.setAppElement('#root');
Individual Test Files
If you prefer per-file setup:
import { render, screen } from '@testing-library/react';
import Modal from 'react-modal';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
beforeAll(() => {
Modal.setAppElement(document.createElement('div'));
});
it('opens modal when button clicked', () => {
render(<MyComponent />);
// ... test code
});
});
Vitest Setup
In vitest.setup.js:
import Modal from 'react-modal';
const div = document.createElement('div');
div.setAttribute('id', 'root');
document.body.appendChild(div);
Modal.setAppElement('#root');
Complete Example: Reusable Modal Component
Here's a complete, reusable modal component that handles the app element correctly:
// components/Modal.jsx
import ReactModal from 'react-modal';
import { useEffect } from 'react';
import styles from './Modal.module.css';
// Set app element once when this module loads
if (typeof window !== 'undefined') {
ReactModal.setAppElement('#root');
}
const customStyles = {
overlay: {
backgroundColor: 'rgba(0, 0, 0, 0.75)',
zIndex: 1000,
},
content: {
top: '50%',
left: '50%',
right: 'auto',
bottom: 'auto',
marginRight: '-50%',
transform: 'translate(-50%, -50%)',
padding: '20px',
borderRadius: '8px',
maxWidth: '500px',
width: '90%',
},
};
export default function Modal({
isOpen,
onClose,
title,
children
}) {
// Close on escape key
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape') onClose();
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
}
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [isOpen, onClose]);
return (
<ReactModal
isOpen={isOpen}
onRequestClose={onClose}
style={customStyles}
contentLabel={title}
>
<div className={styles.header}>
<h2>{title}</h2>
<button
onClick={onClose}
aria-label="Close modal"
>
×
</button>
</div>
<div className={styles.content}>
{children}
</div>
</ReactModal>
);
}
Common App Element Selectors
Different frameworks use different root element IDs:
| Framework | Selector |
|---|---|
| Create React App | #root |
| Vite (React) | #root |
| Next.js | #__next |
| Gatsby | #___gatsby |
| Custom | Whatever ID you gave your root <div> |
Troubleshooting
Still seeing the warning after setting appElement?
- Make sure
setAppElementruns before any Modal renders - Check if you have multiple versions of react-modal installed
- Verify the element exists when setAppElement is called
Modal doesn't close on overlay click?
Add the onRequestClose prop and shouldCloseOnOverlayClick:
<Modal
isOpen={isOpen}
onRequestClose={() => setIsOpen(false)}
shouldCloseOnOverlayClick={true}
>
Related Guides
- Bootstrap Modal Tooltip Issues - Similar modal problems in Bootstrap
Conclusion
The "App element is not defined" warning from react-modal is an accessibility feature, not a bug. The recommended fix is to call Modal.setAppElement('#root') once when your application initializes. This ensures screen readers properly handle modal focus and provides a better experience for all users. Avoid using ariaHideApp={false} unless absolutely necessary, as it degrades accessibility.