headlessui icon indicating copy to clipboard operation
headlessui copied to clipboard

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

Open domdomegg opened this issue 4 years ago • 1 comments

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).

domdomegg avatar Sep 30 '21 09:09 domdomegg

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

to manually clear out the paddingRight

zgwl avatar Apr 17 '22 19:04 zgwl

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!

RobinMalfait avatar Aug 22 '22 13:08 RobinMalfait

.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 }







mak1986 avatar Apr 21 '23 18:04 mak1986

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

Hyporos avatar Jul 05 '23 16:07 Hyporos