0Pricing
Tailwind CSS Academy · درس

إنشاء مربعات حوار سهلة الوصول

استخدم مكوّن Dialog في Headless UI للنوافذ المنبثقة، مع احتجاز التركيز والتعامل المدمج مع مفتاح الهروب، ونسّقه بالكامل باستخدام Tailwind.

إنشاء مربعات حوار سهلة الوصول درس مجاني في Tailwind CSS Academy على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Tailwind CSS Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Tailwind CSS Academy 4 دروس في المجموع.

بعض أجزاء هذا الدرس لم تُترجم بعد وتظهر باللغة الإنجليزية.

What Makes a Dialog Accessible?

An accessible dialog (modal) must satisfy several requirements: it must receive keyboard focus when opened, trap focus within itself so users cannot Tab outside, be dismissable with the Escape key, apply correct ARIA attributes (role='dialog', aria-modal='true'), and return focus to the trigger element when closed. These requirements are complex to implement correctly. Headless UI's Dialog component handles all of them automatically.

Basic Dialog Structure

The Headless UI Dialog composes three key elements: Dialog (root, handles ARIA and focus), Dialog.Panel (the visible modal box), and optionally Dialog.Title and Dialog.Description (for semantic labeling). The open prop controls visibility, and onClose fires when the user presses Escape or clicks outside the panel — you decide what action to take (typically setting open state to false).

import { Dialog } from '@headlessui/react';
import { useState } from 'react';

function AlertDialog() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>Open Dialog</button>

      <Dialog open={open} onClose={() => setOpen(false)}>
        <Dialog.Panel>
          <Dialog.Title>Alert</Dialog.Title>
          <Dialog.Description>This is an important message.</Dialog.Description>
          <button onClick={() => setOpen(false)}>Close</button>
        </Dialog.Panel>
      </Dialog>
    </>
  );
}

Adding the Backdrop Overlay

A modal should dim the page content behind it to draw the user's attention to the dialog. Add a full-screen backdrop using fixed inset-0 with a semi-transparent background. Place it as the first child of Dialog, before the panel container. Use aria-hidden='true' on the backdrop since it is purely decorative — screen readers should not announce it.

<Dialog open={open} onClose={() => setOpen(false)} className='relative z-50'>

  {/* Backdrop */}
  <div
    className='fixed inset-0 bg-black/50 backdrop-blur-sm'
    aria-hidden='true'
  />

  {/* Panel container — centers the dialog */}
  <div className='fixed inset-0 flex items-center justify-center p-4'>
    <Dialog.Panel className='bg-white rounded-2xl shadow-2xl max-w-md w-full'>
      {/* Dialog content */}
    </Dialog.Panel>
  </div>

</Dialog>

Styling the Dialog Panel

The Dialog.Panel is the visible modal container. Apply Tailwind classes for background, border radius, shadow, padding, and max-width to create a polished card. The panel should have a max-w-* constraint so it does not stretch full-width on large screens, while remaining responsive on small screens with w-full. Add a close button in the top-right corner for mouse users who prefer clicking over pressing Escape.

<Dialog.Panel className='relative bg-white rounded-2xl shadow-2xl max-w-lg w-full p-6'>
  {/* Close button */}
  <button
    onClick={() => setOpen(false)}
    className='absolute top-4 right-4 rounded-full p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600'
  >
    <XMarkIcon className='h-5 w-5' />
    <span className='sr-only'>Close</span>
  </button>

  {/* Header */}
  <Dialog.Title className='text-lg font-semibold text-gray-900 pr-8'>
    Delete Project
  </Dialog.Title>
  <Dialog.Description className='mt-2 text-sm text-gray-600'>
    This action cannot be undone. All project data will be permanently removed.
  </Dialog.Description>

  {/* Actions */}
  <div className='mt-6 flex gap-3 justify-end'>
    <button onClick={() => setOpen(false)}
      className='px-4 py-2 text-sm font-medium rounded-lg border border-gray-300 hover:bg-gray-50'>
      Cancel
    </button>
    <button
      className='px-4 py-2 text-sm font-medium rounded-lg bg-red-600 text-white hover:bg-red-700'>
      Delete
    </button>
  </div>
</Dialog.Panel>

Scrollable Dialog for Long Content

Dialogs with long content — like terms of service, form wizards, or detailed previews — need to be scrollable without the backdrop scrolling. Apply overflow-y-auto to the panel and a max-h-* constraint so the dialog does not grow beyond the viewport. The outer centering container should be items-start with top padding to keep the dialog near the top of the screen for very long content.

