Skip to content

Conversation

jakiestfu
Copy link
Contributor

@jakiestfu jakiestfu commented Sep 25, 2025

@AlemTuzlak Hello again! I have built a feature to render the devtools in a shadow dom node. This ensures that styles are not inherited from the parent page. However, there are some caveats and/or things I'm not 100% certain I understand, and wanted to pose this PR to perhaps start those discussions.

As you can see, I've added some styles to the basic example that aggressively target the styles of the devtools UI. After the change, there is no interference.

Before After
before after

Copy link

changeset-bot bot commented Sep 25, 2025

🦋 Changeset detected

Latest commit: 4b3b456

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

This PR includes changesets to release 3 packages
Name Type
@tanstack/react-devtools Minor
@tanstack/devtools Minor
@tanstack/solid-devtools 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

pkg-pr-new bot commented Sep 25, 2025

More templates

@tanstack/devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools@181

@tanstack/devtools-ui

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-ui@181

@tanstack/devtools-utils

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-utils@181

@tanstack/devtools-vite

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-vite@181

@tanstack/devtools-event-bus

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-event-bus@181

@tanstack/devtools-event-client

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-event-client@181

@tanstack/react-devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/react-devtools@181

@tanstack/solid-devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/solid-devtools@181

commit: 4b3b456

observer.disconnect()
})
}
})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

So "syncing" the styles from the parent page to the shadow DOM node is not what I initially set out to do. In fact, there were two approaches I made prior to this, but both failed.

  • Approach 1: Leverage goobers css.bind({ target: <element> }) feature.

    • This basically involves creating a context provider that binds the CSS target and passes that down to child components to use.
    • This looked like a useGoober hook in the UI library, that provides the CSS bound target for the shadow node
    • Just did not work. Even when binding the target manually, goober always appended the styles to the head of the owner document and not the shadow root. Both UI and devtool packages were implementing the context packages correctly.
    • Reference material, Goober Docs
    • Perhaps this is worth a revisit
  • Approach 2: Just do Goobers extractCSS

    • Only works on the server 🤷

}))
}

setPluginContainers((prev) => ({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because we are rendering the component in a shadow DOM node, e.ownerDocument.getElementById(id) always returns null and the plugins are never mounted.

I'm not certain I understand what this if statement guards against since I thought the elements were defined by the devtools plugin itself. Shouldn't they always exist?

@jakiestfu jakiestfu changed the title feat(): render devtools in a shadow node feat: render devtools in a shadow node Sep 25, 2025
@jakiestfu
Copy link
Contributor Author

CC @TakhyunKim @colelawrence

@AlemTuzlak
Copy link
Collaborator

Thank you for the PR, I'll review it in more details as soon as I can, before then, what exactly would be the benefit of rendering it in a shadow dom besides escaping global styling?

@jakiestfu
Copy link
Contributor Author

jakiestfu commented Sep 29, 2025

Of course, @AlemTuzlak!

I honestly haven't really thought about any other benefits but I guess this is what I found after a little research:

Beyond that, I'm not sure what additional benefits we receive. In all honesty, I would hope style isolation would be an absolute requirement for this package if its intended to be used across any application. I've tried to introduce this into our organization, but our CSS reset is getting in the way of the styles in the devtools and things are quite visually broken.

@jakiestfu
Copy link
Contributor Author

As an FYI, I was able to end up binding the goober css function to render inside of the shadow node more idiomatically (using context throughout the codebase for css) but this only solves styles for devtools and devtools-ui. Unfortunately, additional packages like @tanstack/react-query-devtools and @tanstack/react-router-devtools just default to their current document context and still end up appending their styles to the head of the document instead of inside the shadow node.

All this to say, the current solution (in this PR) syncs styles from all goober usage on the page, to within the shadow root and it works perfectly fine. The code for the pip styling was inspiration for this.

This was the proof-of-concept for the context stuff if you cared to see, but it seems like it would be a pain to continue down this route.

// packages/devtools-ui/src/components/shadow-root.tsx

import { createContext, onCleanup, onMount, useContext } from 'solid-js'
import { render } from 'solid-js/web'
import { css as gooberCss } from 'goober'
import type { ParentComponent } from 'solid-js'

export type CssFn = typeof gooberCss
type GooberCtx = { css: CssFn }

const GooberContext = createContext<GooberCtx>()

export const useCss = () => {
  const ctx = useContext(GooberContext)
  if (!ctx) throw new Error('useCss must be used inside <ShadowRoot>')
  return ctx
}

export const ShadowRoot: ParentComponent = (props) => {
  let host!: HTMLDivElement

  onMount(() => {
    const shadow = host.attachShadow({ mode: 'open' })
    const ctx: GooberCtx = { css: gooberCss.bind({ target: shadow }) }

    const dispose = render(
      () => (
        <GooberContext.Provider value={ctx}>
          {props.children}
        </GooberContext.Provider>
      ),
      shadow,
    )
    onCleanup(dispose)
  })

  return <div ref={host} />
}

@colelawrence
Copy link
Contributor

I had a question @jakiestfu - I already started using the DevTools to be a frame for some existing UI pieces I'm using in my app. Does this affect that use case? I'd rather not have to muck around with css injection or something to get around the shadow dom.

@jakiestfu
Copy link
Contributor Author

jakiestfu commented Oct 1, 2025

Do you mean this frame, @colelawrence? If so, I have no idea. I could explore your use-cases for you if you wanted to share a little more or high-level it for me.

MDN does note frame is a deprecated feature and Shadow DOM is shiny and new and stuff 🤷

And if by chance you mean iframe, that comes with its own complexities regarding postMessage and stuff. Unless they're on the same origin.


I'd rather not have to muck around with css injection

We already do that for the PIP window though because we're using Goober 🥺

@jakiestfu
Copy link
Contributor Author

I apologize in advance for my eagerness to have this shipped or explored by the maintainers. The lack of style sandboxing is fully preventing us at Turo from adopting devtools internally as the styles are well... broken

Stronger CSS styling could help this problem, but that pushes the work and effort unto the end-users of the devtools package as opposed to the package itself handling this.

What can I do for ya'll to help support your efforts?

@AlemTuzlak
Copy link
Collaborator

Sorry I had some deadlines I needed to hit so I had no time to look at this, will get to it this week definitely

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.

3 participants