Tailwind CSS Academy · Lezione

Creare dialog accessibili

Utilizzi il componente Dialog di Headless UI per le finestre modali, con gestione integrata del focus e del tasto Esc, applicando lo stile interamente con Tailwind.

Lezione 3 di 413 passaggi

Creare dialog accessibili è una lezione Tailwind CSS Academy gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Tailwind CSS Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Tailwind CSS Academy include 4 lezioni in totale.

Cosa rende accessibile una finestra di dialogo?

Una finestra di dialogo accessibile (modale) deve soddisfare diversi requisiti: deve ricevere il focus da tastiera quando viene aperta, mantenere il focus al suo interno in modo che gli utenti non possano usare Tab per uscirne, poter essere chiusa con il tasto Escape, applicare gli attributi ARIA corretti (role='dialog', aria-modal='true') e restituire il focus all'elemento trigger quando viene chiusa. Implementare correttamente questi requisiti è complesso. Il componente Dialog di Headless UI li gestisce tutti automaticamente.

Struttura di base della finestra di dialogo

Il Dialog di Headless UI combina tre elementi fondamentali: Dialog (la radice, che gestisce ARIA e focus), Dialog.Panel (il riquadro modale visibile) e, facoltativamente, Dialog.Title e Dialog.Description (per l'etichettatura semantica). Il prop open controlla la visibilità, mentre onClose viene eseguito quando l'utente preme Escape o fa clic fuori dal pannello: spetta a voi decidere quale azione eseguire, in genere impostare lo stato open su 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>
    </>
  );
}

Aggiunta dell'overlay di sfondo

Una finestra modale dovrebbe oscurare il contenuto della pagina sottostante per attirare l'attenzione dell'utente sulla finestra di dialogo. Aggiungete uno sfondo a schermo intero usando fixed inset-0 con un colore di sfondo semitrasparente. Inseritelo come primo figlio di Dialog, prima del contenitore del pannello. Usate aria-hidden='true' sullo sfondo, poiché è puramente decorativo: gli screen reader non dovrebbero annunciarlo.

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

Stile del pannello della finestra di dialogo

Dialog.Panel è il contenitore modale visibile. Applicate classi Tailwind per sfondo, raggio dei bordi, ombra, padding e larghezza massima, così da creare una card curata. Il pannello dovrebbe avere un vincolo max-w-* per non estendersi a tutta larghezza sugli schermi grandi, rimanendo però responsive sugli schermi piccoli con w-full. Aggiungete un pulsante di chiusura nell'angolo in alto a destra per gli utenti che preferiscono fare clic anziché premere 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>

Finestra di dialogo scorrevole per contenuti lunghi

Le finestre di dialogo con contenuti lunghi, come condizioni d'uso, procedure guidate per moduli o anteprime dettagliate, devono poter scorrere senza far scorrere lo sfondo. Applicate overflow-y-auto al pannello e un vincolo max-h-* per impedire che la finestra superi l'altezza della viewport. Il contenitore esterno per il centraggio dovrebbe usare items-start con un padding superiore, così da mantenere la finestra vicino alla parte alta dello schermo quando il contenuto è molto lungo.

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

Varianti delle dimensioni della finestra di dialogo

Create varianti riutilizzabili delle dimensioni delle finestre di dialogo usando le utility max-w-* di Tailwind. Usate finestre piccole per le conferme, medie per i moduli e grandi per le anteprime o le procedure guidate a più passaggi. Create un componente DialogModal che accetti un prop size e applichi la classe di larghezza massima corrispondente: questo è un caso d'uso naturale per il pattern 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>
  );
}

Gestione del focus nella pratica

Headless UI sposta automaticamente il focus nella finestra di dialogo quando viene aperta. Per impostazione predefinita, il focus passa al primo elemento focalizzabile all'interno del pannello. Per indirizzare il focus a un elemento specifico, come una CTA principale o un campo di testo, usate il prop initialFocus con un ref React che punti a quell'elemento. Questo migliora l'esperienza degli utenti da tastiera e degli utenti di screen reader che devono interagire immediatamente con un controllo specifico.

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

Sovrapposizione di finestre di dialogo annidate

