Skip to content

fix(dialog): use getRootNode() for Shadow DOM accessibility check#3839

Open
sujiu222 wants to merge 4 commits intoradix-ui:mainfrom
sujiu222:fix/dialog-shadow-dom-accessibility
Open

fix(dialog): use getRootNode() for Shadow DOM accessibility check#3839
sujiu222 wants to merge 4 commits intoradix-ui:mainfrom
sujiu222:fix/dialog-shadow-dom-accessibility

Conversation

@sujiu222
Copy link
Copy Markdown

@sujiu222 sujiu222 commented Apr 3, 2026

Summary

Fixes #3814

Problem

Dialog component uses document.getElementById to find the DialogTitle and DialogDescription elements for accessibility validation. When Dialog is rendered inside a Shadow DOM, elements are not accessible via document.getElementById, causing false-positive accessibility warnings in the console:

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

Root Cause

Both TitleWarning and DescriptionWarning components called document.getElementById(id) to check for the presence of the title/description elements. In Shadow DOM, elements reside in a ShadowRoot, not in the top-level document, so document.getElementById always returns null.

Solution

Replace document.getElementById with contentRef.current?.getRootNode() to obtain the correct root node, then call getElementById on that root. This correctly handles both:

  • Regular DOM: getRootNode() returns the Document, same behavior as before
  • Shadow DOM: getRootNode() returns the ShadowRoot, which has its own getElementById (via the DocumentOrShadowRoot interface)

The TitleWarning component was also updated to receive contentRef (already available in DialogContentImpl) alongside the existing contentRef already passed to DescriptionWarning.

Changes

  • packages/react/dialog/src/dialog.tsx
    • Added contentRef prop to TitleWarningProps and TitleWarning component
    • Pass contentRef to <TitleWarning> in DialogContentImpl
    • Replace document.getElementById with (contentRef.current?.getRootNode() ?? document).getElementById in both warning components

Testing

  • Existing behavior is preserved for regular DOM usage (getRootNode() returns document in non-shadow context)
  • Shadow DOM context now correctly resolves accessibility checks

The DialogTitle and DialogDescription accessibility warnings used
document.getElementById to find elements for validation. When Dialog
is rendered inside a Shadow DOM, elements are not accessible via
document.getElementById, causing false-positive accessibility warnings
in the console.

Fix: Use contentRef.current.getRootNode() to obtain the correct root
node (Document or ShadowRoot), then call getElementById on that root.
This works in both regular DOM and Shadow DOM contexts, since ShadowRoot
implements DocumentOrShadowRoot which defines getElementById.

Fixes radix-ui#3814
Copilot AI review requested due to automatic review settings April 3, 2026 09:45
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 3, 2026

🦋 Changeset detected

Latest commit: d0960ca

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@radix-ui/react-dialog Patch
@radix-ui/react-alert-dialog Patch
radix-ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes false-positive Dialog accessibility warnings when Dialog is rendered inside a Shadow DOM by resolving title/description elements via the correct root node instead of the global document.

Changes:

  • Pass contentRef into TitleWarning so it can resolve elements relative to Dialog content.
  • Use contentRef.current.getRootNode() (with document fallback) to look up DialogTitle/DialogDescription by id in both warning components.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +522 to 526
// 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);
if (!hasTitle) console.error(MESSAGE);
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.
Comment on lines +548 to 552
// 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);
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.
Comment on lines +522 to +525
// 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.
getRootNode() may return a Node without getElementById (e.g. detached
subtree or DocumentFragment). Add instanceof guard and fall back to
ownerDocument ?? document in those cases.

Addresses Copilot review feedback on radix-ui#3839.
@sujiu222
Copy link
Copy Markdown
Author

sujiu222 commented Apr 3, 2026

Thanks for the review! Addressed all three points in the latest commit:

  1. Runtime guard for getRootNode(): Added instanceof Document || instanceof ShadowRoot check before calling getElementById. Falls back to contentRef.current?.ownerDocument ?? document for detached subtrees or DocumentFragment nodes. Applied to both TitleWarning and DescriptionWarning.

  2. Tests: Will add a Shadow DOM regression test — noted as a follow-up in this PR or a separate test-only PR depending on the test setup complexity.

Adds a test that mounts Dialog inside a ShadowRoot (attachShadow) and
asserts that no console.error accessibility warning is fired when
DialogTitle is present, verifying the getRootNode() fix works correctly.

Addresses Copilot review feedback on radix-ui#3839.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Dialog] validation errors in console when used inside Shadow-DOM

2 participants