ui icon indicating copy to clipboard operation
ui copied to clipboard

Dialog Overflow Behavior

Open jnsdrssn opened this issue 2 years ago • 35 comments

When using the dialog with content larger than the screen height, the dialog gets cut off on top and bottom without the ability to scroll.

Steps to reproduce: Put component with height > screen height inside of Dialog. Intended behavior: Scroll

First ever Issue, sorry if its unclear. Happy to elaborate. Thanks a lot for making this open source. It is invaluable!

jnsdrssn avatar Jan 25 '23 17:01 jnsdrssn

I thought it might help to add a repo demonstrating the issue, so I threw one together. It's a fresh install, modifying the index page to have the Dialog component with an inline style of 5000px.

https://github.com/joshwaiam/ui.shadcn.com-dialog-overflow-issue

squishydough avatar Jan 25 '23 19:01 squishydough

Is this a Radix UI issue or?

Nhollas avatar May 12 '23 13:05 Nhollas

Workaround: I fixed this problem for myself by adding overflow-y-scroll max-h-screen classes to DialogContent

example usage:

<Dialog>
  <DialogTrigger asChild>
    <Button variant={"ghost"} title={"Text""} />
  </DialogTrigger>
  <DialogContent className={"lg:max-w-screen-lg overflow-y-scroll max-h-screen"}>
   todo...
  </DialogContent>
</Dialog>

adaptive-shield-matrix avatar Jun 22 '23 12:06 adaptive-shield-matrix

I had a similar problem with the sheet component and too much content, but the overflow-y-scroll max-h-screen fix worked there too. Thanks a lot!

anOatFlake avatar Jul 02 '23 16:07 anOatFlake

I always had the issue with the scrollbar on the right side of the content. Bildschirmfoto 2023-07-05 um 11 07 49

There is a solution by RadixUI that worked fine for me: https://www.radix-ui.com/docs/primitives/components/dialog#scrollable-overlay

I enables scrolling the background thus "moving" the dialog instead of scrolling inside which I found to be a nicer interaction. You need to move the content inside the overlay and change the way it handles the positioning of the content. (I removed some of the animations because I was too lazy to adjust them)

DialogOverlay

<DialogPrimitive.Overlay
    ref={ref}
    className={cn(
      "fixed inset-0 z-50 bg-black/30 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 overflow-y-auto max-h-screen grid place-items-center",
      className
    )}
    {...props}
  />

DialogContent

<DialogPortal>
    <DialogOverlay>
      <DialogPrimitive.Content
        ref={ref}
        className={cn(
          "z-50 relative grid w-full max-w-lg gap-4 bg-background p-6 shadow-lg duration-200 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 sm:rounded-lg md:w-full",
          className
        )}
        {...props}
      >
        {children}
        <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
          <X className="h-4 w-4" />
          <span className="sr-only">Close</span>
        </DialogPrimitive.Close>
      </DialogPrimitive.Content>
    </DialogOverlay>
  </DialogPortal>

edit: Content needs to be set to relative so the closing-button gets rendered on the content. otherwise its at the top right of the overlay

max-hans avatar Jul 05 '23 09:07 max-hans

@max-hans If I try to replicate your example -> I get a darkened screen without the Dialog being displayed at all. I tried following https://www.radix-ui.com/docs/primitives/components/dialog#scrollable-overlay , but It did not show the Dialog too. Maybe I'am doing something wrong?

DialogOverlay

const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<DialogPrimitive.Overlay
  className={cn(
    "fixed inset-0 z-50 ", // overlay
    "bg-black/50 backdrop-blur-sm", // bg
    "transition-all duration-100 data-[state=closed]:animate-out data-[state=open]:fade-in data-[state=closed]:fade-out", // animations
    "grid place-items-center overflow-y-auto", // layout children
    className,
  )}
  {...props}
  ref={ref}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
DialogContent
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
  <DialogOverlay>
    <DialogPrimitive.Content
      ref={ref}
      // shadcn / Dialog Overflow Behavior - https://github.com/shadcn/ui/issues/16
      // radix-ui / Scrollable overlay - https://www.radix-ui.com/docs/primitives/components/dialog#scrollable-overlay
      // radix-ui / dialog src - https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/src/Dialog.tsx
      className={cn(
        "z-50", // overlay
        "relative",
        "min-width-[300px]",
        "grid gap-4 ", // container
        "bg-white dark:bg-slate-900", // bg
        "p-6 rounded-b-lg sm:rounded-lg", // border
        "w-full sm:max-w-screen-sm md:max-w-screen-md", // width
        // "overflow-y-scroll max-h-screen", // prevent overflow / add scrolling -> produces scrollbar even if not needed
        "animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0", // animations
        className,
      )}
      {...props}
    >
      {children}
      <DialogPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800">
        <X className="h-4 w-4" />
        <span className="sr-only">Close</span>
      </DialogPrimitive.Close>
    </DialogPrimitive.Content>
  </DialogOverlay>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName

adaptive-shield-matrix avatar Jul 07 '23 04:07 adaptive-shield-matrix

I will try to create something that works. Maybe I changes something else that seems unrelated at first. But I think I just followed the guide by radix

max-hans avatar Jul 07 '23 06:07 max-hans

I started from a blank Next13 template and tried to recreate it. To make it overflow nicely I just had to adjust the components inside the dialog.tsx created by shadcn. Your issues might be due to the way the dialog is positioned. It initially used some css-transform to center it on the screen. I couldnt find anything in your code on a first glance. Or you are using the Dialog inside some other element that is positioned relative affecting the way the absolute elements inside the dialog component are positioned?

Should we maybe add the Link to the radix documentation regarding overflow somewhere in the docs?

This is my dialog-component:

"use client";

import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";

import { cn } from "@/lib/utils";

const Dialog = DialogPrimitive.Root;

const DialogTrigger = DialogPrimitive.Trigger;

const DialogPortal = ({
  className,
  ...props
}: DialogPrimitive.DialogPortalProps) => (
  <DialogPrimitive.Portal className={cn(className)} {...props} />
);
DialogPortal.displayName = DialogPrimitive.Portal.displayName;

const DialogOverlay = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Overlay
    ref={ref}
    className={cn(
      "fixed inset-0 z-50 bg-black/30 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 overflow-y-auto py-16 grid place-items-center",
      className
    )}
    {...props}
  />
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

const DialogContent = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <DialogPortal>
    <DialogOverlay>
      <DialogPrimitive.Content
        ref={ref}
        className={cn(
          "z-50 grid w-full max-w-lg gap-4 bg-background p-6 shadow-lg duration-200 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 sm:rounded-lg md:w-full relative",
          className
        )}
        {...props}
      >
        {children}
        <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
          <X className="h-4 w-4" />
          <span className="sr-only">Close</span>
        </DialogPrimitive.Close>
      </DialogPrimitive.Content>
    </DialogOverlay>
  </DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;

const DialogHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-1.5 text-center sm:text-left",
      className
    )}
    {...props}
  />
);
DialogHeader.displayName = "DialogHeader";

const DialogFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className
    )}
    {...props}
  />
);
DialogFooter.displayName = "DialogFooter";

const DialogTitle = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Title
    ref={ref}
    className={cn(
      "text-lg font-semibold leading-none tracking-tight",
      className
    )}
    {...props}
  />
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;

const DialogDescription = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;

export {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogFooter,
  DialogTitle,
  DialogDescription,
};

intuity-hans avatar Jul 07 '23 07:07 intuity-hans

thanks @intuity-hans I can confirm your full version is working

Comparing mine and your version, I think you forgot to mention the changes in DialogPortal

DialogPortal
const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogPortalProps) => (
  <DialogPrimitive.Portal className={cn(className)} {...props}>
    <div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">{children}</div>
  </DialogPrimitive.Portal>
)

adaptive-shield-matrix avatar Jul 14 '23 07:07 adaptive-shield-matrix

im have same issue

brandalx avatar Sep 25 '23 20:09 brandalx

i had same error so i tried to solve my way and this actually work i did this by removing the style of pointer events like so:

` onClose: () => { set({ type: null, isOpen: false });

// Delay restoring pointer events for 2 seconds
setTimeout(() => {
  document.body.style.pointerEvents = "auto";
}, 1000);

}, }));`

Note, i did it with small timeout to make sure that first all modal scripts are completed and then im manually removing that. Hope it helps!

brandalx avatar Sep 25 '23 22:09 brandalx

Is this still an issue? Does @intuity-hans solution fix this?

shadcn avatar Oct 21 '23 14:10 shadcn

I can confirm, that

  1. the current dialog component from the CLI is not scrollable
  2. @intuity-hans version fixed the issue for me

I however also found that the @intuity-hans dialog always leaves some space to the top border of the screen + it loses the fullscreen snapping functionality in general.

wotschofsky avatar Oct 22 '23 09:10 wotschofsky

I can look deeper into this and create a PR

max-hans avatar Oct 28 '23 15:10 max-hans

Workaround: I fixed this problem for myself by adding overflow-y-scroll max-h-screen classes to DialogContent

example usage:

<Dialog>
  <DialogTrigger asChild>
    <Button variant={"ghost"} title={"Text""} />
  </DialogTrigger>
  <DialogContent className={"lg:max-w-screen-lg overflow-y-scroll max-h-screen"}>
   todo...
  </DialogContent>
</Dialog>

This is a really nice workaround because it is not so much invasive, liked this approach!

DeveloperMatheus avatar Oct 30 '23 22:10 DeveloperMatheus

overflow-y-scroll

To complement this solution, you may want to hide the vertical scrollbars without compromising on the scroll functionality. To do so, declare the following two CSS classes and use them as an additional class.

`/* For Webkit-based browsers (Chrome, Safari and Opera) */ .scrollbar-hide::-webkit-scrollbar { display: none; }

/* For IE, Edge and Firefox / .scrollbar-hide { -ms-overflow-style: none; / IE and Edge / scrollbar-width: none; / Firefox */ }`

tomatoandbasil avatar Nov 15 '23 17:11 tomatoandbasil

I'm having scroll issue with flexbox inside DialogContent

For example:

<DialogContent className='h-full'>
  <div className='h-full flex flex-col'>
    <div>Title</div>
    <div className='min-h-0 flex-1 overflow-y-auto'>
      <div className='h-[1000px]'>Long long long content</div>
    </div>
  </div>
</DialogContent>

The long content should be scollable when the screen is smaller than 1000px - Title height but it's not.

The most simple solution is making DialogContent block instead of grid

<DialogContent className='block h-full'>
  <div className='w-full h-full flex flex-col gap-4'>
    <div>Title</div>
    <div className='min-h-0 flex-1 overflow-y-auto'>
      <div className='h-[1000px]'>Long long long content</div>
    </div>
  </div>
</DialogContent>

Are there any specify reason why the DialogContent is grid?

since it's content only have 1 element {children} and also have no grid related styling.

tinncdev avatar Nov 24 '23 05:11 tinncdev

Managed to solve this based on the solution by @intuity-hans. The small detail that made the difference was to render DialogContent inside DialogOverlay, instead of rendering them side-by-side as it is currently described in the docs:

const DialogContent = (...) => (
    <DialogPortal>
        <DialogOverlay />
        <DialogPrimitive.Content ... >
         ...
        </DialogPrimitive.Content>
    </DialogPortal>
)

so I changed to this:

const DialogContent = (...) => (
  <DialogPortal>
      <DialogOverlay>
          <DialogPrimitive.Content ... >
               ...
          </DialogPrimitive.Content>
      </DialogOverlay>
  </DialogPortal>
)

hanshs avatar Nov 24 '23 11:11 hanshs

hmmm. this is still an issue for me. I need to make the height of the dialog fit content and also have a max height. i reckon this is a common need.

Jared-Dahlke avatar Dec 01 '23 15:12 Jared-Dahlke

I found it quite frustrating that the footer and header are not fixed while the content is scrollable. For everyone that wants to achieve something similar as on my attached video

https://github.com/shadcn-ui/ui/assets/7522096/395352f9-5f4f-446b-aa10-d302cd4c9e88

Switch grid for flex flex-col in the DialogPrimitive.Content classnames

 <DialogPrimitive.Content
        ref={ref}
        className={cn(
          "fixed left-[50%] top-[50%] z-50 flex w-full max-w-lg translate-x-[-50%] translate-y-[-50%] flex-col gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
          className,
        )}
        {...props}
      >

Example Dialog


<Dialog>
  <DialogTrigger asChild>
        Show Dialog
    </DialogTrigger>
    // adding max-h-[98%] so the modal is not sticking to the borders, but stays within the screen height
    <DialogContent className="max-h-[98%] overflow-hidden px-0">
      <DialogHeader className="px-6">
        <DialogTitle>Filters</DialogTitle>
      </DialogHeader>
      // making the content to take all height available with flex-1 and scrollable
      <div className="flex-1 overflow-y-auto px-6 py-6">
       // your content here
      </div>
      <DialogFooter className="px-6">
        <Button variant="secondary">Reset all filters</Button>
        <Button>Apply filters</Button>
      </DialogFooter>
    </DialogContent>
</Dialog>

Enjoy!

jeromevvb avatar Dec 21 '23 08:12 jeromevvb

Use shadcn ScrollArea component.

<Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="sm:max-w-lg p-0">
        <ScrollArea className="max-h-[80vh] p-6">
          <DialogHeader>
            <DialogTitle>Title</DialogTitle>
          </DialogHeader>
          <div className="space-y-4 mx-1">
          </div>

          <DialogFooter>
          </DialogFooter>
        </ScrollArea>
      </DialogContent>
    </Dialog>

murtaza-ahmad-khan avatar Jan 01 '24 19:01 murtaza-ahmad-khan

I found it quite frustrating that the footer and header are not fixed while the content is scrollable. For everyone that wants to achieve something similar as on my attached video

Screen.Recording.2023-12-21.at.3.14.39.PM.mov Switch grid for flex flex-col in the DialogPrimitive.Content classnames

 <DialogPrimitive.Content
        ref={ref}
        className={cn(
          "fixed left-[50%] top-[50%] z-50 flex w-full max-w-lg translate-x-[-50%] translate-y-[-50%] flex-col gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
          className,
        )}
        {...props}
      >

Example Dialog

<Dialog>
  <DialogTrigger asChild>
        Show Dialog
    </DialogTrigger>
    // adding max-h-[98%] so the modal is not sticking to the borders, but stays within the screen height
    <DialogContent className="max-h-[98%] overflow-hidden px-0">
      <DialogHeader className="px-6">
        <DialogTitle>Filters</DialogTitle>
      </DialogHeader>
      // making the content to take all height available with flex-1 and scrollable
      <div className="flex-1 overflow-y-auto px-6 py-6">
       // your content here
      </div>
      <DialogFooter className="px-6">
        <Button variant="secondary">Reset all filters</Button>
        <Button>Apply filters</Button>
      </DialogFooter>
    </DialogContent>
</Dialog>

Enjoy!

This is ok, but how can i use the styles for the scroll bar that are used in the ScollArea component?

JuanDa237 avatar Feb 03 '24 16:02 JuanDa237

Screenshot 2024-02-11 at 2 20 40 PM

does anyone know why I get this weird bar at the right side of Dialog? and what is the solution for it?

jaeyunha avatar Feb 11 '24 22:02 jaeyunha

Screenshot 2024-02-11 at 2 20 40 PM does anyone know why I get this weird bar at the right side of Dialog? and what is the solution for it?

You can hide it like this: https://github.com/shadcn-ui/ui/issues/16#issuecomment-1812929398

Hope it helps :)

