Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: Cannot properly center Headless UI Dialog/Modal due to scrollbar padding #839

Closed
domdomegg opened this issue Sep 30, 2021 · 4 comments
Assignees

Comments

@domdomegg
Copy link

What package within Headless UI are you using?

@headlessui/react

What version of that package are you using?

1.4.1

What browser are you using?

Chrome

Reproduction repository

https://github.com/domdomegg/headless-ui-dialog-centering

Describe your issue

When a Dialog/Modal is opened, if the scrollbar was visible at the time of opening additional padding is added to make up for the scrollbar disappearing to avoid the page jumping around.

However, this padding is of variable width (the width of the scrollbar) and it's hard to predict when it appears. That makes centering an element relative to the original page difficult as generally the dialog uses position: fixed (as per the guide in the docs on styling).

@zgwl
Copy link

zgwl commented Apr 17, 2022

  useEffect(() => {
    if (isModalOpen) {
      setTimeout(() => {
        document.documentElement.style.paddingRight = '0px';
      }, /*transition-duration-time*/ 300);
    }
  }, [isModalOpen]);

to manually clear out the paddingRight

@RobinMalfait RobinMalfait self-assigned this May 18, 2022
@RobinMalfait
Copy link
Member

Hey! Thank you for your bug report!
Much appreciated! 🙏

The reason we apply a padding-right is to prevent layout shifts (visual jumps) because the moment we apply overflow: hidden; to prevent scrolling, the scrollbar goes away and the UI will jump. The padding-right will counter-act that, which you can see in your example (the black box will stay in the same spot).

One thing you can do is to use the scrollbar-gutter native CSS feature to make sure that your gutter is stable using scrollbar-gutter: stable;. This will also prevent jumps (and Headless UI will then not apply a padding-right). More info: https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-gutter

If you then switch your fixed with absolute to stay within the html then your Dialog will line up with the background.

Hope this helps!

@mak1986
Copy link

mak1986 commented Apr 21, 2023

.dialog-open {
  transition: padding-right 300ms ease-out;
}
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, ReactNode, useEffect, useRef } from 'react'

type Props = {
    children: ReactNode;
    isOpen: boolean;
    onClose: () => void;
}

function CustomDialog(props: Props) {

    const { children, isOpen, onClose } = props

    const dialog = useRef<HTMLDivElement>(null)
    const cancelButtonRef = useRef<HTMLButtonElement>(null)

    useEffect(() => {
        if (isOpen) {
            setTimeout(() => {
                const root = document.documentElement;
                const paddingRight = window.getComputedStyle(root).paddingRight;
                if (dialog?.current) {
                    dialog.current.classList.add('dialog-open');
                    dialog.current.style.paddingRight = paddingRight;
                }
            }, /*transition-duration-time*/ 0);
        } else {
            if (dialog?.current) {
              dialog.current.classList.remove('dialog-open');
              dialog.current.style.paddingRight = '0';
            }
          }
    }, [isOpen]);

    return (
        <Transition.Root show={isOpen} as={Fragment}>
            <Dialog as="div" className="relative z-10" initialFocus={cancelButtonRef} onClose={onClose}>
                <Transition.Child
                    as={Fragment}
                    enter="ease-out duration-300"
                    enterFrom="opacity-0"
                    enterTo="opacity-100"
                    leave="ease-in duration-200"
                    leaveFrom="opacity-100"
                    leaveTo="opacity-0"
                >
                    <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
                </Transition.Child>

                <div ref={dialog} className="fixed inset-0 z-10 overflow-y-auto">
                    <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
                        <Transition.Child
                            as={Fragment}
                            enter="ease-out duration-300"
                            enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
                            enterTo="opacity-100 translate-y-0 sm:scale-100"
                            leave="ease-in duration-200"
                            leaveFrom="opacity-100 translate-y-0 sm:scale-100"
                            leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
                        >
                            <Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8  w-full max-w-md sm:p-6">
                                {children}
                            </Dialog.Panel>
                        </Transition.Child>
                    </div>
                </div>
            </Dialog>
        </Transition.Root>
    )
}

export { CustomDialog }







@Hyporos
Copy link

Hyporos commented Jul 5, 2023

I fixed it seamlessly (without any movement/shifting) by putting ${isOpen && "mr-[15px]"} in the full-screen container.

<div className={`fixed inset-0 flex items-center justify-center p-4 ${isOpen && "mr-[15px]"}`}>

edit: i realized this depends on the padding of your scrollbar, may be different between browsers but you'll have to experiment

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

No branches or pull requests

5 participants