Tailwind CSS Academy · Lektion

Bygg tillgängliga dialogrutor

Använd Headless UIs Dialog-komponent för modala dialogrutor med inbyggd fokusfångst och hantering av Escape-tangenten, helt stylade med Tailwind.

Lektion 3 av 413 steg

Bygg tillgängliga dialogrutor är en gratis lektion i Tailwind CSS Academy på CoddyKit. Detta är lektion 3 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för Tailwind CSS Academy, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Tailwind CSS Academy innehåller totalt 4 lektioner.

Vad gör en dialogruta tillgänglig?

En tillgänglig dialogruta (modal) måste uppfylla flera krav: den måste få tangentbordsfokus när den öppnas, hålla kvar fokus inom sig så att användare inte kan tabba ut, kunna stängas med Escape-tangenten, använda korrekta ARIA-attribut (role='dialog', aria-modal='true') och återföra fokus till utlösarelementet när den stängs. Dessa krav är komplexa att implementera korrekt. Headless UI:s Dialog-komponent hanterar allt detta automatiskt.

Grundläggande dialogstruktur

Headless UI:s Dialog består av tre centrala element: Dialog (roten, som hanterar ARIA och fokus), Dialog.Panel (den synliga modala rutan) och valfritt Dialog.Title samt Dialog.Description (för semantisk etikettering). Egenskapen open styr synligheten och onClose körs när användaren trycker på Escape eller klickar utanför panelen — Ni avgör vilken åtgärd som ska utföras, vanligtvis att sätta öppningstillståndet till 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>
    </>
  );
}

Lägga till bakgrundsöverlägget

En modal bör tona ned sidinnehållet bakom sig för att rikta användarens uppmärksamhet mot dialogrutan. Lägg till ett helskärmsöverlägg med fixed inset-0 och en halvtransparent bakgrund. Placera det som det första barnet till Dialog, före panelens behållare. Använd aria-hidden='true' på överlägget eftersom det enbart är dekorativt — skärmläsare ska inte läsa upp det.

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

Styla dialogpanelen

Dialog.Panel är den synliga modala behållaren. Tillämpa Tailwind-klasser för bakgrund, rundade hörn, skugga, padding och maximal bredd för att skapa ett genomarbetat kort. Panelen bör ha en begränsning med max-w-* så att den inte sträcks över hela bredden på stora skärmar, samtidigt som den förblir responsiv på små skärmar med w-full. Lägg till en stängningsknapp i det övre högra hörnet för musanvändare som föredrar att klicka framför att trycka 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>

Rullningsbar dialogruta för långt innehåll

Dialogrutor med långt innehåll — som användarvillkor, formulärguider eller detaljerade förhandsvisningar — måste kunna rullas utan att bakgrunden rullar. Tillämpa overflow-y-auto på panelen och en begränsning med max-h-* så att dialogrutan inte växer utanför vyporten. Den yttre centreringsbehållaren bör använda items-start med padding upptill för att hålla dialogrutan nära skärmens överkant vid mycket långt innehåll.

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

Storleksvarianter för dialogrutor

Bygg återanvändbara storleksvarianter för dialogrutor med Tailwinds verktygsklasser max-w-*. Små dialogrutor passar för bekräftelser, medelstora för formulär och stora för förhandsvisningar eller guider i flera steg. Skapa en DialogModal-komponent som tar emot en size-egenskap och tillämpar motsvarande klass för maximal bredd — detta är ett naturligt användningsområde för 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>
  );
}

Fokushantering i praktiken

Headless UI flyttar automatiskt fokus till dialogrutan när den öppnas. Som standard placeras fokus på det första fokuserbara elementet i panelen. Om Ni vill styra fokus till ett specifikt element — till exempel en primär CTA eller ett textfält — använder Ni egenskapen initialFocus med en React-ref som pekar på elementet. Detta förbättrar användarupplevelsen för tangentbords- och skärmläsaranvändare som direkt behöver interagera med ett visst kontrollobjekt.

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

