Tailwind CSS Academy · บทเรียน

การสร้างไดอะล็อกที่เข้าถึงได้

ใช้คอมโพเนนต์ Dialog ของ Headless UI สำหรับโมดัลที่มีการจำกัดโฟกัสและจัดการปุ่ม Escape ในตัว โดยจัดสไตล์ทั้งหมดด้วย Tailwind

บทเรียน 3 จาก 413 ขั้นตอน

การสร้างไดอะล็อกที่เข้าถึงได้ เป็นบทเรียน Tailwind CSS Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Tailwind CSS Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Tailwind CSS Academy มีบทเรียนทั้งหมด 4 บทเรียน

อะไรทำให้กล่องโต้ตอบเข้าถึงได้

กล่องโต้ตอบที่เข้าถึงได้ (โมดัล) ต้องเป็นไปตามข้อกำหนดหลายประการ ได้แก่ ต้องรับ โฟกัสจากแป้นพิมพ์ เมื่อเปิด ต้องกักโฟกัสไว้ภายในเพื่อไม่ให้ผู้ใช้กด Tab ออกไปด้านนอก ต้องปิดได้ด้วย ปุ่ม Escape ต้องใช้แอตทริบิวต์ ARIA ที่ถูกต้อง (role='dialog', aria-modal='true') และต้องคืนโฟกัสให้กับองค์ประกอบตัวกระตุ้นเมื่อปิด ข้อกำหนดเหล่านี้นำไปใช้อย่างถูกต้องได้ค่อนข้างซับซ้อน คอมโพเนนต์ Dialog ของเฮดเลส UI จัดการทุกอย่างให้โดยอัตโนมัติ

โครงสร้างกล่องโต้ตอบพื้นฐาน

Dialog ของเฮดเลส UI ประกอบด้วยองค์ประกอบสำคัญสามรายการ ได้แก่ Dialog (รากที่จัดการ ARIA และโฟกัส), Dialog.Panel (กล่องโมดัลที่มองเห็นได้) และอาจมี Dialog.Title กับ Dialog.Description (สำหรับการตั้งชื่อเชิงความหมาย) พร็อพ open ควบคุมการมองเห็น และ onClose จะทำงานเมื่อผู้ใช้กด Escape หรือคลิกนอกแผง โดยคุณเป็นผู้ตัดสินใจว่าจะดำเนินการอย่างไร (โดยทั่วไปคือกำหนดสถานะ open เป็น 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>
    </>
  );
}

การเพิ่มพื้นหลังซ้อนทับ

โมดัลควรทำให้เนื้อหาของหน้าที่อยู่ด้านหลังมืดลง เพื่อดึงความสนใจของผู้ใช้ไปยังกล่องโต้ตอบ เพิ่มพื้นหลังซ้อนทับเต็มหน้าจอโดยใช้ fixed inset-0 พร้อมพื้นหลังแบบโปร่งใสบางส่วน วางพื้นหลังนี้เป็นลูกองค์ประกอบแรกของ Dialog ก่อนคอนเทนเนอร์ของแผง ใช้ aria-hidden='true' กับพื้นหลัง เนื่องจากมีไว้เพื่อการตกแต่งเท่านั้น โปรแกรมอ่านหน้าจอไม่ควรประกาศเนื้อหานี้

<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>

การจัดรูปแบบแผงกล่องโต้ตอบ

Dialog.Panel คือคอนเทนเนอร์โมดัลที่มองเห็นได้ ใช้คลาสของเทลวินด์สำหรับพื้นหลัง ความโค้งของขอบ เงา ระยะห่างภายใน และความกว้างสูงสุด เพื่อสร้างการ์ดที่ดูเรียบร้อย แผงควรมีข้อจำกัด max-w-* เพื่อไม่ให้ขยายเต็มความกว้างบนหน้าจอขนาดใหญ่ ขณะเดียวกันยังคงปรับตามหน้าจอขนาดเล็กได้ด้วย w-full เพิ่มปุ่มปิดไว้ที่มุมขวาบนสำหรับผู้ใช้เมาส์ที่ต้องการคลิกแทนการกด 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>

กล่องโต้ตอบแบบเลื่อนดูได้สำหรับเนื้อหายาว

