}

Fix: React Modal App Element Not Defined [react-modal Setup Guide]

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:

  1. Adds aria-hidden="true" to the root element
  2. This tells screen readers to ignore the main content
  3. 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?

  1. Make sure setAppElement runs before any Modal renders
  2. Check if you have multiple versions of react-modal installed
  3. 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

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.