Opbygning af tilgængelige dialogbokse
Brug Headless UI's Dialog-komponent til modaler med indbygget fastholdelse af fokus og håndtering af Escape-tasten, stylet udelukkende med Tailwind.
Opbygning af tilgængelige dialogbokse er en gratis Tailwind CSS Academy-lektion på CoddyKit. Dette er lektion 3 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Tailwind CSS Academy, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Tailwind CSS Academy-kurset indeholder 4 lektioner i alt.
Hvad gør en dialog tilgængelig
En tilgængelig dialog (modal) skal opfylde flere krav: Den skal modtage tastaturfokus, når den åbnes, fastholde fokus i sig selv, så brugerne ikke kan tabbe ud af den, kunne lukkes med Escape-tasten, anvende korrekte ARIA-attributter (role='dialog', aria-modal='true') og føre fokus tilbage til udløseren, når den lukkes. Disse krav er komplekse at implementere korrekt. Headless UI's Dialog-komponent håndterer dem alle automatisk.
Grundlæggende dialogstruktur
Headless UI's Dialog består af tre centrale elementer: Dialog (roden, der håndterer ARIA og fokus), Dialog.Panel (den synlige modalboks) og eventuelt Dialog.Title og Dialog.Description (til semantisk navngivning). open-propen styrer synligheden, og onClose udløses, når brugeren trykker på Escape eller klikker uden for panelet — du bestemmer, hvilken handling der skal udføres (typisk at sætte open-tilstanden til 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>
</>
);
}Tilføjelse af baggrundsoverlejring
En modal bør nedtone sidens indhold bag sig for at rette brugerens opmærksomhed mod dialogen. Tilføj en baggrundsoverlejring i fuld skærm ved hjælp af fixed inset-0 med en halvgennemsigtig baggrund. Placér den som det første barn af Dialog, før panelbeholderen. Brug aria-hidden='true' på baggrundsoverlejringen, da den udelukkende er dekorativ — skærmlæsere bør ikke annoncere den.
<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 af dialogpanelet
Dialog.Panel er den synlige modalbeholder. Anvend Tailwind-klasser til baggrund, afrundede hjørner, skygge, indvendig margen og maksimal bredde for at skabe et gennemarbejdet kort. Panelet bør have en max-w-*-begrænsning, så det ikke strækker sig over hele bredden på store skærme, samtidig med at det forbliver responsivt på små skærme med w-full. Tilføj en lukkeknap øverst til højre til musebrugere, der foretrækker at klikke frem for at trykke på 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>Dialog med rulning til langt indhold
Dialoger med langt indhold — f.eks. servicevilkår, formularforløb eller detaljerede forhåndsvisninger — skal kunne rulle, uden at baggrunden ruller. Anvend overflow-y-auto på panelet og en max-h-*-begrænsning, så dialogen ikke vokser ud over visningsområdet. Den ydre centreringsbeholder bør have items-start med indvendig margen øverst, så dialogen holdes tæt på toppen af skærmen ved meget langt indhold.
// 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>Dialogvarianter efter størrelse
Opret genbrugelige dialogvarianter efter størrelse ved hjælp af Tailwinds max-w-*-hjælpeklasser. Små dialoger til bekræftelser, mellemstore til formularer og store til forhåndsvisninger eller flertrinsforløb. Opret en DialogModal-komponent, der accepterer en size-prop og anvender den tilsvarende klasse for maksimal bredde — dette er et oplagt anvendelsesområde for CVA-mønstret (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>
);
}Fokushåndtering i praksis
Headless UI flytter automatisk fokus ind i dialogen, når den åbnes. Som standard flyttes fokus til det første fokuserbare element i panelet. Hvis du vil flytte fokus til et bestemt element — f.eks. en primær CTA eller et tekstfelt — skal du bruge initialFocus-propen med en React-ref, der peger på elementet. Det forbedrer brugeroplevelsen for tastatur- og skærmlæserbrugere, der straks har brug for at interagere med en bestemt kontrol.
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>
);
}Stabling af indlejrede dialoger
Nogle gange udløser en dialog en anden dialog — f.eks. en bekræftelse i et indstillingsmodalvindue. Brug gradvist højere z-index-værdier til indlejrede dialoger, så de lagdeles korrekt. Hver dialog håndterer sin egen fokusfastholdelse uafhængigt; Headless UI understøtter flere åbne dialoger samtidigt. Brug tilstandsvariabler til hvert dialogniveau, og luk dem i omvendt rækkefølge.
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>
</>
);
}Forhindring af rulning i baggrunden
Når en dialog er åben, bør indholdet bag den ikke kunne rulle. Headless UI håndterer ikke dette automatisk. Tilføj en sideeffekt, der føjer overflow-hidden til body, når dialogen åbnes, og fjerner den, når dialogen lukkes. I en Next.js- eller React-app kan du bruge useEffect i dialogkomponenten eller en tilpasset hook, der rydder korrekt op ved afmontering.
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>
);
}Tjekliste for dialogens tilgængelighed
Før du sender en dialogkomponent i produktion, skal du kontrollere, at den opfylder kravene til tilgængelighed: Fokus flyttes ind i dialogen, når den åbnes, fokus fastholdes i dialogen, mens den er åben, Escape lukker dialogen, et klik på baggrundsoverlejringen lukker dialogen, fokus føres tilbage til udløseren, når dialogen lukkes, skærmlæsere annoncerer dialogens titel, og alle interaktive elementer i dialogen kan nås med tastaturet. Headless UI håndterer de fleste af disse punkter — kontrollér, at fokus føres tilbage, og at klik på baggrundsoverlejringen fungerer i din implementering.
/*
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
*/Dialogvarianter: Alert vs Confirm vs Form
Dialoger tjener forskellige formål og bør designes derefter. En alert-dialog viser vigtige oplysninger, der kræver hurtig opmærksomhed, med én knap til bekræftelse — brug role='alertdialog' til disse. En bekræftelsesdialog stiller et ja/nej-spørgsmål før en destruktiv handling, med Annuller som standardfokus. En formular-dialog indeholder en komplet formular med validering. Hver type har forskellige konventioner for størrelse, fokus og knappernes rækkefølge, som hjælper brugerne med hurtigt at forstå, hvad der forventes af dem.
<!-- 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>Hurtigt tjek
Test din forståelse af Tailwind CSS Mastery-koncepterne fra denne lektion.
Opsummering af lektionen
I denne lektion lærte du, at Headless UI Dialog automatisk håndterer fastholdelse af fokus, lukning med Escape og ARIA-roller, at baggrundsoverlayet er et fast overlay i fuld skærm, der placeres før panelet, og at initialFocus leder tastaturfokus til et bestemt element, når dialogen åbnes. Nu skal vi animere dialogens åbne- og lukketransitioner med Headless UI's Transition-komponent.
Lær HTML med en AI-underviser — gratis
Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.
- Kurser
- 30
- Lektioner
- 120
Ofte stillede spørgsmål
Er lektionen “Opbygning af tilgængelige dialogbokse” gratis?
Ja — hele teksten til “Opbygning af tilgængelige dialogbokse” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Tailwind CSS Academy-kurset, skal du opgradere til CoddyKit PRO. Tailwind CSS Academy-kurset indeholder 4 lektioner i alt.
Hvad lærer jeg i “Opbygning af tilgængelige dialogbokse”?
Brug Headless UI's Dialog-komponent til modaler med indbygget fastholdelse af fokus og håndtering af Escape-tasten, stylet udelukkende med Tailwind. Du øver dig i Tailwind CSS Academy med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.
Skal jeg have erfaring for at begynde på Tailwind CSS Academy?
Der kræves ingen tidligere erfaring. Tailwind CSS Academy på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 3 af 4.
Hvor lang tid tager lektionen “Opbygning af tilgængelige dialogbokse”?
De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.
Kan jeg skrive og køre kode i denne Tailwind CSS Academy-lektion?
Ja. Alle Tailwind CSS Academy-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.
Alle lektioner i dette kursus
- Introduktion til Headless UI
- Styling af headless-menu og dropdown
- Opbygning af tilgængelige dialogbokse
- Overgange med Headless UI