// Scrollable dialog for long content
<div className='fixed inset-0 overflow-y-auto'>
  <div className='flex min-h-full items-start justify-center p-4 pt-16'>
    <Dialog.Panel
      className='
        bg-white rounded-2xl shadow-xl
        max-w-2xl w-full
        max-h-[80vh] overflow-y-auto
      '
    >
      <div className='sticky top-0 bg-white border-b border-gray-100 px-6 py-4 z-10'>
        <Dialog.Title className='text-lg font-semibold'>Terms of Service</Dialog.Title>
      </div>

      <div className='px-6 py-4 prose prose-sm'>
        {/* Long content */}
      </div>

      <div className='sticky bottom-0 bg-white border-t border-gray-100 px-6 py-4'>
        <button className='w-full bg-blue-600 text-white rounded-lg py-2'>Accept</button>
      </div>
    </Dialog.Panel>
  </div>
</div>

Dialog Size Variants

Build reusable dialog size variants using Tailwind's max-w-* utilities. Small dialogs for confirmations, medium for forms, and large for previews or multi-step wizards. Create a DialogModal component that accepts a size prop and applies the corresponding max-width class — this is a natural use case for the CVA (class-variance-authority) pattern.

const panelSizes = {
  sm: 'max-w-sm',
  md: 'max-w-md',
  lg: 'max-w-lg',
  xl: 'max-w-2xl',
  full: 'max-w-5xl'
};

function DialogModal({ open, onClose, size = 'md', title, description, children }) {
  return (
    <Dialog open={open} onClose={onClose} className='relative z-50'>
      <div className='fixed inset-0 bg-black/50' aria-hidden='true' />
      <div className='fixed inset-0 flex items-center justify-center p-4'>
        <Dialog.Panel
          className={cn(
            'bg-white rounded-2xl shadow-xl w-full p-6',
            panelSizes[size]
          )}
        >
          {title && <Dialog.Title className='text-lg font-semibold'>{title}</Dialog.Title>}
          {description && <Dialog.Description className='mt-1 text-sm text-gray-600'>{description}</Dialog.Description>}
          <div className='mt-4'>{children}</div>
        </Dialog.Panel>
      </div>
    </Dialog>
  );
}

Focus Management in Practice

Headless UI automatically moves focus into the dialog when it opens. By default, focus goes to the first focusable element inside the panel. To direct focus to a specific element — like a primary CTA or a text input — use the initialFocus prop with a React ref pointing to that element. This improves the user experience for keyboard and screen reader users who immediately need to interact with a specific control.

import { Dialog } from '@headlessui/react';
import { useRef } from 'react';

function DeleteConfirm({ open, onClose, onDelete }) {
  const cancelButtonRef = useRef(null);

  return (
    <Dialog
      open={open}
      onClose={onClose}
      initialFocus={cancelButtonRef}  // focus Cancel by default (safer)
    >
      {/* ...backdrop... */}
      <div className='fixed inset-0 flex items-center justify-center p-4'>
        <Dialog.Panel className='bg-white rounded-2xl p-6 max-w-sm w-full shadow-xl'>
          <Dialog.Title className='font-semibold text-gray-900'>Delete?</Dialog.Title>
          <div className='mt-4 flex gap-3 justify-end'>
            {/* initialFocus lands here */}
            <button ref={cancelButtonRef} onClick={onClose}
              className='px-4 py-2 text-sm border rounded-lg'>
              Cancel
            </button>
            <button onClick={onDelete}
              className='px-4 py-2 text-sm bg-red-600 text-white rounded-lg'>
              Delete
            </button>
          </div>
        </Dialog.Panel>
      </div>
    </Dialog>
  );
}

Nested Dialog Stacking

Sometimes a dialog triggers another dialog — like a confirmation within a settings modal. Use incrementally higher z-index values for nested dialogs so they layer correctly. Each dialog manages its own focus trap independently; Headless UI supports multiple open dialogs simultaneously. Use state variables for each dialog level and close them in reverse order.

function SettingsModal({ open, onClose }) {
  const [confirmOpen, setConfirmOpen] = useState(false);

  return (
    <>
      {/* Primary dialog — z-40 */}
      <Dialog open={open} onClose={onClose} className='relative z-40'>
        <div className='fixed inset-0 bg-black/40' aria-hidden='true' />
        <div className='fixed inset-0 flex items-center justify-center p-4'>
          <Dialog.Panel className='bg-white rounded-2xl p-6 max-w-lg w-full shadow-xl'>
            <h2 className='font-semibold text-lg'>Settings</h2>
            <button onClick={() => setConfirmOpen(true)}
              className='mt-4 text-red-600 text-sm'>
              Reset all settings
            </button>
          </Dialog.Panel>
        </div>
      </Dialog>

      {/* Nested confirmation — z-50 (higher) */}
      <Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)} className='relative z-50'>
        {/* ... */}
      </Dialog>
    </>
  );
}

Preventing Background Scroll

