base-ui icon indicating copy to clipboard operation
base-ui copied to clipboard

[popover] Is there a way to render the `Popover` oven an existing element in the DOM?

Open oleksandr-danylchenko opened this issue 6 months ago • 4 comments

Get help

Context:

  • On the page, I have the special element with the id="special-trigger".
  • I have an existing SpecialPopover component that contains all the styles and content formatting logic.

I want to render the SpecialPopover component over the div#special-trigger, queried from the DOM using the querySelector().

Issue:

Unfortunately, the Popover doesn't allow me to use anything beyond its own React tree as a trigger. Also, I can't pass the HTMLElement as a trigger. As it'll create a duplicate in the DOM.

Example

Codesandbox - https://codesandbox.io/p/sandbox/external-popover-trigger-forked-g463pp

Questions

Is there a recommended way to deal with rendering the Popover component over an arbitrary element in the DOM, using it as the Trigger? That includes forwarding the Popover.Trigger props onto that element, including the event listeners. Image


Previously, we used the tippy.js imperative constructor - https://atomiks.github.io/tippyjs/v6/constructor/. It allows assigning popups to an existing HTML element pretty trivially:

tippy(specialElement, {
  content: "Here's the default content",
  trigger: 'click'
});

Unfortunately, the technology is deprecated, has its a11y challenges and rough edges around focus/interactions handling.

oleksandr-danylchenko avatar Jun 24 '25 15:06 oleksandr-danylchenko

I also tried using a combo with passing the anchor to the Popover.Positioner and manually handling the events/attributes/state over a queried element. However, it quickly turns into a clumsy pile of code that basically tries to replicate the Popover.Trigger. It's especially painful if you have a single styled Popover component that tries to handle both trigger and trigger-less variants.

oleksandr-danylchenko avatar Jun 24 '25 15:06 oleksandr-danylchenko

tippy attaches native event listeners while this library primarily uses React props. This means in React, the data (props in this case) flow from top to bottom, unless you synchronize the props with an effect higher in the tree (pretty messy and not performant).

So the Popover.Root needs to be higher in the component tree than where the trigger is; this way Popover.Trigger can be used in the JSX without an error since it has the data available to it, and Popover.Portal etc can be used elsewhere in the child tree

What's the use case for an external trigger?

atomiks avatar Jun 24 '25 22:06 atomiks

tippy attaches native event listeners while this library primarily uses React props. This means in React, the data (props in this case) flow from top to bottom, unless you synchronize the props with an effect higher in the tree (pretty messy and not performant).

That's right. I tried to implement the mentioned synchronization layer between the native element and the Popover rendered via the React.createPortal. But you're correct that it turns messy quite quickly. Also, there's no convenient way to forward the Trigger props up besides using a bunch of callbacks.

What's the use case for an external trigger?

The consumer app can be abstractly described as the "text-reader". Users there can have the "approved" status that allows them to navigate to an arbitrary page. But the "non-approved" users can only read up to N pages. Therefore, when they try to click on the link that goes beyond that N range, a popover with a warning message should appear. The links are rendered within the textual content, and they have a special data-page-family-id attribute. The consumer app can't control their rendering or JSX layout directly. But only query the elements from the DOM upon the render, via the contentNode.querySelectorAll('a[data-page-family-id]'). That's because the textual content itself is rendered using another Text React component coming from the organization's shared library. Therefore, I'd like to find a way to attach the Popover component to those links on the consumer's level. Ideally, using them as triggers, w/o duplicating the content, or some other black magic. Another alternative is to keep using the tippy.js and try to style it in the same way as the Popover. But it leaves a tech debt, a11y issues, and inconsistent "source of truth" for the popovers rendering.

oleksandr-danylchenko avatar Jun 25 '25 07:06 oleksandr-danylchenko

Hey all, I have tried fix this issue please check this PR :https://github.com/mui/base-ui/pull/2168

piyushzala158 avatar Jun 25 '25 17:06 piyushzala158