继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

用React和Framer打造获奖Accordion组件:复刻教程

www说
关注TA
已关注
手记 476
粉丝 83
获赞 493

最近,我看到一位设计师的作品集,她的作品集被多个设计网站展示。她的作品集中亮点多多,其中最让我印象深刻的是她的“过程”部分。她用一个设计精美的手风琴式展示,展示了她每个工作流程阶段的价值。这种平滑展开的方式显得非常高大上。

访问这个链接:https://www.ozgekeles.com/#process,这里介绍了...

出于好奇,我查看了开发工具,看看她是如何构建的。果然,她用 HTML 和 CSS 制作了那些流畅的动画——这种选择很适合那些希望对网站的视觉和交互效果有完全掌控的设计师。

受此启发,我决定使用我的前端技术栈:ReactTailwind CSSFramer Motion 来重新创建相同的Accordion。

在这篇文章里,我将带你了解我如何使用React构建结构部分,使用Tailwind设置样式,以及使用Framer Motion加入动画效果,来模仿手风琴。

前置条件

在开始之前,请确保你有以下内容:

  • 建立了一个 React 项目。
  • 安装了 Tailwind 依赖,执行以下命令:npm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p

使用的文件:2 个组件,1 个 App.jsx 文件。

第一步:创建一个简单的 Accordion 组件

为了本教程更方便,我们将会用默认的 Tailwind 字体样式。

用颜色工具一看,背景是 #e1e1e1,文字是 #202020

你可以在开发工具中找到字体大小,并将其转换成 Tailwind 的字体大小。

AccordionBasic.jsx // 弹出面板基本.jsx

    import React, { useState } from 'react';  

    const Title = "PROCESS";  
    const SubTitle = "我可以帮您的有:";  
    const skillsData = [  
      {  
        title: "艺术指导",  
        description: "我的流程始于为项目构思一个视觉故事,使其让人难忘、引人注目且美丽。我非常重视深入理解项目简报和客户需求,识别项目目标,并对竞争对手和目标受众进行深入研究..."  
      },  
      {  
        title: "数字设计",  
        description: "在确立了预期的氛围和基调之后,我探索不同的设计迭代,以创造愉快的用户体验,同时保持优雅的用户界面。我积极寻求用户和客户的反馈以指导设计演进..."  
      },  
      {  
        title: "Webflow开发",  
        description: "使用Webflow,我专注于自定义设计,以提供一个响应式、像素完美的网站,配以精心设计的动画。我的目标是创建一个无缝、易于维护的网站,能够完美适应任何屏幕尺寸..."  
      },  
      {  
        title: "交互设计",  
        description: "我相信动效和互动在数字环境中是必不可少的,作为受众与产品之间的重要桥梁。微妙的微交互可以提升整体用户体验,留下深刻印象..."  
      },  
    ];  

    const AccordionBasic = () => {  
      const [activeIndexes, setActiveIndexes] = useState([]);  

      const toggleAccordion = (index) => {  
        setActiveIndexes(prevIndexes =>  
          prevIndexes.includes(index)  
            ? prevIndexes.filter(i => i !== index)  
            : [...prevIndexes, index]  
        );  
      };  

      return (  
        <div className="relative min-h-screen flex flex-col bg-[#e1e1e1] text-[#202020]">  
          <div className="max-w-screen mx-auto my-auto px-4 md:px-24 w-full">  
            <div className="flex items-center justify-between">  
              <h1 className="text-5xl sm:text-7xl tracking-wide w-1/3 flex justify-start">  
                {Title}  
              </h1>  
              <span className="text-base sm:text-xl sm:mr-auto mt-auto">  
                {SubTitle}  
              </span>  
            </div>  
            <div className="border-b border-[#505050] mt-2" />  
            <div className="w-full sm:w-2/3 ml-auto">  
              {skillsData.map((skill, index) => (  
                <div  
                  key={index}  
                  className="border-b border-[#505050]"  
                >  
                  <div  
                    className="flex justify-between items-center py-10 cursor-pointer"  
                    onClick={() => toggleAccordion(index)}  
                    aria-expanded={activeIndexes.includes(index)}  
                    aria-controls={`panel-${index}`}  
                  >  
                    <div className="flex items-center gap-x-6 relative">  
                      <span className="text-sm mmd:text-lg tracking-[0.15em] font-semibold mb-auto ">  
                        {String(index + 1).padStart(2, '0')}  
                      </span>  
                      <span className="text-4xl font-medium ">  
                        {skill.title}  
                      </span>  
                    </div>  
                  </div>  
                  <div  
                    id={`panel-${index}`}  
                    className={`overflow-hidden transition-all duration-500 ${  
                      activeIndexes.includes(index) ? 'max-h-full' : 'max-h-0'  
                    }`}  
                  >  
                    <div className="pb-8 pr-12 text-lg whitespace-pre-line">  
                      <p>{skill.description}</p>  
                    </div>  
                  </div>  
                </div>  
              ))}  
            </div>  
          </div>  
        </div>  
      );  
    };  

    export default AccordionBasic;

