headlessui
headlessui copied to clipboard
[Bug]: Cannot properly center Headless UI Dialog/Modal due to scrollbar padding
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).
useEffect(() => {
if (isModalOpen) {
setTimeout(() => {
document.documentElement.style.paddingRight = '0px';
}, /*transition-duration-time*/ 300);
}
}, [isModalOpen]);
to manually clear out the paddingRight
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!
.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 }
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