Errors and fixes

Shadcn: DialogContent requires a DialogTitle, and how to fix it properly

Radix warns when a dialog has no accessible name. The fix needs a title, even an invisible one, and adding only `aria-label` is the second step.

Key points

  • The warning is not cosmetic: without a title the dialog has no name a screen reader can announce.
  • The minimal fix is `` inside `DialogContent`, with text that matches the context.
  • `aria-label` on `DialogContent` is the alternative when the design has no visible title.

Short answer: Radix warns that the dialog has no accessible name. Add <DialogTitle> inside <DialogContent>, visible or with className="sr-only", and the warning goes away with the problem.

The exact message

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

Why it happens

A dialog is a modal window, so a screen reader must announce what opened. Radix links the title to the container with aria-labelledby. Without a title it has no name to announce, so it reports the problem in the console.

Styling alone does not help, because the accessible name must exist in the DOM. An <h2> outside the dialog content is never linked automatically.

How Radix builds the name

When DialogContent mounts, Radix generates an id with useId, puts it on the title node and links it to the container through aria-labelledby. The same mechanism covers DialogDescription and aria-describedby. The title need not be visible: it only has to exist in the React tree while the dialog is open, because the context Radix reads is filled when the component mounts.

The content renders into a portal, outside the node that holds the trigger. That is why a title placed next to the trigger, or inside a component higher up, takes no part: Radix reads the dialog context, not the document. Its check is binary, title present or absent, and the message appears when the context is empty.

The fix

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

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

When the design already has a visible title, use it and drop sr-only. A visible title beats an invisible one, because it also helps people who do not use a screen reader.

If the design has no title and you want no hidden node, pass the name to the content: <DialogContent aria-label="Confirm deletion">. Radix accepts aria-label and aria-labelledby as an alternative.

The diagnostic step

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

If id is null, the dialog has no linked name and the fix has not landed yet. If id exists but resolves is false, the reference points at a node that is gone: that happens when two dialogs share a title through a hand-written id. If both values are good and the warning stays, the culprit is a second dialog mounted off screen rather than the one you are looking at.

When the fix does not work

If the warning stays, check that DialogTitle sits inside DialogContent and not inside DialogTrigger. The trigger renders outside the portal and takes no part in the name. A second case is a wrapper that swaps the dialog content for a custom version: if the wrapper never exposes DialogTitle, Radix cannot see it.

When the usual fix makes it worse

The shortcut is to paste <DialogTitle className="sr-only">Dialog</DialogTitle> into every component. It silences the warning, not the problem for the listener: every dialog on the page announces the same word, and someone using a screen reader can no longer tell which window opened. Worse, whoever adds aria-label as well believes the text is fixed: when aria-labelledby is present the computed name comes from the title and aria-label is ignored. The announcement stays the generic string written in the title.

A named limitation

The documented limit of this protection: the warning exists only in development. Radix prints it with console.warn in the development build and strips it from the production one, so a dialog with no name ships without noise. No error, no crash, just a screen reader user facing a silent container. The only defence left is a test that opens the dialog and queries the role, not the console.

How to prevent it

  • Add a test that opens every dialog and checks for a node with role="dialog" and an accessible name.
  • In code review, treat the warning as an error: it is not noise.
  • For confirmation dialogs, always include a description as well: the name says what, the description says why.
  • Write the title with the text of the context, not the generic word of the component.

Where to go next

See a full dialog in the catalogue: dialog block with a preview, or browse the dialog category and all components and the glossary for accessibility terms. The guides are the React accessibility checklist and dark mode without duplicating the design system; the comparison of primitives is Radix, Base UI and React Aria.

Frequently asked questions

Can I switch the warning off?

Better not. The warning reports a real accessibility problem, a dialog with no name, and the fix costs one line.

Must the title be visible?

No. If the design has no title, use className="sr-only": the node stays in the DOM and screen readers announce it.

Is aria-label on DialogContent enough?

Yes, if the value describes the content. Radix accepts aria-label and aria-labelledby as an alternative accessible name.

Do I need DialogDescription too?

Radix warns separately when the description is missing. If the dialog has introductory text, use it; otherwise aria-describedby={undefined} is the explicit choice.

Does this apply to Sheet and Drawer?

Yes, they are the same Radix primitive with another style. The accessible name is required in every case.