Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-dialog-shadow-dom-accessibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@radix-ui/react-dialog': patch
---

Fix accessibility check for `DialogTitle` and `DialogDescription` when rendered inside Shadow DOM. Previously used `document.getElementById` which fails in Shadow DOM contexts; now uses `element.getRootNode()` to search within the correct document scope.
21 changes: 15 additions & 6 deletions packages/react/dialog/src/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ const DialogContentImpl = React.forwardRef<DialogContentImplElement, DialogConte
</FocusScope>
{process.env.NODE_ENV !== 'production' && (
<>
<TitleWarning titleId={context.titleId} />
<TitleWarning contentRef={contentRef} titleId={context.titleId} />
<DescriptionWarning contentRef={contentRef} descriptionId={context.descriptionId} />
</>
)}
Expand Down Expand Up @@ -503,9 +503,12 @@ const [WarningProvider, useWarningContext] = createContext(TITLE_WARNING_NAME, {
docsSlug: 'dialog',
});

type TitleWarningProps = { titleId?: string };
type TitleWarningProps = {
contentRef: React.RefObject<DialogContentElement | null>;
titleId?: string;
};

const TitleWarning: React.FC<TitleWarningProps> = ({ titleId }) => {
const TitleWarning: React.FC<TitleWarningProps> = ({ contentRef, titleId }) => {
const titleWarningContext = useWarningContext(TITLE_WARNING_NAME);

const MESSAGE = `\`${titleWarningContext.contentName}\` requires a \`${titleWarningContext.titleName}\` for the component to be accessible for screen reader users.
Expand All @@ -516,10 +519,13 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl

React.useEffect(() => {
if (titleId) {
const hasTitle = document.getElementById(titleId);
// Use getRootNode() to support Shadow DOM contexts where document.getElementById
// would fail to find elements rendered inside a shadow root.
const rootNode = contentRef.current?.getRootNode() ?? document;
const hasTitle = (rootNode as Document | ShadowRoot).getElementById(titleId);
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change adds Shadow DOM-specific behavior, but the existing test suite for these warnings doesn’t cover rendering the dialog inside a ShadowRoot. Adding a regression test (e.g. mounting into an element with attachShadow({mode: 'open'}) and asserting the title/description warnings don’t fire) would help ensure this doesn’t break in future refactors.

Copilot uses AI. Check for mistakes.
if (!hasTitle) console.error(MESSAGE);
Comment on lines +564 to 574
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getRootNode() can return a Node that is neither Document nor ShadowRoot (e.g. a detached subtree root element or a DocumentFragment). In those cases, the cast to Document | ShadowRoot can cause a runtime TypeError when calling getElementById. Consider guarding with a runtime check (e.g. verify rootNode has getElementById) and otherwise fall back to contentRef.current?.ownerDocument ?? document.

Copilot uses AI. Check for mistakes.
}
}, [MESSAGE, titleId]);
}, [MESSAGE, contentRef, titleId]);

return null;
};
Expand All @@ -539,7 +545,10 @@ const DescriptionWarning: React.FC<DescriptionWarningProps> = ({ contentRef, des
const describedById = contentRef.current?.getAttribute('aria-describedby');
// if we have an id and the user hasn't set aria-describedby={undefined}
if (descriptionId && describedById) {
const hasDescription = document.getElementById(descriptionId);
// Use getRootNode() to support Shadow DOM contexts where document.getElementById
// would fail to find elements rendered inside a shadow root.
const rootNode = contentRef.current?.getRootNode() ?? document;
const hasDescription = (rootNode as Document | ShadowRoot).getElementById(descriptionId);
if (!hasDescription) console.warn(MESSAGE);
Comment on lines +596 to 606
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern as in TitleWarning: contentRef.current?.getRootNode() may return a Node without getElementById, which would throw at runtime. Add a runtime guard (or resolve via ownerDocument unless the root is a ShadowRoot) before calling getElementById.

Copilot uses AI. Check for mistakes.
}
}, [MESSAGE, contentRef, descriptionId]);
Expand Down