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

ReactJS中点击事件触发含SVG内容的伪元素旋转实现方法

FAQ折叠面板箭头旋转问题解答

问题1:旋转效果实现方式

两种方案都可实现,结合现有代码更推荐JS搭配CSS类名的方案:

  • 纯CSS方案可以实现,但你当前写的button::after:focus选择器完全无效——伪元素不属于独立DOM元素,无法接收焦点触发:focus状态。纯CSS实现要么用<details>+<summary>原生折叠标签,要么用隐藏的checkbox输入框,通过:checked状态搭配兄弟选择器控制样式。但纯CSS方案依赖元素本身的状态,点击页面其他区域导致按钮失焦时,旋转状态会意外重置,不符合FAQ面板展开后保持状态的交互预期。
  • JS+CSS类名方案更稳定:你已经在实现面板显隐切换的逻辑,只需要在切换内容显隐的同时,给对应按钮切换激活类名,通过类名控制伪元素旋转即可,状态不会意外丢失,还能方便加过渡动画。

问题2:React中伪元素的操作方式

::after/::before是CSS渲染层生成的虚拟元素,不属于真实DOM树的节点,无法通过ref获取、也不能直接作为值赋值给JS变量。
要控制伪元素样式只有两种通用方案:

  • 切换元素的CSS类名,通过不同类名下的伪元素样式规则实现效果,是最常用的方案
  • 通过JS动态修改元素的CSS变量,伪元素继承CSS变量值实现动态样式,适合需要传动态数值的场景

现有代码修正方案

1、CSS样式修正

首先修正无效的伪元素焦点选择器,给箭头加过渡动画,用激活类控制旋转:

.container>.textBox>.questions>div>button {
  width: 100%;
  height: 30px;
  background: transparent;
  color: var(--Very-dark-grayish-blue);
  font-size: 15px;
  text-align: left;
  border: 0;
  cursor: pointer;
  outline: none;
  /* 给伪元素做定位参考,替代float更稳定 */
  position: relative;
}

.container>.textBox>.questions>div>button:hover {
  color: var(--Soft-red);
}

/* 激活状态按钮文字加粗 */
.container>.textBox>.questions>div>button.active {
  font-weight: 700;
}

.container>.textBox>.questions>div>button::after {
  content: url(./images/icon-arrow.svg);
  position: absolute;
  right: 0;
  top: 50%;
  /* 默认状态箭头居中 */
  transform: translateY(-50%);
  /* 加旋转过渡效果 */
  transition: transform 0.25s ease;
}

/* 激活状态下箭头旋转180度,注意选择器写法:激活类在button上,不是在伪元素上 */
.container>.textBox>.questions>div>button.active::after {
  transform: translateY(-50%) rotate(180deg);
}

/* 折叠内容样式 */
.not-active {
  display: none;
}

2、JSX逻辑修正

你当前用多个ref操作DOM显隐的写法不符合React数据驱动视图的思路,建议用state存当前激活的面板id,自动关联按钮样式和内容显隐,同时减少重复代码:

import { useState } from 'react';

// 抽离FAQ配置数据,后续修改内容不用重复写JSX结构
const FAQ_CONFIG = [
  {
    id: 1,
    question: "How many team members can I invite?",
    answer: "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Temporibus obcaecati, quae, corporis."
  },
  {
    id: 2,
    question: "What is the maximum file upload size?",
    answer: "No more than 2GB. All files in your account must fit your allotted storage space."
  },
  {
    id: 3,
    question: "How do I reset my password?",
    answer: "Click “Forgot password” from the login page or “Change password” from your profile page. A reset link will be emailed to you."
  },
  {
    id: 4,
    question: "Can I cancel my subscription?",
    answer: "Yes! Send us a message and we’ll process your request no questions asked."
  },
  {
    id: 5,
    question: "Do you provide additional support?",
    answer: "Chat and email support is available 24/7. Phone lines are open during normal business hours."
  }
]

export default function FaqAccordion() {
  // 存储当前展开的FAQ项id,默认值为null代表全部折叠
  const [activeFaqId, setActiveFaqId] = useState(null);

  const toggleFaq = (targetId) => {
    // 点击已展开的项则收起,点击其他项则切换为展开当前项
    setActiveFaqId(prev => prev === targetId ? null : targetId);
  }

  return (
    <div className="container">
      <div className="imgBox">
        <img src={img_1} alt="illustration" className="img_1" />
        <img src={img_2} alt="shadow" className="img_2" />
      </div>

      <div className="textBox">
        <h1>FAQ</h1>
        <div className="questions">
          {FAQ_CONFIG.map(item => (
            <div key={item.id}>
              <button
                onClick={() => toggleFaq(item.id)}
                className={activeFaqId === item.id ? 'active' : ''}
              >
                {item.question}
              </button>
              <p className={activeFaqId === item.id ? 'content-active' : 'not-active'}>
                {item.answer}
              </p>
              <hr />
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 20:18:29