feat(ui): prototype automatic Shadow DOM style isolation - #322
Conversation
🦋 Changeset detectedLatest commit: f735fcb The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
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 |
mic-mart
left a comment
There was a problem hiding this comment.
Code review — all clear
No blocking issues. This is a well-scoped, well-documented Shadow DOM isolation POC: the shadow-root attach + StrictMode single-attach guard, the per-document constructable-stylesheet cache, the <style> fallback path, and ref forwarding through the HOC all check out. No functional bugs and no CLAUDE.md violations were found, and the known limitations (empty SSR host, post-hydration ref timing, @font-face leakage) are correctly captured in ADR 0005 and reflected in the major changeset.
The three inline notes below are informational only. Each scored low confidence/impact in review and none should block this PR — they're recorded for the eventual package-wide rollout.
🤖 Generated with Claude Code
- If this code review was useful, please react with 👍. Otherwise, react with 👎.
| For !important declarations on a shadow host, the shadow-tree declaration | ||
| outranks an outer author declaration by design. */ | ||
| :host::before, | ||
| :host::after { |
There was a problem hiding this comment.
Informational only — low confidence, non-blocking.
This :host::before/::after block is added outside any @layer, unlike every other SDK rule in this file (which live in @layer yv-sdk-*, per the file header and commit 694325f).
Caveat: there is no functional impact. :host only matches inside a shadow tree, and the shadow-context !important already outranks outer author CSS regardless of layer placement, so nothing leaks into the consumer cascade. This is purely a convention nit — if you want to keep the file's layer discipline consistent you could add a short comment marking it a deliberate exception, but it is safe as-is.
| data-yv-theme={theme} | ||
| className={cn( | ||
| 'yv:shadow-none yv:p-3 yv:h-auto yv:w-fit', | ||
| 'yv:font-sans yv:shadow-none yv:p-3 yv:h-auto yv:w-fit', |
There was a problem hiding this comment.
Informational only — low confidence, non-blocking.
The added yv:font-sans (here and at L239) is redundant: the SDK preflight in packages/core/src/styles/theme.css already applies font-family: var(--yv-font-sans) to every :where([data-yv-sdk]) *, and that preflight ships inside __YV_STYLES__, which is adopted into the shadow root — so the button's font was never relying on light-DOM inheritance.
Caveat: the class is harmless and renders correctly — this is not a bug. The only real nit is that ADR 0005's rationale ("the reset removes the light-DOM font inheritance the button previously relied on") slightly misreads the mechanism. No code change needed; optionally tweak the ADR wording.
| * Automatically rendered in a Shadow DOM so host-page selectors cannot style | ||
| * the button's internal DOM. No consumer wrapper or opt-in flag is required. | ||
| */ | ||
| export const YouVersionAuthButton = withShadowIsolation( |
There was a problem hiding this comment.
Informational only — low confidence, non-blocking (out of scope for this POC).
Isolation only applies because this component is manually wrapped in withShadowIsolation. As the pattern rolls out, a future component that forgets the wrapper would silently lose isolation, with no guardrail test catching it (echoes a concern raised on #215).
Caveat: this PR explicitly defers package-wide rollout — "Rollout to all exported components" is in ADR 0005's Deliberately deferred list — so this is a note for that future phase, not something to address here.
cameronapak
left a comment
There was a problem hiding this comment.
My overall assessment is this is a cool experiment, and I appreciate how Austin walked through his thinking and work clearly in his YouTube video.
This is a hard problem to solve, and I think that this scratches the surface in showing that Shadow DOM is a viable option, given some of the derred things are solved
I think no option to solve this problem will be incredibly straightforward, so which of the options are the lesser of two, or more, evils. (per se.)
There were time constraints that Austin was given, and this was/is a hard problem to solve. So my overall assessment is I think that Austin did a good work in showing a viable solution
| `YouVersionAuthButton` automatically creates an open shadow root and renders its | ||
| existing implementation inside it through a React portal. The SDK's compiled | ||
| Tailwind CSS—generated from `src/styles/global.css` and embedded as | ||
| `__YV_STYLES__`—is installed inside that root. Consumers continue to write | ||
| `<YouVersionAuthButton />`; isolation is not an option they must discover or | ||
| enable. |
There was a problem hiding this comment.
praise: For dev experience, I like backwards compatability. Looks like this is not going to bork everyone's pre-existing code
| ## Compatibility impact | ||
|
|
||
| Although the React props API is unchanged, the rendered DOM structure is not. | ||
| Consumers that query or style internal light-DOM markup must instead account for | ||
| the shadow root. Because the prototype attaches the shadow root in `useEffect`, | ||
| server output contains an empty host. The button appears after hydration, and | ||
| its forwarded ref becomes available later than it did previously. This is | ||
| therefore represented as a breaking change rather than an implementation | ||
| detail. |
There was a problem hiding this comment.
note: Good to know. It would be a breaking change.
| ## Deliberately deferred | ||
|
|
||
| - Rollout to all exported components. | ||
| - Radix popover/dialog portal placement and focus management. |
There was a problem hiding this comment.
note: Yeah, this one is a big one. Would it still work in the context of the popover and dialog with portals?
There was a problem hiding this comment.
Appreciate the callout in https://www.youtube.com/watch?v=MdSEjq49CMk
| ## Deliberately deferred | ||
|
|
||
| - Rollout to all exported components. | ||
| - Radix popover/dialog portal placement and focus management. | ||
| - Form association when controls live outside their form's tree scope. | ||
| - SSR/hydration and the first client paint. | ||
| - A package-wide custom-property audit. `all: initial` does not reset custom | ||
| properties; the larger investigation branch tested redeclaring Tailwind v4's | ||
| generated theme tokens on the protected internal wrapper. | ||
| - A deliberate inheritance policy for writing direction and future custom | ||
| properties. Some host values may be intentional localization inputs, while | ||
| SDK-owned visual tokens need shadow-local defaults. | ||
| - Host `@font-face` rules, which are not scoped by Shadow DOM. | ||
| - Ancestor layout constraints, which Shadow DOM cannot isolate. | ||
| - Event retargeting, nested-root behavior, and a supported consumer customization | ||
| model. | ||
| - Stylesheet construction/adoption failure recovery beyond feature fallback. | ||
| - A full browser and assistive-technology matrix; current browser verification is | ||
| Chromium-focused. | ||
| - Consumer test-query migration guidance and a component-by-component rollout. |
There was a problem hiding this comment.
praise: I appreciate seeing this deferred section. The communication is helpful
There was a problem hiding this comment.
This was a great demo in https://www.youtube.com/watch?v=MdSEjq49CMk!
Addresses PR review feedback: the non-adoptedStyleSheets fallback rendered a bare <style> tag inside the shadow-root portal. Use React 19's <style href precedence> resource form instead so React hoists and de-duplicates the stylesheet within the shadow root rather than risking duplicate insertion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Same-repo copy is up so CI can read Fork PRs do not get those secrets. This branch is the same SHA ( Code Implementor, sent on behalf of Cam. I am AI. |
|
Closed due to #322 (comment). Feature PR branch available at #348 |
Note
This is an architectural proof of concept on one representative component,
not a package-wide rollout.
Summary
Host applications can apply global CSS—Tailwind preflight or even a plain
button { ... }rule—to React SDK component internals and substantially changetheir appearance.
This PR prototypes automatic Shadow DOM isolation on
YouVersionAuthButton. Consumers continue rendering the component normally,with no wrapper or opt-in.
Shadow DOM was selected because stronger selectors, resets,
!important,cascade layers, and
@scopestill participate in the host document's cascade.They can reduce conflicts, but they cannot prevent outside selectors from
matching SDK internals.
Changes
Root while preserving its props and forwarded ref.
<style>fallback.pseudo-element attacks.
existing interactions, and same-origin iframe mounting.
controls beside the isolated SDK button.
ADR 0005.
Start here: run the Vite example and open Hostile CSS. The light-DOM
controls should visibly break while the SDK button remains stable.
What this proves
For
YouVersionAuthButton, the POC guards against the principal host-author CSSvectors:
!importantdeclarations::beforeand::aftergenerated contentThe
@font-facecheckbox demonstrates the primary exception: font-familyregistrations remain document-wide and are not isolated by Shadow DOM.
Known limitations
Although the React props API is unchanged, the rendered DOM structure changes.
Consumer queries, automation, SSR behavior, ref timing, and native event targets
may be affected. A package-wide rollout also requires validation of forms,
portals, focus, accessibility, custom properties, performance, and additional
browsers.
This PR asks whether Shadow DOM is the right architectural foundation before
expanding the approach to other components.
Test plan
Passed locally:
pnpm lintpnpm typecheckThe combined UI integration run completed 449 of 450 tests. One unrelated
Bible Reader test exceeded its five-second timeout under full-suite concurrency;
that complete file passes 33/33 when run independently.
Greptile Summary
The PR prototypes automatic Shadow DOM style isolation for
YouVersionAuthButton.<style>fallback.Confidence Score: 5/5
The PR appears safe to merge because no blocking failure remains within the eligible follow-up review scope.
No blocking failure remains.
Important Files Changed
ShadowRootHost.Sequence Diagram
Reviews (2): Last reviewed commit: "fix(ui): use style precedence for shadow..." | Re-trigger Greptile
Context used: