HeadlessUI弹窗关闭后html自动加overflow致页面滚动锁定
问题复现描述
我的页面效果如下图所示:
点击卫衣缩略图通过HeadlessUI唤起弹窗的逻辑运行正常,但关闭后页面滚动被锁定,根html标签被自动添加了如下内联样式:
style="overflow: hidden; padding-right: 16px;"
样式残留效果参考如下截图:
Headless UI 的 Dialog 组件默认内置弹窗打开时的背景滚动锁定逻辑:弹窗激活时会自动给根<html>标签写入overflow: hidden和匹配浏览器滚动条宽度的padding-right值,避免弹窗出现时页面横向抖动;正常关闭弹窗时组件会自动清理这些写入的内联样式。你遇到的关闭弹窗后滚动锁定不解除、样式残留问题,属于组件内置清理逻辑未正常触发,和使用Tailwind CSS还是Bootstrap没有直接关联,两类技术栈下的修复逻辑通用。
方案1:直接禁用组件自带的滚动锁定(推荐)
Dialog 组件原生提供了禁用滚动锁定的属性,加一行配置即可彻底禁止组件修改html标签的滚动相关样式,从根源避免残留问题。
修改代码中Dialog标签的定义,添加disableScrollLock={true}属性:
<Dialog as="div" className="relative z-10" onClose={() => setOpen(false)} disableScrollLock={true} >
如果业务需要保留弹窗打开时的背景滚动锁定能力,自行通过useEffect监听open状态控制document的overflow属性即可,逻辑完全可控,不会出现清理不及时的问题。
方案2:修复组件生命周期触发问题(保留内置滚动锁场景用)
如果需要使用组件自带的滚动锁定能力,优先检查两处配置:
- 确认Recoil中存储弹窗开关状态的
openState,关闭弹窗时能正确同步为false,不存在多组件同时修改状态导致状态不一致的问题 - 给
Transition.Root组件添加unmount={true}属性,确保弹窗离场动画执行完成后完全卸载组件,触发内置的样式清理逻辑:
<Transition.Root show={open} as={Fragment} unmount={true}>
你当前代码中遮罩层添加了hidden md:block类,移动端场景下遮罩层默认隐藏,容易出现Transition离场动画未正常执行、组件提前终止生命周期的问题,加unmount属性可以规避这类场景下的清理失效问题。
方案3:兜底手动清理残留样式
如果上述方案仍未解决问题,可以在关闭弹窗的回调中手动移除根节点的残留内联样式:
const handleModalClose = () => { setOpen(false) // 手动清理根节点滚动锁样式 const rootEl = document.documentElement rootEl.style.removeProperty('overflow') rootEl.style.removeProperty('padding-right') }
将代码中所有触发弹窗关闭的逻辑(Dialog的onClose属性、右上角关闭按钮的onClick事件)替换为调用handleModalClose即可。
附提问者提供的弹窗组件参考代码:
import React from 'react' import { Dialog, Transition } from '@headlessui/react' import { Fragment } from 'react' import {XIcon} from '@heroicons/react/solid' import {useRecoilState} from 'recoil' import {openState} from '../../../atoms/modalAtom' const product = { name: "Women's Basic Tee", price: '$32', rating: 3.9, reviewCount: 512, href: '#', imageSrc: 'https://tailwindui.com/img/ecommerce-images/product-page-01-featured-product-shot.jpg', imageAlt: "Back of women's Basic Tee in black.", colors: [ { name: 'Black', bgColor: 'bg-gray-900', selectedColor: 'ring-gray-900' }, { name: 'Heather Grey', bgColor: 'bg-gray-400', selectedColor: 'ring-gray-400' }, ], sizes: [ { name: 'XXS', inStock: true }, { name: 'XS', inStock: true }, { name: 'S', inStock: true }, { name: 'M', inStock: true }, { name: 'L', inStock: true }, { name: 'XL', inStock: true }, { name: 'XXL', inStock: false }, ], } export function DesignQuickView ({design}) { const [open, setOpen] = useRecoilState(openState) return ( <Transition.Root show={open} as={Fragment}> <Dialog as="div" className="relative z-10" onClose={() => setOpen(false)}> <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="hidden fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity md:block" /> </Transition.Child> <div className="fixed z-10 inset-0 overflow-y-auto"> <div className="flex items-stretch md:items-center justify-center min-h-full text-center md:px-2 lg:px-4"> {/* This element is to trick the browser into centering the modal contents. */} <span className="hidden md:inline-block md:align-middle md:h-screen" aria-hidden="true"> ​ </span> <Transition.Child as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0 translate-y-4 md:translate-y-0 md:scale-95" enterTo="opacity-100 translate-y-0 md:scale-100" leave="ease-in duration-200" leaveFrom="opacity-100 translate-y-0 md:scale-100" leaveTo="opacity-0 translate-y-4 md:translate-y-0 md:scale-95" > <Dialog.Panel className="flex text-base text-left transform transition w-full md:max-w-2xl md:px-4 md:my-8 lg:max-w-4xl"> <div className="w-full relative flex items-center bg-white px-4 pt-14 pb-8 overflow-hidden shadow-2xl sm:px-6 sm:pt-8 md:p-6 lg:p-8"> <button type="button" className="absolute top-4 right-4 text-gray-400 hover:text-gray-500 sm:top-8 sm:right-6 md:top-6 md:right-6 lg:top-8 lg:right-8" onClick={() => setOpen(false)} > <span className="sr-only">Close</span> <XIcon className="h-6 w-6" aria-hidden="true" /> </button> <div className="w-full grid grid-cols-1 gap-y-8 gap-x-6 items-start sm:grid-cols-12 lg:items-center lg:gap-x-8"> <div className="aspect-w-3 aspect-h-4 rounded-lg bg-gray-100 overflow-hidden sm:col-span-4 lg:col-span-5"> <img src={design.url} alt={design.name} className="object-center object-cover" /> </div> <div className="sm:col-span-8 lg:col-span-7"> <h2 className="text-xl font-medium text-gray-900 sm:pr-12">Hoodie</h2> <section aria-labelledby="information-heading" className="mt-1"> <h3 id="information-heading" className="sr-only"> Product information </h3> <p className="font-medium text-gray-900">{product.price}</p> </section> <section aria-labelledby="options-heading" className="mt-8"> <h3 id="options-heading" className="sr-only"> Product options </h3> <form> {/* Color picker */} <div> <h4 className="text-sm font-medium text-gray-900">Color</h4> </div> {/* Size picker */} <div className="mt-8"> <div className="flex items-center justify-between"> <h4 className="text-sm font-medium text-gray-900">Size</h4> <a href="#" className="text-sm font-medium text-indigo-600 hover:text-indigo-500"> Size guide </a> </div> </div> </form> </section> </div> </div> </div> </Dialog.Panel> </Transition.Child> </div> </div> </Dialog> </Transition.Root> ) } export default DesignQuickView
内容的提问来源于stack exchange,提问作者rohit

