使用mouseenter事件切换状态时CSS transition属性失效问题
鼠标悬停按钮时文本过渡动画失效的解决方案
问题场景
想实现鼠标悬停按钮时,按钮旁显示带过渡动画的文本。使用按钮的click事件时动画正常,但换成mouseenter和mouseleave事件后,CSS transition动画完全不生效。
用到的CSS过渡代码:
#text-container { transition: max-width 3s ease; overflow-x: hidden; }
对应的React代码:
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;
问题原因
核心问题在于组件挂载时机和浏览器样式优化:
- 触发
mouseenter时,TextComponent才开始挂载,组件渲染时初始maxWidth: 0vw的状态刚被设置,useEffect就立刻把它改成100vw。 - 浏览器会合并这两次连续的样式修改,直接渲染最终的
100vw状态,不会触发过渡动画。 - 而
click事件第一次点击时逻辑和hover一样,但第二次点击是切换状态(组件从显示到隐藏再到显示),hover则是每次进入都重新挂载组件,没有给浏览器留下捕捉初始样式的时间。
解决方案
方案1:始终渲染组件,用状态控制样式(推荐)
不要通过条件渲染销毁/挂载组件,而是让组件一直存在,直接用show状态控制maxWidth的值,这样状态变化时浏览器能捕捉到样式的前后变化,触发过渡:
修改后的App.tsx:
import { useState } from 'react'; import './App.css'; 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> <div id="text-container" style={{ maxWidth: show ? '100vw' : '0vw', paddingLeft: '8px', overflowX: 'hidden', transition: 'max-width 3s ease' }} > <div style={{ textWrap: 'nowrap' }}>Sample text??</div> </div> </div> ); } export default App;
方案2:延迟修改样式,让浏览器捕捉初始状态
如果一定要保留组件挂载的逻辑,可以在useEffect中加一个微任务延迟,让浏览器先渲染初始的0vw状态,再修改为100vw:
修改TextComponent:
const TextComponent = () => { const [maxWidth, setMaxWidth] = useState('0vw'); useEffect(() => { // 用setTimeout让浏览器先完成初始渲染 const timer = setTimeout(() => { setMaxWidth('100vw'); }, 0); // 组件卸载时清除定时器 return () => clearTimeout(timer); }, []); return ( <div id="text-container" style={{ maxWidth, paddingLeft: '8px' }}> <div style={{ textWrap: 'nowrap' }}>Sample text??</div> </div> ); };
同时处理鼠标离开时,需要先触发反向动画,再销毁组件:
function App() { const [show, setShow] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const handleMouseEnter = () => { setShow(true); // 延迟触发展开动画 setTimeout(() => setIsExpanded(true), 0); }; const handleMouseLeave = () => { setIsExpanded(false); // 等待动画结束后再隐藏组件 setTimeout(() => setShow(false), 3000); // 时长和transition保持一致 }; return ( <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', width: '300px', }} > <button onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > Show Text </button> {show && ( <div id="text-container" style={{ maxWidth: isExpanded ? '100vw' : '0vw', paddingLeft: '8px', overflowX: 'hidden', transition: 'max-width 3s ease' }} > <div style={{ textWrap: 'nowrap' }}>Sample text??</div> </div> )} </div> ); }
内容的提问来源于stack exchange,提问作者Anmol Kansal
相关产品推荐
相关产品推荐