https://scintillating-valkyrie-ffb53f.netlify.app/ 页面

第二步:加入一些动画和互动效果

在基本的手风琴布局完成后,是时候通过加入动画和互动来增强它了。在查看作品集中的动画时,有几个关键要素需要关注:

https://www.ozgekeles.com/#process

  1. 滚动页面时,标题会流畅地向上滑动进入视野。
  2. 每个技能标题在悬停时会有垂直旋转的效果,增加了一种微妙的互动效果。
  3. 折叠面板部分有一个“+”图标,当展开时会动画变为“–”
  4. 此外,悬停时,一个加粗的下划线会从左到右动画显示,引导用户与折叠面板项互动。

AccordionAnimation.jsx // 弹出面板动画组件

    import React, { useState, useEffect } from 'react';  
    import { motion, useAnimation } from 'framer-motion';  
    import { useInView } from 'react-intersection-observer';  

    // 折叠面板的标题和描述  
    const Title = "工作流程";  
    const SubTitle = "我能帮您的有:";  
    const skillsData = [  
      {  
        title: "艺术指导",  
        description: "我的工作流程从为项目打造一个视觉叙事开始,让它变得难忘、引人注目且美丽。我非常重视理解项目需求和客户需求,明确项目目标,并对竞品和目标受众进行详尽研究...",  
      },  
      {  
        title: "数字设计",  
        description: "确定了期望的氛围和调性后,我会探索不同的设计版本,以创造一个愉悦的用户体验,同时保持优雅的用户界面。我积极寻求用户和客户的反馈,以指导设计的演变...",  
      },  
      {  
        title: "Webflow 开发",  
        description: "通过 Webflow,我专注于自定义设计,打造一个响应式、像素完美的网站,带有精心设计的动画。我的目标是创建一个无缝、易于维护的网站,能够完美适应任何屏幕尺寸...",  
      },  
      {  
        title: "交互设计",  
        description: "我认为动态和可交互性在数字环境中至关重要,它们是观众和产品之间的重要桥梁。细微的互动可以提升整体用户体验,留下深刻印象...",  
      },  
    ];  

    // 一个图标组件,在折叠面板打开时旋转  
    const ExpandIcon = ({ isOpen }) => (  
      <motion.svg  
        width="32"  
        height="32"  
        viewBox="0 0 24 24"  
        className="ml-4"  
      >  
        <line  
          x1="5" y1="12"  
          x2="19" y2="12"  
          stroke="currentColor"  
          strokeWidth="1.5"  
        />  
        <motion.line  
          x1="12" y1="5"  
          x2="12" y2="19"  
          stroke="currentColor"  
          strokeWidth="1.5"  
          animate={{  
            rotate: isOpen ? 90 : 0,  
          }}  
          initial={false}  
          transition={{  
            duration: 0.5,  
            ease: 'easeInOut'  
          }}  
          style={{ originX: '50%', originY: '50%' }}  
        />  
      </motion.svg>  
    );  

    const AccordianAnimation = () => {  
      const [activeIndexes, setActiveIndexes] = useState([]);  
      const controls = useAnimation();  
      const [ref, inView] = useInView({ threshold: 0.1 });  

      const toggleAccordion = (index) => {  
        setActiveIndexes(prevIndexes =>  
          prevIndexes.includes(index)  
            ? prevIndexes.filter(i => i !== index)  
            : [...prevIndexes, index]  
        );  
      };  

      useEffect(() => {  
        if (inView) {  
          controls.start('visible');  
        }  
      }, [controls, inView]);  

      // 字母级动画的变体  
      const letterVariants = {  
        hidden: { opacity: 0, y: 30 }, // 开始时在屏幕外  
        visible: (i) => ({  
          opacity: 1,  
          y: 0,  
          transition: {  
            delay: i * 0.05, // 每个字母的逐帧延迟  
            duration: 0.4,  
          },  
        }),  
      };  

      // 标题和索引的滑动动画变体  
      const titleVariants = {  
        initial: { y: 0 },  
        hover: { y: 40 }, // 悬停时下移  
      };  

      const hiddenTitleVariants = {  
        initial: { y: -40 }, // 开始时隐藏在上方  
        hover: { y: 0 }, // 悬停时滑动到位置  
      };  

      const indexVariants = {  
        initial: { y: 0 },  
        hover: { y: -40 }, // 悬停时上移  
      };  

      const hiddenIndexVariants = {  
        initial: { y: 40 }, // 开始时隐藏在下方  
        hover: { y: 0 }, // 悬停时滑动到位置  
      };  

      return (  
        <div className="relative min-h-screen flex flex-col bg-[#e1e1e1] text-[#202020]">  
          <div className="max-w-screen mx-auto my-auto px-4 md:px-24 w-full">  
            <div ref={ref} className="flex items-center justify-between">  
              <h1 className="flex justify-start text-5xl sm:text-7xl tracking-wide w-1/3">  
              {Title.split("").map((letter, index) => (  
                  <motion.span  
                    key={index}  
                    custom={index}  
                    initial="hidden"  
                    animate={controls}  
                    variants={letterVariants}  
                    className="inline-block"  
                  >  
                    {letter}  
                  </motion.span>  
                ))}  
              </h1>  
              <span className="text-base sm:text-xl md:mr-auto mt-auto">  
                {SubTitle}  
              </span>  
            </div>  
            <div className="border-b border-[#505050] mt-2" />  
            <div className="w-full sm:w-2/3 ml-auto">  
              {skillsData.map((skill, index) => (  
                <motion.div  
                  key={index}  
                  className="border-b border-[#505050] relative group"  
                  initial="initial"  
                  whileHover="hover" // 触发悬停动画  
                >  
                  <div  
                    className="flex justify-between items-center py-10 cursor-pointer"  
                    onClick={() => toggleAccordion(index)}  
                    aria-expanded={activeIndexes.includes(index)}  
                    aria-controls={`panel-${index}`}  
                  >  
                    <div className="flex items-center gap-x-6 relative">  
                      <div className="relative overflow-hidden">  
                        {/* 显示的索引 */}  
                        <motion.span  
                          className="text-xs sm:text-lg tracking-[0.15em] font-bold mb-auto block"  
                          variants={indexVariants}  
                          transition={{ duration: 0.3 }}  
                        >  
                          {String(index + 1).padStart(2, '0')}  
                        </motion.span>  
                        {/* 隐藏的索引 */}  
                        <motion.span  
                          className="text-xs sm:text-lg tracking-[0.15em] font-bold mb-auto block absolute top-0 left-0"  
                          variants={hiddenIndexVariants}  
                          transition={{ duration: 0.3 }}  
                        >  
                          {String(index + 1).padStart(2, '0')}  
                        </motion.span>  
                      </div>  
                      <div className="relative overflow-hidden">  
                        {/* 显示的标题 */}  
                        <motion.span  
                          className="text-xl sm:text-4xl font-medium block"  
                          variants={titleVariants}  
                          transition={{ duration: 0.3 }}  
                        >  
                          {skill.title}  
                        </motion.span>  
                        {/* 隐藏的标题 */}  
                        <motion.span  
                          className="text-xl sm:text-4xl font-medium block absolute top-0 left-0"  
                          variants={hiddenTitleVariants}  
                          transition={{ duration: 0.3 }}  
                        >  
                          {skill.title}  
                        </motion.span>  
                      </div>  
                    </div>  
                    <ExpandIcon isOpen={activeIndexes.includes(index)} />  
                  </div>  
                  <motion.div  
                    id={`panel-${index}`}  
                    initial={{ height: 0 }}  
                    animate={{  
                      height: activeIndexes.includes(index) ? 'auto' : 0,  
                    }}  
                    transition={{ duration: 0.5, ease: 'easeInOut' }}  
                    style={{ overflow: 'hidden' }}  
                  >  
                    <div className="pb-8 pr-12 text-sm sm:text-lg whitespace-pre-line">  
                      <p>{skill.description}</p>  
                    </div>  
                  </motion.div>  
                  <div className="absolute bottom-0 left-0 w-full h-[1.5px] bg-transparent group-hover:bg-[#2b2a2a] transition-all duration-700 ease-in">  
                    <div className="h-full w-0 group-hover:w-full bg-[#2b2a2a] transition-all duration-700 ease-out"></div>  
                  </div>  
                </motion.div>  
              ))}  
            </div>  
          </div>  
        </div>  
      );  
    };  

    export default AccordianAnimation;

这是这个链接挺有趣的, 你可以去看看: https://scintillating-valkyrie-ffb53f.netlify.app/的第二页。

就这样!我们使用React、Tailwind以及Framer Motion构建了一个既优雅又可复用的Accordion组件——提供了流畅的动画效果和交互体验,而不再需要使用复杂的HTML或CSS动画技巧。

虽然我不会深入探讨代码的细节,但主要的工作是添加四个关键动画,这些动画让折叠面板有了更流畅、动感的感觉。

作为额外的奖励,我在代码中加入了几行 Tailwind CSS,以确保该组件在移动设备上也能良好显示。

https://scintillating-valkyrie-ffb53f.netlify.app/

可以在这里查看我的GitHub,下载全部组件哦!

讲明白

感谢你的参与!加入_In Plain English_社区!在你离开前,请注意:

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP