Errori e fix

Shadcn: DialogContent requires a DialogTitle, come risolverlo davvero

Radix avvisa quando un dialog non ha un nome accessibile. La correzione richiede un titolo, anche invisibile: aggiungere solo `aria-label` è il secondo passo.

In sintesi

  • L'avviso non è cosmetico: senza titolo il dialog non ha un nome accessibile per uno screen reader.
  • La correzione minima è `` dentro `DialogContent`, con lo stesso testo del contesto.
  • `aria-label` su `DialogContent` è l'alternativa quando il titolo visivo non esiste per scelta di design.

Risposta breve: Radix avvisa che il dialog non ha un nome accessibile. Aggiungi <DialogTitle> dentro <DialogContent>, visibile o con className="sr-only", e l'avviso sparisce insieme al problema.

Il messaggio esatto

`DialogContent` requires a `DialogTitle` for the component to be
accessible for screen reader users.

If you are not rendering a visible title, you can use the
`DialogTitle` component with the `sr-only` class.

Perché succede

Un dialog è una finestra modale: uno screen reader deve annunciare cosa si è aperto. Radix collega il titolo al contenitore con aria-labelledby; se non trova il titolo, non ha un nome da annunciare e lo segnala in console.

Aggiungere solo uno stile non basta: il nome accessibile deve esistere nel DOM. Un <h2> fuori dal contenuto del dialog non viene collegato automaticamente.

Come Radix costruisce il nome

Quando DialogContent monta, Radix genera un id con useId, lo mette sul nodo del titolo e lo collega al contenitore tramite aria-labelledby. Lo stesso meccanismo vale per DialogDescription e aria-describedby. Il titolo non deve essere visibile: deve solo esistere nell'albero React mentre il dialog è aperto, perché il contesto che Radix legge è popolato al montaggio del componente.

Il contenuto viene renderizzato in un portal, fuori dal nodo che contiene il trigger. Per questo un titolo scritto accanto al trigger, o in un componente che vive più in alto, non partecipa: Radix guarda solo il contesto del dialog, non il documento. La verifica che fa è binaria, titolo presente o assente, e il messaggio appare quando il contesto è vuoto.

La correzione

import {
  Dialog, DialogContent, DialogDescription,
  DialogHeader, DialogTitle, DialogTrigger,
} from "@/components/ui/dialog";

export function ConfirmDelete() {
  return (
    
      Elimina
      
        
          Conferma eliminazione
          
            L'operazione non può essere annullata.
          
        
      
    
  );
}

Quando il titolo visivo esiste già nel design, usa quello e togli sr-only: un titolo visibile è meglio di uno invisibile, perché aiuta anche chi non usa uno screen reader.

Se il design non prevede un titolo e non vuoi un nodo nascosto, passa un nome direttamente al contenuto: <DialogContent aria-label="Conferma eliminazione">. Radix accetta aria-label e aria-labelledby come alternativa.

Il passo diagnostico

// browser console, with the dialog open
const d = document.querySelector('[role="dialog"]');
const id = d && d.getAttribute("aria-labelledby");
console.log({ id, resolves: Boolean(id && document.getElementById(id)) });

Se id è null, il dialog non ha un nome collegato e la correzione non è ancora passata. Se id esiste ma resolves è false, il riferimento punta a un nodo che non c'è più: succede quando due dialog condividono lo stesso titolo tramite un id scritto a mano. Se entrambi i valori sono buoni e l'avviso resta, il problema è un secondo dialog montato fuori schermo, non quello che stai guardando.

Quando la correzione non funziona

Se l'avviso resta, controlla di aver messo DialogTitle dentro DialogContent e non dentro DialogTrigger: il trigger viene renderizzato fuori dal portale e non partecipa al nome. Un secondo caso è un wrapper che sostituisce il contenuto del dialog con una versione custom: se il wrapper non espone DialogTitle, Radix non lo vede.

Quando la correzione comune peggiora le cose

La scorciatoia è copiare <DialogTitle className="sr-only">Dialog</DialogTitle> in ogni componente. Funziona per l'avviso, non per chi ascolta: tutti i dialog della pagina si annunciano con la stessa parola, e chi naviga con uno screen reader non sa più quale finestra ha aperto. Peggio, chi aggiunge anche aria-label crede di aver corretto il testo: quando aria-labelledby è presente, il nome calcolato viene dal titolo e aria-label viene ignorata. Il testo resta quello generico scritto nel titolo.

Un limite con un nome

Limite dichiarato di questa protezione: l'avviso esiste solo in sviluppo. Radix lo stampa con console.warn nel build di sviluppo e lo rimuove da quello di produzione, quindi un dialog senza nome arriva in produzione senza rumore. Nessun errore, nessun crash: solo un utente con screen reader davanti a un contenitore muto. La sola difesa che resta è un test che apre il dialog e interroga il ruolo, non la console.

Come prevenirlo

  • Aggiungi un test che apre ogni dialog e verifica che esista un nodo con role="dialog" e un nome accessibile.
  • Nel code review, tratta l'avviso come errore: non è rumore.
  • Per i dialog di sola conferma, includi sempre anche una descrizione: il nome dice cosa, la descrizione dice perché.
  • Scrivi il titolo con il testo del contesto, non la parola generica del componente.

Dove continuare

Guarda un dialog completo nel catalogo: blocco dialog con preview, oppure sfoglia la categoria dialog e tutti i componenti e il glossario per i termini di accessibilità. Le guide sono checklist di accessibilità React e dark mode senza duplicare il design system; il confronto tra primitive è Radix, Base UI e React Aria.

Domande frequenti

Posso disattivare l'avviso?

Non conviene. L'avviso segnala un problema di accessibilità reale: un dialog senza nome. La correzione costa una riga.

Il titolo deve essere visibile?

No. Se il design non prevede un titolo, usa `className="sr-only"`: resta nel DOM e viene letto dagli screen reader.

Basta `aria-label` su DialogContent?

Sì, se il valore descrive il contenuto. Radix accetta `aria-label` e `aria-labelledby` come nome accessibile alternativo.

Serve anche DialogDescription?

Radix avvisa separatamente quando manca la descrizione. Se il dialog ha testo introduttivo, usala; altrimenti `aria-describedby={undefined}` è la scelta esplicita.

Vale per Sheet e Drawer?

Sì: sono la stessa primitiva Radix con un altro stile. Il nome accessibile serve in tutti i casi.