如何避免按钮交互时CSS Transition动画提前结束?
解决按钮
:active过渡动画提前结束的问题 问题描述
我给样式化的button绑定了:active状态的CSS Transition,原本期望它平滑切换至按压状态,再平滑恢复到正常状态,但过渡动画总是提前结束——哪怕将transition-duration设为2秒,实际时长也短很多,移动端点击时尤其明显。只有触摸板长按(点击时长较长)或移动端长按按钮时,动画才能完整播放。我希望鼠标点击和移动端点击都能完整触发动画,曾尝试将:active状态设置在父元素上,通过父元素控制子元素位移,但未成功。
原按钮代码:
import styled, { css } from 'styled-components'; export const Button = styled.button` --easing: cubic-bezier(0.18, 0.89, 0.32, 1.28); --bg-color: #ddd; --shadow-color: grey; width: 100%; height: 5rem; color: black; outline-offset: 5px; border: none; text-transform: uppercase; display: grid; place-items: center; font-family: inherit; font-weight: 700; touch-action: manipulation; transition: all 0.5s var(--easing); background-color: var(--bg-color); border-radius: 15px; font-size: 20px; line-height: 25px; letter-spacing: 1.25px; box-shadow: 0px 8px 0px var(--shadow-color); &:active { transform: translateY(8px); box-shadow: 0px 0px 0px var(--shadow-color); } `;
问题根源
浏览器的:active状态会在mouseup/touchend事件触发后立即移除,而此时过渡动画还未完成。移动端点击触发的:active状态持续时间极短,因此动画被打断的情况更显著。
解决方案
方法1:JavaScript强制维持按压状态到动画结束
通过监听鼠标/触摸事件,用自定义类模拟按压状态,在动画结束后再移除类,确保过渡完整执行:
import styled, { css } from 'styled-components'; import { useEffect, useRef } from 'react'; const StyledButton = styled.button` --easing: cubic-bezier(0.18, 0.89, 0.32, 1.28); --bg-color: #ddd; --shadow-color: grey; width: 100%; height: 5rem; color: black; outline-offset: 5px; border: none; text-transform: uppercase; display: grid; place-items: center; font-family: inherit; font-weight: 700; touch-action: manipulation; transition: transform 0.5s var(--easing), box-shadow 0.5s var(--easing); background-color: var(--bg-color); border-radius: 15px; font-size: 20px; line-height: 25px; letter-spacing: 1.25px; box-shadow: 0px 8px 0px var(--shadow-color); &.pressed { transform: translateY(8px); box-shadow: 0px 0px 0px var(--shadow-color); } `; export const Button = ({ children }) => { const buttonRef = useRef(null); const transitionDuration = 500; // 与CSS过渡时长对应 useEffect(() => { const button = buttonRef.current; let timeoutId; const handlePressStart = () => { button.classList.add('pressed'); }; const handlePressEnd = () => { timeoutId = setTimeout(() => { button.classList.remove('pressed'); }, transitionDuration); }; button.addEventListener('mousedown', handlePressStart); button.addEventListener('touchstart', handlePressStart, { passive: true }); button.addEventListener('mouseup', handlePressEnd); button.addEventListener('touchend', handlePressEnd); button.addEventListener('mouseleave', handlePressEnd); // 鼠标离开时终止动画 return () => { clearTimeout(timeoutId); button.removeEventListener('mousedown', handlePressStart); button.removeEventListener('touchstart', handlePressStart); button.removeEventListener('mouseup', handlePressEnd); button.removeEventListener('touchend', handlePressEnd); button.removeEventListener('mouseleave', handlePressEnd); }; }, [transitionDuration]); return <StyledButton ref={buttonRef}>{children}</StyledButton>; };
方法2:纯CSS方案(利用:focus-within)
给按钮父容器设置tabindex="0",通过:focus-within状态触发动画,配合:active增强兼容性:
import styled, { css } from 'styled-components'; const ButtonWrapper = styled.div` width: 100%; tab-index: 0; outline: none; &:focus-within button, &:active button { transform: translateY(8px); box-shadow: 0px 0px 0px var(--shadow-color); } `; export const Button = styled.button` --easing: cubic-bezier(0.18, 0.89, 0.32, 1.28); --bg-color: #ddd; --shadow-color: grey; width: 100%; height: 5rem; color: black; outline-offset: 5px; border: none; text-transform: uppercase; display: grid; place-items: center; font-family: inherit; font-weight: 700; touch-action: manipulation; transition: transform 0.5s var(--easing), box-shadow 0.5s var(--easing); background-color: var(--bg-color); border-radius: 15px; font-size: 20px; line-height: 25px; letter-spacing: 1.25px; box-shadow: 0px 8px 0px var(--shadow-color); `;
使用方式:
<ButtonWrapper> <Button>点击测试</Button> </ButtonWrapper>
方法3:优化CSS过渡逻辑
避免使用all过渡(性能差且易冲突),明确指定过渡属性,同时在动画期间禁用指针事件防止重复触发:
import styled, { css } from 'styled-components'; export const Button = styled.button` --easing: cubic-bezier(0.18, 0.89, 0.32, 1.28); --bg-color: #ddd; --shadow-color: grey; width: 100%; height: 5rem; color: black; outline-offset: 5px; border: none; text-transform: uppercase; display: grid; place-items: center; font-family: inherit; font-weight: 700; touch-action: manipulation; transition: transform 0.5s var(--easing), box-shadow 0.5s var(--easing); background-color: var(--bg-color); border-radius: 15px; font-size: 20px; line-height: 25px; letter-spacing: 1.25px; box-shadow: 0px 8px 0px var(--shadow-color); &.pressed { transform: translateY(8px); box-shadow: 0px 0px 0px var(--shadow-color); pointer-events: none; /* 动画期间禁止重复点击 */ } `;
配合方法1中的JavaScript逻辑添加/移除pressed类即可。
内容的提问来源于stack exchange,提问作者TigranK
相关产品推荐
相关产品推荐