A volte una finestra di dialogo ne attiva un'altra, ad esempio una conferma all'interno di una modale delle impostazioni. Usate valori di z-index progressivamente più alti per le finestre annidate, così da sovrapporle correttamente. Ogni finestra gestisce indipendentemente il proprio focus trap; Headless UI supporta più finestre di dialogo aperte contemporaneamente. Usate variabili di stato separate per ogni livello e chiudetele in ordine inverso.

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

Impedire lo scorrimento dello sfondo

Quando una finestra di dialogo è aperta, il contenuto della pagina sottostante non dovrebbe scorrere. Headless UI non gestisce automaticamente questo comportamento. Aggiungete un effetto collaterale che applichi overflow-hidden al body quando la finestra viene aperta e lo rimuova quando viene chiusa. In un'app Next.js o React, usate un useEffect all'interno del componente della finestra oppure un hook personalizzato che esegua correttamente la pulizia al momento dello smontaggio.

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

Checklist di accessibilità della finestra di dialogo

Prima di distribuire un componente per finestre di dialogo, verificate che soddisfi i requisiti di accessibilità: il focus deve spostarsi nella finestra all'apertura, rimanere intrappolato al suo interno mentre è aperta, Escape deve chiuderla, un clic sullo sfondo deve chiuderla, il focus deve tornare all'elemento trigger alla chiusura, gli screen reader devono annunciare il titolo della finestra e tutti gli elementi interattivi al suo interno devono essere raggiungibili da tastiera. Headless UI gestisce la maggior parte di questi aspetti: verificate nella vostra implementazione il ritorno del focus e il comportamento del clic sullo sfondo.

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

Varianti delle finestre di dialogo: avviso, conferma e modulo

Le finestre di dialogo hanno scopi diversi e devono essere progettate di conseguenza. Una finestra di dialogo di avviso comunica informazioni urgenti con un solo pulsante di conferma: per queste utilizzi role='alertdialog'. Una finestra di dialogo di conferma pone una domanda con risposta sì/no prima di un'azione distruttiva, con Annulla come elemento attivo predefinito. Una finestra di dialogo con modulo contiene un modulo completo con validazione. Ogni tipo prevede convenzioni diverse per dimensioni, stato attivo e ordine dei pulsanti, che aiutano gli utenti a capire rapidamente che cosa ci si aspetta da loro.

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

Verifica rapida

Verifichi la Sua comprensione dei concetti di Tailwind CSS Mastery trattati in questa lezione.

Riepilogo della lezione

In questa lezione ha imparato che Headless UI Dialog gestisce automaticamente il confinamento del focus, la chiusura con Escape e i ruoli ARIA; il backdrop è un overlay fisso a schermo intero posizionato prima del pannello; e initialFocus indirizza il focus della tastiera a un elemento specifico all'apertura. Ora passeremo ad animare le transizioni di apertura e chiusura delle finestre di dialogo utilizzando il componente Transition di Headless UI.

Gratis per iniziare

Impara HTML con un tutor IA — gratis

Scrivi ed esegui vero codice nel tuo browser, ricevi aiuto istantaneo da un tutor IA disponibile 24/7, e riprendi da dove hai lasciato sul web o nell'app.

Corsi
30
Lezioni
120

Domande Frequenti

La lezione «Creare dialog accessibili» è gratuita?

Sì — il testo completo di «Creare dialog accessibili» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Tailwind CSS Academy, passa a CoddyKit PRO. Il corso Tailwind CSS Academy include 4 lezioni in totale.

Cosa imparerò in «Creare dialog accessibili»?

Utilizzi il componente Dialog di Headless UI per le finestre modali, con gestione integrata del focus e del tasto Esc, applicando lo stile interamente con Tailwind. Eserciti Tailwind CSS Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare Tailwind CSS Academy?

Non è richiesta alcuna esperienza precedente. Tailwind CSS Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.

Quanto tempo richiede la lezione «Creare dialog accessibili»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione Tailwind CSS Academy?

Sì. Ogni lezione Tailwind CSS Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Introduzione a Headless UI
  2. Dare stile a Menu e Dropdown headless
  3. Creare dialog accessibili
  4. Le transizioni con Headless UI
← Torna a Tailwind CSS Academy