feat(ui): prototype automatic Shadow DOM style isolation - #322
Conversation
🦋 Changeset detectedLatest commit: 7a311b3 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.
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 isolation for
YouVersionAuthButtonwhile preserving its React props and forwarded ref.<style>fallback.Confidence Score: 5/5
The prototype appears safe to merge within its explicitly documented proof-of-concept scope.
The changed component mounts through an open shadow root, installs styles with a feature-detected fallback, preserves its forwarded ref and interaction path, and includes focused coverage for the principal behavior introduced; the remaining compatibility effects are explicitly documented as known prototype limitations.
Important Files Changed
ShadowRootHost.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart LR Consumer[Consumer renders YouVersionAuthButton] --> Wrapper[withShadowIsolation] Wrapper --> Host[Light-DOM shadow host] Host --> Root[Open ShadowRoot] Root --> Styles[Per-document constructed stylesheet or style fallback] Root --> Portal[React portal] Portal --> Button[YouVersionAuthButtonImpl] HostCSS[Host application CSS] -. blocked from selecting internals .-> RootReviews (1): Last reviewed commit: "docs(examples): keep hostile demo labels..." | Re-trigger Greptile