When a dialog is open, the page content behind it should not scroll. Headless UI does not handle this automatically. Add a side effect that adds overflow-hidden to the body when the dialog opens and removes it when it closes. In a Next.js or React app, use a useEffect inside the dialog component or a custom hook that cleans up correctly on unmount.

import { useEffect } from 'react';

function useBodyScrollLock(isLocked) {
  useEffect(() => {
    if (isLocked) {
      document.body.classList.add('overflow-hidden');
    } else {
      document.body.classList.remove('overflow-hidden');
    }
    // Cleanup on unmount
    return () => document.body.classList.remove('overflow-hidden');
  }, [isLocked]);
}

// Usage in dialog component
function MyDialog({ open, onClose }) {
  useBodyScrollLock(open);

  return (
    <Dialog open={open} onClose={onClose}>
      {/* ... */}
    </Dialog>
  );
}

Dialog Accessibility Checklist

Before shipping a dialog component, verify it meets accessibility requirements: focus moves into the dialog on open, focus is trapped inside while open, Escape dismisses the dialog, clicking the backdrop dismisses the dialog, focus returns to the trigger element on close, screen readers announce the dialog title, and all interactive elements inside are keyboard reachable. Headless UI handles most of these — verify focus return and backdrop click behavior in your implementation.

/*
  Dialog Accessibility Checklist:

  [✓] Focus enters dialog on open (Headless UI automatic)
  [✓] Focus trapped inside while open (Headless UI automatic)
  [✓] Escape key closes dialog (Headless UI automatic)
  [✓] role='dialog' + aria-modal='true' (Headless UI automatic)
  [✓] Dialog.Title used for aria-labelledby (Headless UI automatic)
  [✓] Dialog.Description for aria-describedby (Headless UI automatic)
  [ ] Focus returns to trigger on close → store triggerRef
  [ ] Backdrop click closes dialog → pass handler to onClose
  [ ] Body scroll locked while open → useBodyScrollLock hook
  [ ] Close button has visible label or aria-label
*/

Dialog Variants: Alert vs Confirm vs Form

Dialogs serve different purposes and should be designed accordingly. An alert dialog delivers urgent information with a single acknowledgement button — use role='alertdialog' for these. A confirmation dialog asks a yes/no question before a destructive action, with Cancel as the default focus. A form dialog contains a complete form with validation. Each type has different size, focus, and button order conventions that help users quickly understand what is expected of them.

<!-- Alert dialog: urgent info, single action -->
<Dialog.Panel class='bg-white rounded-2xl p-6 max-w-sm shadow-xl'>
  <div class='flex items-start gap-4'>
    <div class='flex-shrink-0 w-10 h-10 rounded-full bg-red-100 flex items-center justify-center'>
      <ExclamationTriangleIcon class='h-5 w-5 text-red-600' />
    </div>
    <div>
      <Dialog.Title class='text-base font-semibold text-gray-900'>Session Expired</Dialog.Title>
      <Dialog.Description class='mt-1 text-sm text-gray-600'>
        Your session has expired. Please log in again.
      </Dialog.Description>
      <button class='mt-4 w-full bg-blue-600 text-white rounded-lg py-2 text-sm font-medium'>
        Log In
      </button>
    </div>
  </div>
</Dialog.Panel>

Quick Check

Test your understanding of Tailwind CSS Mastery concepts from this lesson.

Lesson Recap

In this lesson you learned: Headless UI Dialog handles focus trapping, Escape dismissal, and ARIA roles automatically, the backdrop is a full-screen fixed overlay positioned before the panel, and initialFocus directs keyboard focus to a specific element on open. Next up we animate dialog open and close transitions using Headless UI's Transition component.

الأسئلة الشائعة

هل درس «إنشاء مربعات حوار سهلة الوصول» مجاني؟

نعم — نص درس «إنشاء مربعات حوار سهلة الوصول» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Tailwind CSS Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Tailwind CSS Academy 4 دروس في المجموع.

ماذا ستتعلم في «إنشاء مربعات حوار سهلة الوصول»؟

استخدم مكوّن Dialog في Headless UI للنوافذ المنبثقة، مع احتجاز التركيز والتعامل المدمج مع مفتاح الهروب، ونسّقه بالكامل باستخدام Tailwind. تتمرن على Tailwind CSS Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Tailwind CSS Academy؟

لا تُشترط خبرة سابقة. Tailwind CSS Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.

كم من الوقت يستغرق درس «إنشاء مربعات حوار سهلة الوصول»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Tailwind CSS Academy هذا؟

نعم. كل درس في Tailwind CSS Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. مقدمة إلى Headless UI
  2. تنسيق القوائم المنسدلة في Headless UI
  3. إنشاء مربعات حوار سهلة الوصول
  4. الانتقالات باستخدام Headless UI
← العودة إلى Tailwind CSS Academy