Stapling av nästlade dialogrutor

Ibland öppnar en dialogruta en annan dialogruta — till exempel en bekräftelse i en inställningsmodal. Använd successivt högre värden för z-index för nästlade dialogrutor så att de lagras korrekt i lager. Varje dialogruta hanterar sitt eget fokusfång oberoende av de andra; Headless UI stöder flera öppna dialogrutor samtidigt. Använd tillståndsvariabler för varje dialognivå och stäng dem i omvänd ordning.

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

Förhindra rullning i bakgrunden

När en dialogruta är öppen bör sidinnehållet bakom den inte kunna rulla. Headless UI hanterar inte detta automatiskt. Lägg till en bieffekt som lägger till overflow-hidden på body när dialogrutan öppnas och tar bort det när den stängs. I en Next.js- eller React-app kan Ni använda useEffect i dialogkomponenten eller en anpassad hook som städar upp korrekt när komponenten avmonteras.

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

Checklista för dialogrutors tillgänglighet

Innan Ni levererar en dialogkomponent bör Ni kontrollera att den uppfyller tillgänglighetskraven: fokus flyttas till dialogrutan när den öppnas, fokus hålls kvar där medan den är öppen, Escape stänger dialogrutan, ett klick på överlägget stänger dialogrutan, fokus återförs till utlösarelementet när den stängs, skärmläsare läser upp dialogrutans titel och alla interaktiva element inuti kan nås med tangentbordet. Headless UI hanterar de flesta av dessa delar — verifiera att fokus återförs och att klick på överlägget fungerar i Er 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
*/

Dialogvarianter: alert vs confirm vs formulär

Dialoger har olika syften och bör utformas därefter. En alertdialog visar brådskande information med en enda bekräftelseknapp – använd role='alertdialog' för dessa. En bekräftelsedialog ställer en ja/nej-fråga innan en destruktiv åtgärd, med Avbryt som standardfokus. En formulärdialog innehåller ett komplett formulär med validering. Varje typ har olika konventioner för storlek, fokus och knappordning som hjälper användarna att snabbt förstå vad som förväntas av 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>

Snabbkontroll

Testa dina kunskaper om koncepten i Tailwind CSS Mastery från den här lektionen.

Lektionssammanfattning

I den här lektionen lärde du dig att Headless UI Dialog automatiskt hanterar fokusfångst, stängning med Escape och ARIA-roller, att bakgrunden är ett fast överlägg över hela skärmen som placeras före panelen och att initialFocus styr tangentbordsfokus till ett specifikt element när dialogen öppnas. Nästa steg är att animera dialogens öppnings- och stängningstransitioner med Headless UI:s Transition-komponent.

Gratis att börja

Lär dig HTML med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
30
Lektioner
120

Vanliga frågor

Är lektionen ”Bygg tillgängliga dialogrutor” gratis?

Ja – hela texten till ”Bygg tillgängliga dialogrutor” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i Tailwind CSS Academy, kan Ni uppgradera till CoddyKit PRO. Kursen i Tailwind CSS Academy innehåller totalt 4 lektioner.

Vad lär jag mig i ”Bygg tillgängliga dialogrutor”?

Använd Headless UIs Dialog-komponent för modala dialogrutor med inbyggd fokusfångst och hantering av Escape-tangenten, helt stylade med Tailwind. Ni övar på Tailwind CSS Academy med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Tailwind CSS Academy?

Du behöver inga förkunskaper. Utbildningen i Tailwind CSS Academy på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 3 av 4.

Hur lång tid tar lektionen ”Bygg tillgängliga dialogrutor”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Tailwind CSS Academy-lektionen?

Ja. Varje Tailwind CSS Academy-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Introduktion till Headless UI
  2. Styla Headless-menyer och rullgardinsmenyer
  3. Bygg tillgängliga dialogrutor
  4. Övergångar med Headless UI
← Tillbaka till Tailwind CSS Academy