กล่องโต้ตอบที่มีเนื้อหายาว เช่น ข้อกำหนดการให้บริการ ตัวช่วยสร้างแบบฟอร์ม หรือภาพตัวอย่างแบบละเอียด จำเป็นต้องเลื่อนดูได้โดยไม่ทำให้พื้นหลังซ้อนทับเลื่อนตาม ใช้ overflow-y-auto กับแผงและกำหนดข้อจำกัด max-h-* เพื่อไม่ให้กล่องโต้ตอบขยายเกินหน้าจอ คอนเทนเนอร์ด้านนอกที่ใช้จัดกึ่งกลางควรเป็น items-start พร้อมระยะห่างด้านบน เพื่อให้กล่องโต้ตอบอยู่ใกล้ด้านบนของหน้าจอเมื่อมีเนื้อหายาวมาก

// 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>

รูปแบบขนาดของกล่องโต้ตอบ

สร้างรูปแบบขนาดกล่องโต้ตอบที่นำกลับมาใช้ซ้ำได้ด้วยยูทิลิตี max-w-* ของเทลวินด์ ใช้กล่องโต้ตอบขนาดเล็กสำหรับการยืนยัน ขนาดกลางสำหรับแบบฟอร์ม และขนาดใหญ่สำหรับภาพตัวอย่างหรือตัวช่วยสร้างแบบหลายขั้นตอน สร้างคอมโพเนนต์ DialogModal ที่รับพร็อพ size แล้วใช้คลาสความกว้างสูงสุดที่ตรงกัน ซึ่งเป็นกรณีใช้งานที่เหมาะสมสำหรับรูปแบบ CVA (class-variance-authority)

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>
  );
}

การจัดการโฟกัสในทางปฏิบัติ

เฮดเลส UI จะย้ายโฟกัสเข้าไปในกล่องโต้ตอบโดยอัตโนมัติเมื่อเปิด ตามค่าเริ่มต้น โฟกัสจะไปยังองค์ประกอบแรกภายในแผงที่รับโฟกัสได้ หากต้องการกำหนดโฟกัสไปยังองค์ประกอบใดโดยเฉพาะ เช่น CTA หลักหรือช่องป้อนข้อความ ให้ใช้พร็อพ initialFocus พร้อมรีฟของ React ที่ชี้ไปยังองค์ประกอบนั้น วิธีนี้ช่วยปรับปรุงประสบการณ์ของผู้ใช้แป้นพิมพ์และผู้ใช้โปรแกรมอ่านหน้าจอที่ต้องโต้ตอบกับตัวควบคุมเฉพาะทันที

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>
  );
}

การซ้อนกล่องโต้ตอบแบบลำดับชั้น

บางครั้งกล่องโต้ตอบหนึ่งจะเปิดอีกกล่องหนึ่ง เช่น กล่องยืนยันภายในโมดัลการตั้งค่า ใช้ค่า z-index ที่สูงขึ้นตามลำดับสำหรับกล่องโต้ตอบที่ซ้อนกัน เพื่อให้จัดชั้นได้ถูกต้อง กล่องโต้ตอบแต่ละรายการจะจัดการการกักโฟกัสของตนเองอย่างเป็นอิสระ และเฮดเลส UI รองรับการเปิดกล่องโต้ตอบหลายรายการพร้อมกัน ใช้ตัวแปรสถานะแยกกันสำหรับกล่องโต้ตอบแต่ละระดับ และปิดกล่องเหล่านั้นตามลำดับย้อนกลับ

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>
    </>
  );
}

การป้องกันการเลื่อนพื้นหลัง

เมื่อเปิดกล่องโต้ตอบ เนื้อหาของหน้าที่อยู่ด้านหลังไม่ควรเลื่อน เฮดเลส UI ไม่ได้จัดการเรื่องนี้โดยอัตโนมัติ ให้เพิ่มผลข้างเคียงที่ใส่ overflow-hidden ให้กับ body เมื่อเปิดกล่องโต้ตอบ และนำออกเมื่อปิด ในแอป Next.js หรือ React ให้ใช้ useEffect ภายในคอมโพเนนต์กล่องโต้ตอบ หรือใช้ฮุกแบบกำหนดเองที่ล้างการทำงานได้อย่างถูกต้องเมื่อยกเลิกการเมานต์

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>
  );
}

รายการตรวจสอบการเข้าถึงของกล่องโต้ตอบ

