primitives
                                
                                 primitives copied to clipboard
                                
                                    primitives copied to clipboard
                            
                            
                            
                        [Dialog][Popover] Scrolling issue when Popover inside Dialog
Documentation
Relevant Radix Component(s)
Here in the docs it say to make Overlay a sibling to Content.

This setup doesn't work if you have a scrollable popover in the dialog content. On this line a ref is passed to RemoveScroll but I think that by using just a ref that RemoveScroll doesn't consider the portalled element a part of the react tree.
In my own code I fixed this by putting my Content inside of my Overlay. After this change my scrollable popover could scroll again since RemoveScroll now has access to the react tree.

Both compositions are valid, for different use cases. In fact we have an example of a scrollable overlay here: https://www.radix-ui.com/docs/primitives/components/dialog#scrollable-overlay
It's not overlay that I need scrollable though. I'll come up with a code sandbox to show what I'm talking about
Here you can see if you follow all of the docs for both popover and dialog you can't scroll a popover in a dialog. I had to read through radix's source code and dependencies to figure out that I needed to wrap my content in the overlay. While both are valid I think the docs should steer people towards code+structure that will work out of the box without having to understand how the internals are actually working.
https://codesandbox.io/s/twilight-http-veu4gw?file=/App.js
And if you're unwilling to change that I think the docs should at least detail why both composition are valid and what the use cases actually are.
Oh sorry I misunderstood what you meant! That seems like a bug potentially, I'll take a look.
Hmm, yep looks like it could be a RemoveScroll shards thing 🙈 we need a ticket to create our own react-tree-aware remove scroll at some stage.
I'm running into the same issue. I'd like to adopt Radix for the design system I'm working on but we have some custom selects with a scrollable area (implemented as popover) which are rendered inside modals. Would it be possible to disable the scroll-blocking in the dialog components? Or just to enable scroll-blocking of the body since the rest is not scrollable because of the dialog overlay anyway.
To anyone reading, I just found a workaround. The scroll removal is implemented in the Overlay component.
https://github.com/radix-ui/primitives/blob/6da75e0dbb2d1aebd2dae5a044c595bca39a2347/packages/react/dialog/src/Dialog.tsx#L201-L219
So replacing the Dialog.Overlay with a div does the trick. Just keep in mind that you need to block body scroll and deal with the scrollbar yourself with this workaround.
Thanks @hipstersmoothie @dcastil ! It seems to fix my #1128 issue 🙏
Ran into this as well, when using non-Radix UI bits inside a Dialog.
Would really be nice to have an option to disable it.
Any news on this? scroll doesn't work on third parties with Radix dialog.
it's apparently related to the <Modal.Overlay /> component. removing it solves the issue
Yeah, this is still an issue for us
This is still an issue, any idea on a potential fix?
@Nhollas If you are using the Radix select, a possible workaround https://github.com/radix-ui/primitives/issues/2125#issuecomment-1545885362
i think this is something to do with the third-party remove-scroll lib that radix uses. for now you can set modal={false} on your Dialog. that might not be ideal for a11y (and will hide the overlay) but users will be able to scroll things for now at least.
modal={false} is the path I took as well. I had composed the library's components in a custom Dialog component. Here's a simple suggestion to substitute the component's overlay (creatively named Overlay2). Even though this is far from pretty, it “works well enough”. The styles applied to Overlay2 are identical to what I had applied to radix' Overlay.
// Above code removed for brevity
export default ({
  children,
  description,
  hasCloseButton = true,
  title,
  trigger,
  ...props
}: ComponentProps) => {
  const [open, setOpen] = useState(false)
  useEffect(() => {
    const body = document.querySelector('body')
    if (!body) return
    if (open) {
      body.style.overflow = 'hidden'
    } else {
      body.style.overflow = ''
    }
  }, [open])
  return (
    <Dialog.Root {...props} open={open} onOpenChange={setOpen}>
      <Dialog.Trigger asChild>{trigger}</Dialog.Trigger>
      {open && <StyledDialog.Overlay2 data-state={open ? 'open' : 'closed'} />}
      <StyledDialog.Content>
        {title && <StyledDialog.Title>{title}</StyledDialog.Title>}
        {description && (
          <StyledDialog.Description>{description}</StyledDialog.Description>
        )}
        {children}
        {hasCloseButton && (
          <StyledDialog.Close asChild>
            <Button shape="square" variant="flat">
              <XMarkIcon className="icon" />
            </Button>
          </StyledDialog.Close>
        )}
      </StyledDialog.Content>
    </Dialog.Root>
  )
}
// Using emotion
export const Overlay2 = styled.div({
  '&[data-state="closed"]': {
    animation: `${FadeFromTo({ from: 0.75, to: 0 })} 200ms ease-in`,
  },
  '&[data-state="open"]': {
    animation: `${FadeFromTo({ from: 0, to: 0.75 })} 300ms ease-out`,
  },
  backgroundColor: theme.color.gray['500'],
  inset: 0,
  opacity: 0.75,
  position: 'fixed',
  zIndex: 10002,
})
Another possible workaround is to render popover content directly inside dialog content and not in portal. So instead of:
<Popover.Portal>
    <Popover.Content />
