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

如何避免按钮交互时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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 21:50:39