ก่อนนำคอมโพเนนต์กล่องโต้ตอบไปใช้งานจริง ให้ตรวจสอบว่าเป็นไปตามข้อกำหนดด้านการเข้าถึง ได้แก่ โฟกัสย้ายเข้าไปในกล่องโต้ตอบเมื่อเปิด โฟกัสถูกกักไว้ภายในขณะที่เปิดอยู่ ปุ่ม Escape ใช้ปิดกล่องโต้ตอบได้ การคลิกพื้นหลังซ้อนทับใช้ปิดกล่องโต้ตอบได้ โฟกัสคืนกลับไปยังองค์ประกอบตัวกระตุ้นเมื่อปิด โปรแกรมอ่านหน้าจอประกาศชื่อกล่องโต้ตอบ และองค์ประกอบแบบโต้ตอบทั้งหมดภายในสามารถเข้าถึงได้ด้วยแป้นพิมพ์ เฮดเลส UI จัดการเรื่องเหล่านี้ได้เกือบทั้งหมด แต่ให้ตรวจสอบพฤติกรรมการคืนโฟกัสและการคลิกพื้นหลังซ้อนทับในการใช้งานของคุณ

/*
  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
*/

รูปแบบของกล่องโต้ตอบ: การแจ้งเตือน เทียบกับการยืนยัน เทียบกับแบบฟอร์ม

กล่องโต้ตอบมีจุดประสงค์แตกต่างกัน จึงควรออกแบบให้เหมาะสมกับการใช้งาน กล่องโต้ตอบการแจ้งเตือน ใช้แสดงข้อมูลเร่งด่วนพร้อมปุ่มรับทราบเพียงปุ่มเดียว — สำหรับกรณีนี้ให้ใช้ role='alertdialog' ส่วน กล่องโต้ตอบยืนยัน ใช้ถามคำถามแบบใช่หรือไม่ใช่ก่อนดำเนินการที่อาจทำลายข้อมูล โดยให้โฟกัสเริ่มต้นอยู่ที่ปุ่มยกเลิก และ กล่องโต้ตอบแบบฟอร์ม จะมีแบบฟอร์มที่สมบูรณ์พร้อมการตรวจสอบข้อมูล กล่องโต้ตอบแต่ละประเภทมีแนวทางเกี่ยวกับขนาด การโฟกัส และลำดับปุ่มที่แตกต่างกัน ซึ่งช่วยให้ผู้ใช้เข้าใจได้อย่างรวดเร็วว่าต้องดำเนินการอย่างไร

<!-- 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>

ตรวจสอบความเข้าใจอย่างรวดเร็ว

ทดสอบความเข้าใจแนวคิด Tailwind CSS Mastery จากบทเรียนนี้

สรุปบทเรียน

ในบทเรียนนี้ คุณได้เรียนรู้ว่า Headless UI Dialog จัดการการจำกัดโฟกัส การปิดด้วยปุ่ม Escape และบทบาท ARIA ให้โดยอัตโนมัติ, ฉากหลัง คือส่วนซ้อนทับแบบตรึงเต็มหน้าจอที่วางไว้ก่อนแผง และ initialFocus ใช้กำหนดให้โฟกัสจากแป้นพิมพ์ไปยังองค์ประกอบที่ระบุเมื่อเปิด จากนั้นเราจะสร้างภาพเคลื่อนไหวสำหรับการเปิดและปิดกล่องโต้ตอบโดยใช้คอมโพเนนต์ Transition ของ Headless UI

เริ่มต้นได้ฟรี

เรียนรู้ HTML ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
30
บทเรียน
120

คำถามที่พบบ่อย

บทเรียน “การสร้างไดอะล็อกที่เข้าถึงได้” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การสร้างไดอะล็อกที่เข้าถึงได้” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Tailwind CSS Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Tailwind CSS Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การสร้างไดอะล็อกที่เข้าถึงได้”

ใช้คอมโพเนนต์ Dialog ของ Headless UI สำหรับโมดัลที่มีการจำกัดโฟกัสและจัดการปุ่ม Escape ในตัว โดยจัดสไตล์ทั้งหมดด้วย Tailwind คุณปฏิบัติ Tailwind CSS Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Tailwind CSS Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Tailwind CSS Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “การสร้างไดอะล็อกที่เข้าถึงได้” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Tailwind CSS Academy นี้ได้ไหม

ได้ บทเรียน Tailwind CSS Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. บทนำสู่ Headless UI
  2. การจัดสไตล์เมนูและเมนูดรอปดาวน์แบบ Headless
  3. การสร้างไดอะล็อกที่เข้าถึงได้
  4. การเปลี่ยนผ่านด้วย Headless UI
← กลับไปที่ Tailwind CSS Academy