</Popover.Portal>
you can do:
<Popover.Content />
For sure it doesn't cover all cases because some popovers have to be rendered in portal, but it has covered my case and works well. This solution doesn't force you to set modal={false} on <Dialog.Root />
To anyone reading, I just found a workaround. The scroll removal is implemented in the
Overlaycomponent.https://github.com/radix-ui/primitives/blob/6da75e0dbb2d1aebd2dae5a044c595bca39a2347/packages/react/dialog/src/Dialog.tsx#L201-L219
So replacing the
Dialog.Overlaywith adivdoes the trick. Just keep in mind that you need to block body scroll and deal with the scrollbar yourself with this workaround.
This was the best solution for me for now as it doesn't remove the overlay. It also fixed an issue where padding was getting added to the body of my application whenever the overlay was showing.
Thanks @dcastil
Possible Solution
I think it's possible to use the reference of a wrapper or another one that is inside the modal based on this:
Radix
const ExampleOfAnyPopoverInsideModal = () => {
    const containerRef = useRef<HTMLDivElement>(null);
    return (
        <div ref={containerRef}>
            <Popover.Root>
                <Popover.Trigger />
                <Popover.Anchor />
                <Popover.Portal>
                    <Popover.Content container={containerRef.current}>
                        <Popover.Close />
                        <Popover.Arrow />
                    </Popover.Content>
                </Popover.Portal>
            </Popover.Root>
        </div>
    );
};
Shadcn:
const ExampleOfAnyPopoverInsideModal = () => {
    const containerRef = useRef<HTMLDivElement>(null);
    const [open, setOpen] = useState(false);
    return (
        <div ref={containerRef}>
            <Popover open={open} onOpenChange={setOpen}>
                <PopoverTrigger asChild>
                    <Button
                        variant="outline"
                        role="combobox"
                        aria-expanded={open}
                        className="w-full justify-between"
                    >
                        <span className="truncate">Something</span>
                        <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
                    </Button>
                </PopoverTrigger>
                <PopoverContent
                    className="p-0"
                    style={{
                        width: containerRef.current?.offsetWidth
                    }}
                    container={containerRef.current}
                >
                    <Command>
                        //Command with scroll inside :)
                    </Command>
                </PopoverContent>
          </Popover>
      </div>
    );
};
// ...
// Added container prop to PopoverContent
const PopoverContent = React.forwardRef<
    React.ElementRef<typeof PopoverPrimitive.Content>,
    React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
        container?: HTMLElement | null;
    }
>(
    (
        { className, container, align = "center", sideOffset = 4, ...props },
        ref
    ) => (
        <PopoverPrimitive.Portal container={container}>
            <PopoverPrimitive.Content
                ref={ref}
                align={align}
                sideOffset={sideOffset}
                className={cn(
                    "z-50 w-72 rounded-lg border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
                    className
                )}
                {...props}
            />
        </PopoverPrimitive.Portal>
    )
);
// ...
A handy tip to anyone implementing the solution from @illodev - if this will seem like devs have to pass the ref as props all the way through a bunch of components, it may help to store the ref in a Context on the styled Dialog component for your popover component to read.
This helped me a lot! @illodev
I think solution suggested by @illodev defeats the purpose of portal, it's really no different than just removing the portal, and that's easier.
@ozguruysal that's correct.
The solution by @illodev also does not work if your Dialog.Content has overflow-auto set - since the div with the containerRef is rendered inside of it, the popover won't be rendered outside of the dialog.
Worth mentioning that this also happens to other components that renders in portal. i.e When rendering a react-select with menuPortalTarget in the radix dialog.
@benoitgrelard Is it possible to get a review on this PR? It sounds like it would solve this issue and it's been open for quite a while now so it would be good to get it merged. Thanks! https://github.com/radix-ui/primitives/pull/2250
also just ran into this as well!
facing this issue.. any other workarounds, other than removing portal from popover and DialogPrimitive.Overlay ? I would like to keep those.
Possible Solution
I think it's possible to use the reference of a wrapper or another one that is inside the modal based on this:

Radix
const ExampleOfAnyPopoverInsideModal = () => { const containerRef = useRef<HTMLDivElement>(null); return ( <div ref={containerRef}> <Popover.Root> <Popover.Trigger /> <Popover.Anchor /> <Popover.Portal> <Popover.Content container={containerRef.current}> <Popover.Close /> <Popover.Arrow /> </Popover.Content> </Popover.Portal> </Popover.Root> </div> ); };Shadcn:
const ExampleOfAnyPopoverInsideModal = () => { const containerRef = useRef<HTMLDivElement>(null); const [open, setOpen] = useState(false); return ( <div ref={containerRef}> <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={open} className="w-full justify-between" > <span className="truncate">Something</span> <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="p-0" style={{ width: containerRef.current?.offsetWidth }} container={containerRef.current} > <Command> //Command with scroll inside :) </Command> </PopoverContent> </Popover> </div> ); };// ... // Added container prop to PopoverContent const PopoverContent = React.forwardRef< React.ElementRef<typeof PopoverPrimitive.Content>, React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & { container?: HTMLElement | null; } >( ( { className, container, align = "center", sideOffset = 4, ...props }, ref ) => ( <PopoverPrimitive.Portal container={container}> <PopoverPrimitive.Content ref={ref} align={align} sideOffset={sideOffset} className={cn( "z-50 w-72 rounded-lg border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className )} {...props} /> </PopoverPrimitive.Portal> ) ); // ...
ILY <3
