Budowanie dostępnych okien dialogowych
Użyją Państwo komponentu Dialog biblioteki Headless UI do tworzenia modali z wbudowanym przechwytywaniem fokusu i obsługą klawisza Escape, stylizowanych wyłącznie za pomocą Tailwind.
Budowanie dostępnych okien dialogowych to bezpłatna lekcja Tailwind CSS Academy na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Tailwind CSS Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Tailwind CSS Academy zawiera 4 lekcji w sumie.
Co sprawia, że dialog jest dostępny?
Dostępny dialog (modal) musi spełniać kilka wymagań: po otwarciu musi otrzymać fokus klawiatury, utrzymywać fokus wewnątrz siebie, aby użytkownicy nie mogli przejść klawiszem Tab poza dialog, umożliwiać zamknięcie za pomocą klawisza Escape, stosować prawidłowe atrybuty ARIA (role='dialog', aria-modal='true') oraz po zamknięciu przywracać fokus elementowi wyzwalającemu. Poprawna implementacja tych wymagań jest złożona. Komponent Dialog biblioteki Headless UI obsługuje je wszystkie automatycznie.
Podstawowa struktura dialogu
Komponent Dialog biblioteki Headless UI składa się z trzech kluczowych elementów: Dialog (główny element obsługujący ARIA i fokus), Dialog.Panel (widoczny kontener modalu) oraz opcjonalnie Dialog.Title i Dialog.Description (zapewniające semantyczne etykietowanie). Właściwość open steruje widocznością, a onClose jest wywoływana, gdy użytkownik naciśnie Escape lub kliknie poza panelem — to Państwo decydują, jakie działanie ma zostać wykonane (zwykle ustawienie stanu open na 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>
</>
);
}Dodawanie warstwy przyciemniającej tło
Modal powinien przyciemniać zawartość strony znajdującą się za nim, aby skierować uwagę użytkownika na dialog. Dodaj pełnoekranową warstwę przyciemniającą za pomocą fixed inset-0 i półprzezroczystego tła. Umieść ją jako pierwsze dziecko elementu Dialog, przed kontenerem panelu. Użyj na tej warstwie aria-hidden='true', ponieważ pełni ona wyłącznie funkcję dekoracyjną — czytniki ekranu nie powinny jej odczytywać.
<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>Stylowanie panelu dialogu
Dialog.Panel jest widocznym kontenerem modalu. Zastosuj klasy Tailwind odpowiadające za tło, zaokrąglenie rogów, cień, odstępy wewnętrzne i maksymalną szerokość, aby utworzyć dopracowaną kartę. Panel powinien mieć ograniczenie max-w-*, aby na dużych ekranach nie rozciągał się na całą szerokość, a jednocześnie pozostać responsywny na małych ekranach dzięki w-full. Dodaj przycisk zamykania w prawym górnym rogu dla użytkowników myszy, którzy wolą kliknięcie od naciskania klawisza 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>Przewijany dialog dla długiej treści
Dialogi z długą treścią — takie jak regulaminy, kreatory formularzy lub szczegółowe podglądy — muszą umożliwiać przewijanie bez przewijania warstwy przyciemniającej tło. Zastosuj overflow-y-auto do panelu oraz ograniczenie max-h-*, aby dialog nie urósł poza obszar widoczny. Zewnętrzny kontener wyśrodkowujący powinien używać items-start wraz z górnym odstępem wewnętrznym, aby przy bardzo długiej treści dialog znajdował się blisko górnej krawędzi ekranu.
// 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>Warianty rozmiaru dialogu
Twórz wielokrotnego użytku warianty rozmiaru dialogu za pomocą narzędzi max-w-* Tailwind. Małe dialogi służą do potwierdzeń, średnie do formularzy, a duże do podglądów lub wieloetapowych kreatorów. Utwórz komponent DialogModal, który przyjmuje właściwość size i stosuje odpowiadającą jej klasę maksymalnej szerokości — jest to naturalny przypadek użycia wzorca 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>
);
}Zarządzanie fokusem w praktyce
Headless UI automatycznie przenosi fokus do dialogu po jego otwarciu. Domyślnie fokus trafia na pierwszy element panelu, na którym można ustawić fokus. Aby skierować fokus do konkretnego elementu — takiego jak główny przycisk CTA lub pole tekstowe — użyj właściwości initialFocus wraz z referencją React wskazującą ten element. Poprawia to komfort użytkowników klawiatury i czytników ekranu, którzy od razu muszą wejść w interakcję z konkretnym elementem sterującym.
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>
);
}Zagnieżdżanie i nakładanie dialogów
Czasami dialog otwiera kolejny dialog — na przykład potwierdzenie wewnątrz modalu ustawień. Użyj stopniowo wyższych wartości z-index dla zagnieżdżonych dialogów, aby poprawnie układały się warstwami. Każdy dialog niezależnie zarządza własnym uwięzieniem fokusu; Headless UI obsługuje jednocześnie wiele otwartych dialogów. Użyj osobnych zmiennych stanu dla każdego poziomu dialogu i zamykaj je w odwrotnej kolejności.
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>
</>
);
}Zapobieganie przewijaniu tła
Gdy dialog jest otwarty, znajdująca się za nim zawartość strony nie powinna się przewijać. Headless UI nie obsługuje tego automatycznie. Dodaj efekt uboczny, który po otwarciu dialogu dodaje overflow-hidden do elementu body, a po jego zamknięciu usuwa tę klasę. W aplikacji Next.js lub React użyj useEffect wewnątrz komponentu dialogu albo własnego hooka, który prawidłowo wykonuje czyszczenie podczas odmontowywania.
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>
);
}Lista kontrolna dostępności dialogu
Przed wdrożeniem komponentu dialogu sprawdź, czy spełnia on wymagania dostępności: fokus po otwarciu przenosi się do dialogu, podczas otwarcia jest w nim uwięziony, klawisz Escape zamyka dialog, kliknięcie warstwy przyciemniającej tło zamyka dialog, fokus po zamknięciu wraca do elementu wyzwalającego, czytniki ekranu odczytują tytuł dialogu, a do wszystkich elementów interaktywnych wewnątrz można dotrzeć za pomocą klawiatury. Headless UI obsługuje większość tych funkcji — w swojej implementacji sprawdź przywracanie fokusu oraz zachowanie po kliknięciu tła.
/*
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
*/Warianty okien dialogowych: Alert vs Confirm vs Form
Okna dialogowe służą różnym celom, dlatego należy odpowiednio je projektować. Okno alertu przekazuje pilną informację i zawiera tylko jeden przycisk potwierdzenia — w tym przypadku należy użyć role='alertdialog'. Okno potwierdzenia zadaje pytanie typu tak/nie przed wykonaniem destrukcyjnej operacji, a domyślnie ustawia fokus na przycisku Anuluj. Okno formularza zawiera kompletny formularz z walidacją. Każdy typ ma własne konwencje dotyczące rozmiaru, fokusu i kolejności przycisków, które pomagają użytkownikom szybko zrozumieć, czego się od nich oczekuje.
<!-- 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>Szybkie sprawdzenie
Sprawdź swoją znajomość zagadnień Tailwind CSS Mastery z tej lekcji.
Podsumowanie lekcji
W tej lekcji nauczyli się Państwo, że Headless UI Dialog automatycznie obsługuje przechwytywanie fokusu, zamykanie klawiszem Escape oraz role ARIA, backdrop to stała, pełnoekranowa warstwa nakładana przed panelem, a initialFocus kieruje fokus klawiatury do określonego elementu po otwarciu. Następnie zajmiemy się animowaniem otwierania i zamykania okien dialogowych za pomocą komponentu Transition z Headless UI.
Ucz się HTML dzięki korepetycjom AI — za darmo
Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.
- Kursy
- 30
- Lekcje
- 120
Często zadawane pytania
Czy lekcja „Budowanie dostępnych okien dialogowych” jest bezpłatna?
Tak — pełny tekst „Budowanie dostępnych okien dialogowych” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Tailwind CSS Academy, przejdź na CoddyKit PRO. Kurs Tailwind CSS Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Budowanie dostępnych okien dialogowych”?
Użyją Państwo komponentu Dialog biblioteki Headless UI do tworzenia modali z wbudowanym przechwytywaniem fokusu i obsługą klawisza Escape, stylizowanych wyłącznie za pomocą Tailwind. Ćwiczysz Tailwind CSS Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Tailwind CSS Academy?
Nie wymagamy żadnego doświadczenia. Tailwind CSS Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.
Ile czasu zajmuje lekcja „Budowanie dostępnych okien dialogowych”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Tailwind CSS Academy?
Tak. Każda lekcja Tailwind CSS Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Wprowadzenie do Headless UI
- Stylizowanie menu i listy rozwijanej Headless UI
- Budowanie dostępnych okien dialogowych
- Przejścia z Headless UI