Search code examples
cssreactjstailwind-csscss-transitions

position transition does not take effect in tailwind


I have this simple react component:

function DivModal() {
  const [isOpen, setIsOpen] = useState(true)

  return (
    <div>
      <button onClick={() => setIsOpen(prev => !prev)} className={buttonStyle}>Open</button>
      {isOpen && (
        <div className='fixed top-0 left-0 w-full h-full bg-black/50'>
          <div className={`fixed top-0 ${isOpen ? 'left-0' : '-left-full'} flex transition-left duration-1000 ease-in-out`}>
            <ul className='bg-white'>
              <li>first</li>
              <li>second</li>
              <li>third</li>
            </ul>
            <button onClick={() => setIsOpen(prev => !prev)}>close</button>
          </div>
        </div>
      )}
    </div>
  )
}

The component appears and disappears correctly when clicked, but is instant. No transition from left to right when opening and left to right when closing. How to make it?


Solution

  • transition-left is not a Tailwind class with default configuration. Setting a non-zero transition-duration via a duration-* class implies transition-property: all anyway so transition-left is obsolete.

    To have the transition play, you'd need to keep the element in the DOM:

    const { useState } = React;
    const buttonStyle = '';
    
    function DivModal() {
      const [isOpen, setIsOpen] = useState(true)
    
      return (
        <div>
          <button onClick={() => setIsOpen(prev => !prev)} className={buttonStyle}>Open</button>
          <div className='fixed top-0 left-0 w-full h-full bg-black/50'>
            <div className={`fixed top-0 ${isOpen ? 'left-0' : '-left-full'} flex duration-1000 ease-in-out`}>
              <ul className='bg-white'>
                <li>first</li>
                <li>second</li>
                <li>third</li>
              </ul>
              <button onClick={() => setIsOpen(prev => !prev)}>close</button>
            </div>
          </div>
        </div>
      )
    }
    
    ReactDOM.createRoot(document.getElementById('app')).render(<DivModal/>);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js" integrity="sha512-8Q6Y9XnTbOE+JNvjBQwJ2H8S+UV4uA6hiRykhdtIyDYZ2TprdNmWOUaKdGzOhyr4dCyk287OejbPvwl7lrfqrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js" integrity="sha512-MOCpqoRoisCTwJ8vQQiciZv0qcpROCidek3GTFS6KTk2+y7munJIlKCVkFCYY+p3ErYFXCjmFjnfTTRSC1OHWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdn.tailwindcss.com/3.4.1"></script>
    
    <div id="app"></div>

    But then we see that the bg-black/50 overlay still exists. So, we can improve this by hiding this along with the inner drawer, perhaps with a transitional fade too:

    const { useState } = React;
    const buttonStyle = '';
    
    function DivModal() {
      const [isOpen, setIsOpen] = useState(true)
    
      return (
        <div>
          <button onClick={() => setIsOpen(prev => !prev)} className={buttonStyle}>Open</button>
          <div className={`fixed top-0 left-0 w-full h-full bg-black/50 duration-200 ${isOpen ? '' : 'invisible bg-black/0'}`}>
            <div className={`fixed top-0 ${isOpen ? 'left-0' : '-left-full'} flex duration-1000 ease-in-out`}>
              <ul className='bg-white'>
                <li>first</li>
                <li>second</li>
                <li>third</li>
              </ul>
              <button onClick={() => setIsOpen(prev => !prev)}>close</button>
            </div>
          </div>
        </div>
      )
    }
    
    ReactDOM.createRoot(document.getElementById('app')).render(<DivModal/>);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js" integrity="sha512-8Q6Y9XnTbOE+JNvjBQwJ2H8S+UV4uA6hiRykhdtIyDYZ2TprdNmWOUaKdGzOhyr4dCyk287OejbPvwl7lrfqrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js" integrity="sha512-MOCpqoRoisCTwJ8vQQiciZv0qcpROCidek3GTFS6KTk2+y7munJIlKCVkFCYY+p3ErYFXCjmFjnfTTRSC1OHWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdn.tailwindcss.com/3.4.1"></script>
    
    <div id="app"></div>

    You could also consider using transform: translateX() instead of left. This improves the performance of the transition since the browser does not need to reperform layout calculations every animation frame. Applying transform: translateX(-100%) also means the drawer will only slide the distance of its own width, meaning there is no strange delay of waiting for it to slide the width of the viewport. This also means we can "sync" the duration of both the overlay and the drawer itself:

    const { useState } = React;
    const buttonStyle = '';
    
    function DivModal() {
      const [isOpen, setIsOpen] = useState(true)
    
      return (
        <div>
          <button onClick={() => setIsOpen(prev => !prev)} className={buttonStyle}>Open</button>
          <div className={`fixed top-0 left-0 w-full h-full bg-black/50 duration-500 ${isOpen ? '' : 'invisible bg-black/0'}`}>
            <div className={`fixed top-0 ${isOpen ? '' : '-translate-x-full'} flex duration-500 ease-out`}>
              <ul className='bg-white'>
                <li>first</li>
                <li>second</li>
                <li>third</li>
              </ul>
              <button onClick={() => setIsOpen(prev => !prev)}>close</button>
            </div>
          </div>
        </div>
      )
    }
    
    ReactDOM.createRoot(document.getElementById('app')).render(<DivModal/>);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js" integrity="sha512-8Q6Y9XnTbOE+JNvjBQwJ2H8S+UV4uA6hiRykhdtIyDYZ2TprdNmWOUaKdGzOhyr4dCyk287OejbPvwl7lrfqrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js" integrity="sha512-MOCpqoRoisCTwJ8vQQiciZv0qcpROCidek3GTFS6KTk2+y7munJIlKCVkFCYY+p3ErYFXCjmFjnfTTRSC1OHWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdn.tailwindcss.com/3.4.1"></script>
    
    <div id="app"></div>