Backdrop Filter Style Being Applied After Animation

1.2k Views Asked by At

To further understand React I have created a project using react-router-dom, styled-components and framer-motion.

My versions are like so.

package-json:

    ....
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-icons": "^4.7.1",
    "react-router-dom": "^6.8.1",
    "react-scripts": "5.0.1",
    "styled-components": "^5.3.6",
    ....

My modal has a simple animation with opacity and the container has backdrop-filter: blur(10px) applied to it. I want the blur effect to be applied as soon as the fading in of the modal starts. As it is now, the animation is carried out and only after is the backdrop-filter applied. I find this strange, as all other styles, such as background-color, are applied from the moment the animation starts.

My component Modal.js:

import { IntroModal, ModalContainer } from './Modal.styled';
import { useEffect, useRef } from 'react';
import { motion } from 'framer-motion';
import { AnimatePresence } from 'framer-motion';
import { IoMdClose } from 'react-icons/io';

const Modal = ({ isOpen, toggleModal, closeOnOutsideClick, children }) => {
  const modalRef = useRef(null);

  useEffect(() => {
    const handleClickOutside = (event) => {
      if (
        closeOnOutsideClick &&
        modalRef.current &&
        !modalRef.current.contains(event.target)
      ) {
        toggleModal();
      }
    };

    document.addEventListener('mousedown', handleClickOutside);
    return () => {

      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [modalRef, closeOnOutsideClick, toggleModal]);
  const closeButtonStyle = {
    color: 'white',
    fontSize: '1.5rem',
    cursor: 'pointer',
    filter: 'drop-shadow(0px 0px 2px #000)',
  };
  !isOpen
    ? (document.body.style.overflow = 'hidden')
    : (document.body.style.overflow = 'auto');
  return (
    <AnimatePresence>
      !isOpen && (
      <motion.div
        style={isOpen ? { display: 'none' } : null}
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
        key={isOpen}
      >
        <IntroModal>
          <ModalContainer ref={modalRef}>
            <button onClick={toggleModal}>
              <IoMdClose style={closeButtonStyle} />
            </button>
            {children}
          </ModalContainer>
        </IntroModal>
      </motion.div>
      )
    </AnimatePresence>
  );
};
export default Modal;

Modal.styled.js:

import styled from 'styled-components';

export const IntroModal = styled.div`
  position: fixed;
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 11;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.7);

    button {
      position: absolute;
      top: 0.3rem;
      right: 0.3rem;
      padding: 0.5rem;
      border: none;
      background: transparent;
      transition: transform 0.2s ease-in-out;
      &:hover {
        transform: scale(1.1);
      }
    }

    img {
      width: 50%;
      box-shadow: 0 0 15px #000;
      margin-top: 1rem;
    }

    p {
      font-weight: bold;
      font-size: 1rem;
      padding: 0 1rem;
    }
`;

export const ModalContainer = styled.div`
  backdrop-filter: blur(10px);
  position: absolute;
  top: 50%;
  width: 30%;
  background-color: rgba(38, 50, 50, 0.5);
  text-align: center;
  transform: translateY(-50%);
  padding: 1rem 0.2rem 0.5rem;
  border-top: 0.7px solid #b6b6b666;
  border-right: 0.7px solid #8e8e8e66;
  border-bottom: 0.7px solid #000;
  border-left: 0.7px solid #77777766;
  border-radius: 18px;
  box-shadow: 0 0 1rem 0 #000;
  min-height: 200px;
  display: inherit;
  flex-wrap: wrap;
  flex-direction: row;
  justify-content: center;
  align-items: center;

  @media screen and (max-width: 768px) {
    width: 80vw;
    height: 80vh;
  }
`;

export const ModalWrapper = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 50%;
  height: 25%;
  font-size: 2rem;
`;

Here is a gif of the appearance of it now:

Clicking on modal

I have tried to apply the styles directly to the component, eg style={backdropFilter: blur(10px), and also including it in the animation like so (tried both in outer motion.div and ModalContainer separately):

      <motion.div
        style={isOpen ? { display: 'none' } : null}
        initial={{ opacity: 0, backdropFilter: 'blur(0px)' }}
        animate={{ opacity: 1, backdropFilter: 'blur(10px)' }}
        exit={{ opacity: 0, backdropFilter: 'blur(0px)' }}
        key={isOpen}
      >
        <IntroModal>
          <motion.ModalContainer ref={modalRef}
              initial={{ opacity: 0, backdropFilter: 'blur(0px)' }}
              animate={{ opacity: 1, backdropFilter: 'blur(10px)' }}
              exit={{ opacity: 0, backdropFilter: 'blur(0px)' }}
              key={isOpen}
          >
            <button onClick={toggleModal}>
              <IoMdClose style={closeButtonStyle} />
            </button>
            {children}
          </motion.ModalContainer>
        </IntroModal>
      </motion.div>

Any tips and advice much appreciated!

4

There are 4 best solutions below

1
Pavel Savva On

I don't believe you can animate backdrop-filter: blur, so what you are only seeing two states of it - blur(0) and blur(10px) instead of all the intermediate animation steps, so it looks like it is applied after the other transitions complete.

This answer suggests animating from backdrop-filter: blur(10px) opacity(0); to backdrop-filter: blur(4px) opacity(1); since backdrop-filter: opacity can be animated.

2
CuteKims On

Not sure the problem comes from CSS or Framer-Motion. But if the problem blames to the Framer-Motion, I guess you can use React-Transition-Group to implement this animation and maybe the problem would be solved.

0
CuteKims On

After some research on my own, I found that any opacity != 1 could cause blur effect disappear, it blames to CSS. I guess the workaround is change the opacity in other ways, like a mask or something.

0
Alexander BMAS On

With Framer Motion, if you use AnimatePresence to "hide" the component, which already has backdrop filter applied. You can avoid the POP in caused by opacity. The downside being you remove the node from the dom. But depending on your use case this might be suitable.

export const MastheadMenuCard = ({
  children,
  className
}: {
  children: ReactNode
  className?: string
}) => {
  const { navOpen, setNavOpen } = useAppContext()

  const item = {
    hidden: { y: 4 },
    visible: { y: 0 }
  }

  return (
    <AnimatePresence>
      {navOpen && (
        <motion.div
          animate={navOpen ? 'visible' : 'hidden'}
          className={cn('pointer-events-auto rounded-lg p-8 px-12 frosted-glass', className)}
          exit={{ opacity: 0 }}
          initial="hidden"
          transition={{ opacity: { duration: 0.4 }, y: { duration: 0.1 } }}
          variants={item}
        >
          {children}
        </motion.div>
      )}
    </AnimatePresence>
  )
}