tomatoandbasil avatar Feb 12 '24 01:02 tomatoandbasil

Screenshot 2024-02-11 at 2 20 40 PM does anyone know why I get this weird bar at the right side of Dialog? and what is the solution for it?

You can hide it like this: #16 (comment)

Hope it helps :)

I see, that completely hides the scroll bar even while scrolling, what if we want to show the bar only while scrolling?

jaeyunha avatar Feb 12 '24 19:02 jaeyunha

Screenshot 2024-02-11 at 2 20 40 PM does anyone know why I get this weird bar at the right side of Dialog? and what is the solution for it?

using overflow-auto will fix and only show the scrollbar if necessary.

andfk avatar Feb 18 '24 06:02 andfk

I had a similar problem with the sheet component and too much content, but the overflow-y-scroll max-h-screen fix worked there too. Thanks a lot!

hello, which component exactly did you put this code? I've been trying but doesn't work for me. Cheers!

markpedong avatar Apr 22 '24 13:04 markpedong

I had a similar problem with the sheet component and too much content, but the overflow-y-scroll max-h-screen fix worked there too. Thanks a lot!

hello, which component exactly did you put this code? I've been trying but doesn't work for me. Cheers!

Hello, Were you able to get this to work?

Psarmmiey avatar Apr 26 '24 09:04 Psarmmiey

// sheet.tsx

   <SheetPrimitive.Content
      ref={ref}
      className={cn(sheetVariants({ side }), className)}
      // automatically scrolling up bug fixed
      onCloseAutoFocus={event => event.preventDefault()}
      {...props}

Go to sheet component and add the onCloseAutoFocus prop and the code I sent.

#this code is not my own, I also just found this #somewhere.

markpedong avatar Apr 26 '24 10:04 markpedong

This is the solution to make the entire overlay scrollable without errors, just change the Overlay and the Content as below.

const DialogOverlay = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Overlay
    ref={ref}
    className={cn(
      "fixed inset-0 z-50 bg-black/80 grid place-items-center overflow-y-auto data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
      className
    )}
    {...props}
  />
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

const DialogContent = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <DialogPortal>
    <DialogOverlay className="p-16">
      <DialogPrimitive.Content
        ref={ref}
        className={cn(
          "relative border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 sm:rounded-lg",
          className
        )}
        {...props}
      >
        {children}
        <DialogPrimitive.Close asChild>
          <Button
            size="icon"
            variant="ghost"
            className="absolute top-3 right-3"
          >
            <X className="h-5 w-5" />
            <span className="sr-only">Close</span>
          </Button>
        </DialogPrimitive.Close>
      </DialogPrimitive.Content>
    </DialogOverlay>
  </DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;

askides avatar May 06 '24 16:05 askides