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

React + Tailwind CSS:移动端侧边栏关闭过渡效果实现求助

侧边栏关闭时无法触发过渡效果的解决方案

我已实现点击汉堡图标时侧边栏带过渡效果显示,但点击关闭按钮时无法触发过渡效果。尝试过条件渲染操作但无效,以下是最新代码实现,恳请提供实现过渡效果的建议。

效果演示
侧边栏过渡效果演示

现有代码

Home.jsx

/*
  notes:

  date created: 15/03/2023
*/

import React, { useState, useRef, useEffect } from 'react';
import { Sidebar, UserProfile } from '../components';
import Pins from './Pins';
import { client } from '../client';
import myLogo from '../assets/logoCropped.png';
import { userQuery, fetchUserFromLocalStorage } from '../utils/data';
import { HiMenu } from 'react-icons/hi';
import { AiFillCloseCircle } from 'react-icons/ai';
import { Link, Route, Routes } from 'react-router-dom';

// TODO: added this along the way. continue doing this until it works
const hiddenSidebar = 'fixed w-1/2 bg-black h-screen overflow-y-auto shadow-md z-10 animate-slide-in duration-75';
const activeSidebar = 'bg-red-500 z-10 transition transition-all ';

const Home = () => {

  const [toggleSideBar, setToggleSideBar] = useState(false);
  const [user, setUser] = useState(null);
  const scrollRef = useRef(null);

  const userInfo = fetchUserFromLocalStorage();

  // set up scroll to be at the top of the sidebar
  useEffect(() => {
    scrollRef.current.scrollTo(0, 0)
  }, [])

  // retrieve data using a query from data.js
  useEffect(() => {
    const query = userQuery(userInfo?.sub)

    client.fetch(query)
      .then((data) => {
        setUser(data[0]);
      })
  })

  return (
    <div className='flex bg-gray-50 md:flex-row flex-col h-screen transition-height duration-75 ease-out'>

      {/* desktop sidebar */}
      <div className='hidden md:flex h-screen flex-initial'>
        <Sidebar user={user && user} />
      </div>

      {/* mobile sidebar */}
      <div className='flex md:hidden flex-row'>
        <div className='p-2 w-full flex flex-row justify-between items-center shadow-md'>
          <HiMenu fontSize={40} className="cursor-pointer" onClick={() => setToggleSideBar(true)} />
          <Link to='/'>
            <img src={myLogo} alt="logo" className='w-16' />
          </Link>
          <Link to={`user-profile/${user?._id}`}>
            <img src={user?.image} alt="logo" className='w-16' />
          </Link>
        </div>

        {
          toggleSideBar && (
            <div className={toggleSideBar ? hiddenSidebar : activeSidebar}>
              <div className='absolute w-full flex justify-end items-center p-2 '>
                <AiFillCloseCircle fontSize={30} className="cursor-pointer" onClick={() => 
                {
                  setToggleSideBar(false)
                }
                  } />
              </div>
              <Sidebar user={user && user} closeToggle={setToggleSideBar} />
            </div>
          )} 
      </div>

      {/* routes */}
      <div className='pb-2 flex-1 h-screen overflow-y-scroll' ref={scrollRef}>
        <Routes>
          <Route path="user-profile/:userId" element={<UserProfile />} />
          <Route path='/*' element={<Pins user={user && user} />} />
        </Routes>
      </div>

    </div>
  )
}

export default Home

tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
  darkMode: false, // or media or class

  content: [],
  theme: {
    extend: {
      margin: {
        320: '320px',
      },
      width: {
        190: '190px',
        275: '275px',
        300: '300px',
        340: '340px',
        350: '350px',
        656: '656px',
        880: '880px',
        508: '508px',
      },
      height: {
        80: '80px',
        340: '340px',
        370: '370px',
        420: '420px',
        510: '510px',
        600: '600px',
        685: '685px',
        800: '800px',
        '90vh': '90vh',
      },
      flex: {
        0.7: '0.7 1 0%',
      },
      maxHeight: {
        370: '370px',
      },

      // further tweeks
      minWidth: {
        210: '210px',
        350: '350px',
        620: '620px',
      },

      // change this for personalization
      textColor: {
        lightGray: '#F1EFEE',
        primary: '#FAFAFA',
        secColor: '#efefef',
        navColor: '#BEBEBE',
      },

      // change this for personalization
      backgroundColor: {
        mainColor: '#FBF8F9',
        secondaryColor: '#F0F0F0',
        blackOverlay: 'rgba(0, 0 ,0 ,0.7)',
      },
      /*
        TODO: create slide-out keyframne and animation config for tailwindcss
        basically a reverse of slide in
      */ 
      keyframes: {
        'slide-in': {
          '0%': {
            '-webkit-transform': 'translateX(-200px)',
            transform: 'translateX(-200px)',
          },
          '100%': {
            '-webkit-transform': 'translateX(0px)',
            transform: 'translateX(0px)',
          },
        },
        
        // TODO: added this out
        'slide-out': {
          '0%': {
            '-webkit-transform': 'translateX(0px)',
            transform: 'translateX(0px)',
          },
          '100%': {
            '-webkit-transform': 'translateX(-200px)',
            transform: 'translateX(-200px)',
          },
        },

        'slide-fwd': {
          '0%': {
            '-webkit-transform': 'translateZ(0px)',
            transform: 'translateZ(0px)',
          },
          '100%': {
            '-webkit-transform': 'translateZ(160px)',
            transform: 'translateZ(160px)',
          },
        },

        
      },
      animation: {
        'slide-in': 'slide-in 0.5s ease-out',
        'slide-fwd': 'slide-fwd 0.45s cubic-bezier(0.250, 0.460, 0.450, 0.940) both',

        // TODO: added this
        'slide-out' : 'slide-out 0.5s ease-out',
      },
      transitionProperty: {
        height: 'height',
      },
    },
    cursor: {
      'zoom-in': 'zoom-in',
      pointer: "pointer",
    },
  },
  variants: {
    extend: {
      
    }
  },


  plugins: [],
};

问题原因及解决方案

核心问题

当前代码通过toggleSideBar && (...)直接卸载侧边栏组件,关闭时组件被立即移除,没有时间执行过渡动画;同时类名逻辑错误,导致关闭动画无法触发。

修改步骤

  1. 添加关闭状态跟踪
    新增状态标记侧边栏是否正在执行关闭动画,避免组件被立即卸载:
const [isClosing, setIsClosing] = useState(false);
  1. 调整关闭逻辑
    点击关闭按钮时先触发关闭动画,动画结束后再隐藏组件:
const handleCloseSidebar = () => {
  setIsClosing(true);
};

const handleAnimationEnd = () => {
  setIsClosing(false);
  setToggleSideBar(false);
};
  1. 修正类名与动画绑定
    根据状态动态切换动画类,确保显示/关闭时分别触发对应的动画:
{toggleSideBar && (
  <div 
    className={`fixed w-1/2 bg-black h-screen overflow-y-auto shadow-md z-10 ${
      isClosing ? 'animate-slide-out' : 'animate-slide-in'
    }`}
    onAnimationEnd={handleAnimationEnd}
  >
    <div className='absolute w-full flex justify-end items-center p-2 '>
      <AiFillCloseCircle fontSize={30} className="cursor-pointer" onClick={handleCloseSidebar} />
    </div>
    <Sidebar user={user && user} closeToggle={handleCloseSidebar} />
  </div>
)}
  1. 移除无效类定义
    删除原来的hiddenSidebar和activeSidebar变量,避免混淆。

原理说明

通过isClosing状态延迟组件卸载,让关闭动画完整执行;利用onAnimationEnd事件监听动画结束,再正式隐藏组件,确保过渡效果正常触发。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 18:36:59