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

React useState钩子下CSS关闭动画失效问题及修复咨询

滑动子菜单关闭动画失效问题排查与修复

问题描述

我用React、TypeScript和Tailwind构建了一个滑动子菜单,通过useState控制子菜单显示状态。打开动画data-[state=open]:animate-[submenu-show_300ms]正常工作,但关闭动画data-[state=closed]:animate-[submenu-hide_300ms]完全没效果,尝试了两种实现方式都没能解决:

最初实现(条件渲染卸载组件)

{clickedItem === item && (
        <ul
          data-state={dataSate}
          className="fixed top-0 flex h-svh w-[300px] flex-col bg-red-300 ease-in data-[state=closed]:animate-[submenu-hide_300ms] data-[state=open]:animate-[submenu-show_300ms]"
        >
          <li>
            <button
              onClick={() => setClickedItem(null)}
              type="button"
              className="flex items-center p-2"
            >
              <span className="mr-10">Back</span>
              <span className="font-bold">{item.title}</span>
            </button>
          </li>
          {item.items?.map((item) => (
            <li key={item.url}>
              {item.items && item.items.length > 0 ? (
                <button
                  onClick={() => handleClick(item)}
                  className="flex w-full justify-between p-2"
                  type="button"
                >
                  {item.title}
                  <span>[+]</span>
                </button>
              ) : (
                <a href={item.url} className="p-2">
                  {item.title}
                </a>
              )}
            </li>
          ))}
        </ul>
       )}

改进尝试(CSS控制显示隐藏)

<ul
              data-state={dataSate}
              className={`${clickedItem === item ? 'left-0 animate-[submenu-show_300ms]' : 'left-[-375px] animate-[submenu-hide_300ms] overflow-hidden'} fixed top-0 flex h-svh w-[375px] flex-col bg-red-300 ease-in`}
            >
              <li>
                <button
                  onClick={() => setClickedItem(null)}
                  type="button"
                  className="flex items-center p-2"
                >
                  <span className="mr-10">Back</span>
                  <span className="font-bold">{item.title}</span>
                </button>
              </li>
              {item.items?.map((item) => (
                <li key={item.url}>
                  {item.items && item.items.length > 0 ? (
                    <button
                      onClick={() => handleClick(item)}
                      className="flex w-full justify-between p-2"
                      type="button"
                    >
                      {item.title}
                      <span>[+]</span>
                    </button>
                  ) : (
                    <a href={item.url} className="p-2">
                      {item.title}
                    </a>
                  )}
                </li>
              ))}
            </ul>

问题根源

  1. 第一种实现:关闭时直接通过clickedItem === item的条件将组件从DOM中卸载,动画还未执行完成就被移除,自然看不到关闭效果。
  2. 第二种实现:切换left属性的同时绑定自定义动画类,Tailwind的animate-*关键帧动画与直接修改left的过渡逻辑冲突,且类名切换时没有预留动画执行的时间窗口。

修复方案

方案一:用过渡类替代自定义动画(推荐)

放弃自定义关键帧动画,改用Tailwind内置的过渡类实现滑动效果,同时确保组件在动画完成后再卸载:

import { useState } from 'react';

// 假设MenuItem类型定义
type MenuItem = {
  title: string;
  url?: string;
  items?: MenuItem[];
};

export default function SlideMenu() {
  const [activeItem, setActiveItem] = useState<MenuItem | null>(null);
  const [isMenuVisible, setIsMenuVisible] = useState(false);

  const openSubmenu = (item: MenuItem) => {
    setActiveItem(item);
    setIsMenuVisible(true);
  };

  const closeSubmenu = () => {
    setIsMenuVisible(false);
    // 动画结束后卸载组件
    setTimeout(() => setActiveItem(null), 300);
  };

  // 示例菜单数据
  const menuItems: MenuItem[] = [
    {
      title: "菜单1",
      items: [
        { title: "子项1-1", url: "/sub1-1" },
        { title: "子项1-2", url: "/sub1-2" }
      ]
    },
    {
      title: "菜单2",
      items: [
        { title: "子项2-1", url: "/sub2-1" },
        { title: "子项2-2", items: [{ title: "嵌套子项", url: "/nested" }] }
      ]
    }
  ];

  return (
    <div className="flex">
      {/* 主菜单 */}
      <ul className="w-[300px] bg-gray-100">
        {menuItems.map((item) => (
          <li key={item.title}>
            <button
              onClick={() => openSubmenu(item)}
              className="flex w-full justify-between p-2"
            >
              {item.title}
              <span>[+]</span>
            </button>
          </li>
        ))}
      </ul>

      {/* 滑动子菜单 */}
      {activeItem && (
        <ul
          className={`fixed top-0 left-0 flex h-svh w-[300px] flex-col bg-red-300 transition-all duration-300 ease-in ${isMenuVisible ? 'translate-x-0' : '-translate-x-full'}`}
        >
          <li>
            <button onClick={closeSubmenu} className="flex items-center p-2">
              <span className="mr-10">Back</span>
              <span className="font-bold">{activeItem.title}</span>
            </button>
          </li>
          {activeItem.items?.map((subItem) => (
            <li key={subItem.url || subItem.title}>
              {subItem.items?.length ? (
                <button
                  onClick={() => openSubmenu(subItem)}
                  className="flex w-full justify-between p-2"
                >
                  {subItem.title}
                  <span>[+]</span>
                </button>
              ) : (
                <a href={subItem.url} className="block p-2">
                  {subItem.title}
                </a>
              )}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

方案二:修复自定义关键帧动画

如果坚持使用自定义动画,需要确保关键帧定义正确,且动画完成后再卸载组件:

  1. 配置Tailwind自定义关键帧:
// tailwind.config.js
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {
      animation: {
        'submenu-show': 'submenu-show 300ms ease-in forwards',
        'submenu-hide': 'submenu-hide 300ms ease-in forwards',
      },
      keyframes: {
        'submenu-show': {
          '0%': { transform: 'translateX(-100%)' },
          '100%': { transform: 'translateX(0)' },
        },
        'submenu-hide': {
          '0%': { transform: 'translateX(0)' },
          '100%': { transform: 'translateX(-100%)' },
        },
      },
    },
  },
  plugins: [],
}
  1. 修改子菜单JSX:
{activeItem && (
  <ul
    data-state={isMenuVisible ? 'open' : 'closed'}
    className="fixed top-0 flex h-svh w-[300px] flex-col bg-red-300 data-[state=open]:animate-submenu-show data-[state=closed]:animate-submenu-hide"
    onAnimationEnd={() => {
      if (!isMenuVisible) setActiveItem(null);
    }}
  >
    <li>
      <button onClick={closeSubmenu} className="flex items-center p-2">
        <span className="mr-10">Back</span>
        <span className="font-bold">{activeItem.title}</span>
      </button>
    </li>
    {/* 子项内容 */}
  </ul>
)}

核心修复要点

  • 永远不要在动画执行时直接卸载组件,必须等动画完成后再处理DOM移除。
  • 用状态控制动画触发时机,关闭时先切换动画状态,再延迟更新卸载状态。
  • 自定义关键帧动画要确保forwards属性保留动画最终状态,避免元素跳回初始位置。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 21:05:02