javascriptcssreactjstypescriptcss-transitions

css transition property does not work when setting state using mousenter event


I want to show an animated text next to the button upon hovering the button. This works fine when I am using click event on button but doesn't work when using mouseenter and mouseleave events.

I'm animating using CSS transition -

#text-container {
  transition: max-width 3s ease;
  overflow-x: hidden;
}

App.tsx file -

import { useEffect, useState } from 'react';
import './App.css';

const TextComponent = () => {
  const [maxWidth, setMaxWidth] = useState('0vw');

  useEffect(() => {
    setMaxWidth('100vw');
  }, []);

  return (
    <div id="text-container" style={{ maxWidth, paddingLeft: '8px' }}>
      <div style={{ textWrap: 'nowrap' }}>Sample text??</div>
    </div>
  );
};

function App() {
  const [show, setShow] = useState(false);

  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'flex-start',
        width: '300px',
      }}
    >
      <button
        onClick={() => setShow((prevShow) => !prevShow)}
        // onMouseEnter={() => setShow(true)}
        // onMouseLeave={() => setShow(false)}
      >
        Show Text
      </button>

      {show ? <TextComponent /> : null}
    </div>
  );
}

export default App;

StackBlitz - link


Solution

  • https://stackblitz.com/edit/vitejs-vite-osndza?file=src%2FApp.tsx When using these events how React handles component re-renders with the mouseenter and mouseleave events, React adds/removes the TextComponent, causing the CSS transition to restart each time.

    I have checked in StackBlitz working fine

    CSS (App.css)

    #text-container {
      max-width: 0;
      transition: max-width 0.5s ease;
      overflow-x: hidden;
      white-space: nowrap;
      padding-left: 8px;
    }
    
    #text-container.show {
      max-width: 100vw;
      transition: max-width 3s ease;
    }
    

    App.tsx

    import { useState } from 'react';
    import './App.css';
    
    const TextComponent = ({ show }) => (
      <div id="text-container" className={show ? 'show' : ''}>
        <div>Sample text??</div>
      </div>
    );
    
    function App() {
      const [show, setShow] = useState(false);
    
      return (
        <div
          style={{
            display: 'flex',
            flexDirection: 'row',
            alignItems: 'center',
            justifyContent: 'flex-start',
            width: '300px',
          }}
        >
          <button
            onMouseEnter={() => setShow(true)}
            onMouseLeave={() => setShow(false)}
          >
            Show Text
          </button>
          <TextComponent show={show} />
        </div>
      );
    }
    
    export default App;
    

    Explanation:

    Solution 2: To avoid always mounting TextComponent, you can use CSS transitions with inline styles but conditionally render the component only when the transition ends.

    CSS (App.css)

    #text-container {
      max-width: 0;
      transition: max-width 3s ease;
      overflow-x: hidden;
      white-space: nowrap;
      padding-left: 8px;
    }
    
    #text-container.show {
      max-width: 100vw;
    }
    

    App.tsx

    import { useEffect, useState } from 'react';
    import './App.css';
    
    const TextComponent = ({ show, onTransitionEnd }) => (
      <div
        id="text-container"
        className={show ? 'show' : ''}
        onTransitionEnd={onTransitionEnd}
      >
        <div>Sample text??</div>
      </div>
    );
    
    function App() {
      const [show, setShow] = useState(false);
      const [isMounted, setIsMounted] = useState(false);
    
      useEffect(() => {
        if (show) setIsMounted(true);
      }, [show]);
    
      const handleTransitionEnd = () => {
        if (!show) setIsMounted(false);
      };
    
      return (
        <div
          style={{
            display: 'flex',
            flexDirection: 'row',
            alignItems: 'center',
            justifyContent: 'flex-start',
            width: '300px',
          }}
        >
          <button
            onMouseEnter={() => setShow(true)}
            onMouseLeave={() => setShow(false)}
          >
            Show Text
          </button>
          {isMounted && (
            <TextComponent show={show} onTransitionEnd={handleTransitionEnd} />
          )}
        </div>
      );
    }
    
    export default App;
    

    Explanation

    1. isMounted controls whether TextComponent is in the DOM.
    2. show triggers the transition, and onTransitionEnd removes TextComponent from the DOM only after the exit transition completes.