You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React模态窗口关闭无平滑动画问题的修复方案咨询

React模态框关闭无动画问题修复

问题现象

模态框打开时平滑有过渡动画,但点击关闭或按ESC后直接瞬间消失,完全看不到关闭动画效果。


现有代码

Modal组件代码

const ModalContent = ({ children, closeModal }) => {
  useEffect(() => {
    const listenerHandler = (e) => {
      if (e.key === 'Escape') {
        closeModal()
      }
    }
    document.addEventListener('keydown', listenerHandler)

    return () => {
      document.removeEventListener('keydown', listenerHandler)
    }
  }, [closeModal])

  return children
}

export function Modal({
  showModal, handleClose, children,
}) {
  useEffect(() => {
    showModal
      ? document.body.style.overflow = 'hidden'
      : document.body.style.overflow = 'unset'
  }, [showModal])

  if (!showModal) return null

  const classes = classNames({
    modal: true,
    hide: !showModal,
  })

  return createPortal(
    <div className={classes} onClick={handleClose}>
      <div className="modalContent" onClick={(e) => e.stopPropagation()}>

        <ModalContent closeModal={handleClose}>
          {children}
        </ModalContent>

      </div>
    </div>,

    document.getElementById('modal-root'),
  )
}

CSS样式

.modal {
    width: 100vw;
    height: 100vh;
    position: fixed;
    top: 0;
    left: 0;
    z-index: 99;
    background-color: rgba(0, 0, 0, .9);
    display: flex;
    justify-content: center;
    align-items: center;

    animation-name: fade;
    animation-duration: .4s;
}

.modal.hide {
    animation-name: fade-out;
    animation-duration: .5s;
}

@keyframes fade {
     from {
         opacity: 0.7;
     }
     to {
         opacity: 1;
     }
 }

 @keyframes fade-out {
    from {
        opacity: 1;
    }
    to {
        opacity: 0;
    }
}

使用模态框的组件代码

export function Project({
  projectName, projectDescription, projectLinkToGitHub, arrayOfUsedTools, orientation,
}) {
  const [currentImage, setCurrentImage] = useState(null)
  const [showModal, setShowModal] = useState(false)

  const imageClickHandler = (picture) => {
    setCurrentImage(picture)
    setShowModal(true)
  }

  const handleClose = () => setShowModal(false)

  return (
     ......
     <Swiper
        spaceBetween={30}
        centeredSlides
        autoplay={{
        delay: 2000,
        disableOnInteraction: false,}}
        pagination={{
        clickable: true,}}
        navigation
        modules={[Autoplay, Pagination, Navigation]}
        grabCursor
        loop
        slidesPerView={1}
        className={leftStyles.swiperContainer}
        >
         {dogFoodImages.map((picture, i) => (
            <SwiperSlide
              key={i}
              className={leftStyles.swiperSlide}
              onClick={() => imageClickHandler(picture)}
             >
               <img
                  src={picture}
                  alt={picture}
                  className={leftStyles.screens}
               />
             </SwiperSlide>
     ......
      <Modal showModal={showModal} handleClose={handleClose}>
        <img
          src={currentImage}
          alt=""
          className={rightStyles.screensForModal}
        />
      </Modal>
}

问题根源

核心问题是:当showModal变为false时,Modal组件直接return null,DOM被立即移除,关闭动画根本没机会执行。


修复方案

1. 修改Modal组件逻辑,延迟移除DOM

新增isClosing状态标记关闭过程,等动画执行完再移除DOM:

import { useEffect, useState, useRef } from 'react';
import { createPortal } from 'react-dom';
import classNames from 'classnames';

const ModalContent = ({ children, closeModal }) => {
  useEffect(() => {
    const listenerHandler = (e) => {
      if (e.key === 'Escape') {
        closeModal();
      }
    };
    document.addEventListener('keydown', listenerHandler);

    return () => {
      document.removeEventListener('keydown', listenerHandler);
    };
  }, [closeModal]);

  return children;
};

export function Modal({ showModal, handleClose, children }) {
  const [isClosing, setIsClosing] = useState(false);

  useEffect(() => {
    if (showModal) {
      document.body.style.overflow = 'hidden';
      setIsClosing(false);
    } else {
      setIsClosing(true);
      // 等待关闭动画结束后恢复body滚动
      const timer = setTimeout(() => {
        document.body.style.overflow = 'unset';
      }, 500); // 时长要和fade-out动画的duration一致
      return () => clearTimeout(timer);
    }
  }, [showModal]);

  // 只有当模态框既不显示也不在关闭过程中时,才不渲染DOM
  if (!showModal && !isClosing) return null;

  const classes = classNames({
    modal: true,
    hide: !showModal,
  });

  return createPortal(
    <div
      className={classes}
      onClick={handleClose}
      // 动画结束后重置isClosing,确保下次打开正常
      onAnimationEnd={() => isClosing && setIsClosing(false)}
    >
      <div className="modalContent" onClick={(e) => e.stopPropagation()}>
        <ModalContent closeModal={handleClose}>{children}</ModalContent>
      </div>
    </div>,
    document.getElementById('modal-root')
  );
}

2. 优化CSS动画,确保结束状态生效

添加animation-fill-mode让动画结束后保持最终状态,同时在关闭动画结束后隐藏元素避免点击:

.modal {
    width: 100vw;
    height: 100vh;
    position: fixed;
    top: 0;
    left: 0;
    z-index: 99;
    background-color: rgba(0, 0, 0, .9);
    display: flex;
    justify-content: center;
    align-items: center;
    animation-name: fade;
    animation-duration: .4s;
    animation-fill-mode: forwards; /* 保持动画结束后的状态 */
}

.modal.hide {
    animation-name: fade-out;
    animation-duration: .5s;
    animation-fill-mode: forwards;
}

@keyframes fade {
     from {
         opacity: 0.7;
     }
     to {
         opacity: 1;
     }
 }

 @keyframes fade-out {
    from {
        opacity: 1;
        visibility: visible;
    }
    to {
        opacity: 0;
        visibility: hidden; /* 动画结束后隐藏,防止点击穿透 */
    }
}

关键修复点

  • 用isClosing状态延迟DOM移除,给关闭动画留足执行时间
  • 定时器时长必须和CSS中fade-out的animation-duration一致,保证动画完整执行
  • animation-fill-mode: forwards确保动画结束后保持最终的透明度和可见性
  • 通过onAnimationEnd事件在动画结束后重置状态,不影响下次打开

内容的提问来源于stack exchange,提问作者Ramil

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 02:07:09