Loading Developer Playground

Loading ...

Skip to main content
ARIA ATTRIBUTEWidget Attributes

aria-modal

Indicates whether an element is modal when displayed. When true, background content is hidden from assistive technologies and focus is trapped within the dialog.

Value Type
true | false
Common Use
Modal Dialogs
Applies To
dialog, alertdialog

Overview

The aria-modal attribute indicates whether an element is modal. When set to true, assistive technologies should navigate only within the dialog, ignoring background content.

This is essential for accessible modal dialogs. Combined with proper focus management, it ensures screen reader users can't accidentally navigate to content behind the modal.

Focus Management Required

aria-modal="true" alone isn't enough. You must also implement focus trapping (Tab/Shift+Tab stay in modal) and keyboard handling (Escape to close).

Live Demo

Screen reader: "Modal Dialog Demo, dialog". With aria-modal="true", only content inside the dialog is accessible. Background is hidden from AT.

Code Examples

Basic Modal

<!-- Basic modal dialog with aria-modal -->
<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
  aria-describedby="modal-description"
>
  <h2 id="modal-title">Confirm Action</h2>
  <p id="modal-description">
    Are you sure you want to delete this item?
  </p>
  <button>Cancel</button>
  <button>Delete</button>
</div>

<!-- Screen reader: "Confirm Action, dialog" -->
<!-- Background content is hidden from AT when aria-modal="true" -->

Modal vs Non-Modal

<!-- aria-modal=true vs aria-modal=false -->

<!-- MODAL dialog (aria-modal="true") -->
<div role="dialog" aria-modal="true" aria-label="Settings">
  <!-- 
    • Background content hidden from screen readers
    • Focus is trapped inside the dialog
    • User must dismiss to interact with page
    • Use for: confirmations, alerts, forms
  -->
</div>

<!-- NON-MODAL dialog (aria-modal="false" or absent) -->
<div role="dialog" aria-modal="false" aria-label="Help">
  <!--
    • Background content still accessible
    • Focus can move outside the dialog
    • User can interact with page while open
    • Use for: tooltips, help panels, sidebars
  -->
</div>

Alert Dialog

<!-- Alert dialog (always modal) -->
<div
  role="alertdialog"
  aria-modal="true"
  aria-labelledby="alert-title"
  aria-describedby="alert-desc"
>
  <h2 id="alert-title">⚠️ Warning</h2>
  <p id="alert-desc">
    You have unsaved changes. Leaving now will discard them.
  </p>
  <div class="button-group">
    <button>Stay</button>
    <button>Leave Anyway</button>
  </div>
</div>

<!-- alertdialog implies modal behavior, but still add aria-modal -->

React Component

// React Modal Component with aria-modal
import { useEffect, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom';

function Modal({ 
  isOpen, 
  onClose, 
  title, 
  children,
  role = 'dialog' // or 'alertdialog'
}) {
  const modalRef = useRef(null);
  const previousFocus = useRef(null);

  // Store focus and trap it
  useEffect(() => {
    if (isOpen) {
      previousFocus.current = document.activeElement;
      modalRef.current?.focus();
      
      // Prevent body scroll
      document.body.style.overflow = 'hidden';
    } else {
      document.body.style.overflow = '';
      previousFocus.current?.focus();
    }
    
    return () => {
      document.body.style.overflow = '';
    };
  }, [isOpen]);

  // Handle keyboard events
  const handleKeyDown = useCallback((e) => {
    if (e.key === 'Escape') {
      onClose();
      return;
    }
    
    // Focus trap
    if (e.key === 'Tab') {
      const focusable = modalRef.current.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      const first = focusable[0];
      const last = focusable[focusable.length - 1];
      
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault();
        last.focus();
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
  }, [onClose]);

  if (!isOpen) return null;

  return createPortal(
    <>
      {/* Backdrop */}
      <div 
        className="modal-backdrop"
        onClick={onClose}
        aria-hidden="true"
      />
      
      {/* Modal */}
      <div
        ref={modalRef}
        role={role}
        aria-modal="true"
        aria-labelledby="modal-title"
        tabIndex={-1}
        onKeyDown={handleKeyDown}
        className="modal"
      >
        <header className="modal-header">
          <h2 id="modal-title">{title}</h2>
          <button 
            onClick={onClose}
            aria-label="Close dialog"
          >
            ✕
          </button>
        </header>
        
        <div className="modal-body">
          {children}
        </div>
      </div>
    </>,
    document.body
  );
}

// Usage
function App() {
  const [isOpen, setIsOpen] = useState(false);
  
  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Open Settings
      </button>
      
      <Modal
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        title="Settings"
      >
        <p>Configure your preferences here.</p>
        <button onClick={() => setIsOpen(false)}>
          Save Changes
        </button>
      </Modal>
    </>
  );
}

Related Attributes

Specifications & Resources