纯CSS&JS实现:丝滑渐变过渡的动态导航栏

150 阅读4分钟

背景

在前端开发中,通常实现的导航栏切换时没有添加交互效果,今天就从0到1实现一个美观且交互流畅的导航栏。

直接看效果:

tab-ezgif.com-video-to-gif-converter.gif

1.导航栏实现思路

实现的导航栏需要具备丝滑过渡动画,切换选项时,指示器通过自定义贝塞尔曲线(cubic-bezier(0.68, -0.55, 0.265, 1.55))实现灵动过渡效果。

2. 案例代码

下面逐部分拆解代码,带你理解每个模块的作用。

2.1 模板结构

代码如下:

    <template>
      <div class="nav-container">
        <!-- 渐变指示器:核心动画元素 -->
        <div class="indicator" :style="indicatorStyle"></div>
        
        <!-- 导航容器:包裹所有导航项 -->
        <div class="nav">
          <!-- 循环渲染导航项 -->
          <div 
            v-for="(tab, index) in tabs" 
            :key="index" 
            class="nav-item" 
            :class="{ active: activeTab === index }"
            @click="changeTab(index)"
          >
            <span>{{ tab.label }}</span>
          </div>
        </div>
      </div>
    </template>
  • 层级关系:nav-container 作为最外层容器,内部包含 indicator(指示器)和 nav(导航项容器),通过 z-index 控制层级(指示器 z=0,导航项 z=2)
  • 激活状态:通过 :class="{ active: activeTab === index }" 控制选中项的文字颜色(白色)

2.2 逻辑处理

主要控制交互与动画效果。

代码如下:

    <script setup>
    import { ref, computed } from 'vue'
    // 1. 导航数据:可根据需求扩展(如添加路由地址、图标等)
    const tabs = [
        { label: '首页', value: 0 },
        { label: '分类', value: 1 },
        { label: '购物车', value: 2 },
        { label: '我的', value: 3 }
    ]
    // 2. 选中项状态:用 ref 定义响应式变量
    const activeTab = ref(0)
    // 3. 指示器样式计算:用 computed 实时更新位置和宽度
    const indicatorStyle = computed(() => ({
      // 左偏移量:选中项索引 * (100% / 导航项数量)
      left: `${activeTab.value * (100 / tabs.length)}%`,
      // 宽度:均分导航栏(100% / 导航项数量)
      width: `${100 / tabs.length}%`
    }))
    </script>

2.3 实现视觉与动画

核心代码如下:

    <style scoped>
    /* 渐变指示器:核心视觉元素 */
    .indicator {
      position: absolute;
      top: 0;
      height: 100%;
      background: linear-gradient(45deg, #4facfe 0%, #00f2fe 100%); /* 蓝青渐变 */
      border-radius: 35px; /* 与容器圆角一致:避免棱角 */
      /* 自定义过渡曲线:让动画更灵动(非匀速) */
      transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
      z-index: 0; /* 指示器在最下层 */
    }
    </style>

3. 导航栏优化

基础版本已实现核心功能,下面分享几个实用的优化方向,满足更多场景需求。

3.1 添加 hover 效果:增强交互反馈

在 .nav-item 中添加 hover 样式,让未选中项也有交互反馈:

    .nav-item:hover:not(.active) {
      color: #4facfe; /* hover 时文字变浅蓝 */
      transform: scale(1.05); /* 轻微放大:增强感知 */
    }

3.2 支持路由跳转:适配单页应用

  1. 先引入 Vue Router:
    import { useRouter } from 'vue-router'
    const router = useRouter()
  1. 在 tabs 数组中添加路由地址:
    const tabs = [
      { label: '首页', value: 1, path: '/' },
      { label: '分类', value: 2, path: '/category' },
      { label: '购物车', value: 3, path: '/cart' },
      { label: '我的', value: 4, path: '/mine' }
    ]
  1. 修改 changeTab 方法,实现路由跳转:
    const changeTab = (index) => {
      activeTab.value = index
      router.push({ path: tabs[index].path })
    }

3.3 自定义指示器样式

代码中注释了 “下划线样式” 的指示器,若不需要渐变背景,可替换为:

    .indicator {
      position: absolute;
      top: auto;
      bottom: 0; /* 下划线靠底部 */
      height: auto;
      border-bottom: 2px solid #4facfe; /* 下划线颜色 */
      border-radius: 0; /* 取消圆角 */
      transition: all 0.3s ease; /* 简化过渡曲线 */
    }

3.4 支持触摸滑动

若需要在移动端支持 “触摸滑动切换”,可结合 touchstart 和 touchend 事件,计算滑动距离来判断是否切换导航项,核心逻辑如下:

    // 记录触摸起始位置
    const startX = ref(0)
    // 触摸开始
    const handleTouchStart = (e) => {
      startX.value = e.touches[0].clientX
    }
    // 触摸结束
    const handleTouchEnd = (e) => {
      const endX = e.changedTouches[0].clientX
      const diffX = endX - startX.value
      
      // 滑动距离超过 50px 才切换(避免误触)
      if (diffX > 50 && activeTab.value > 0) {
        activeTab.value--
      } else if (diffX < -50 && activeTab.value < tabs.length - 1) {
        activeTab.value++
      }
    }
    // 在模板中添加触摸事件
    <div class="nav" @touchstart="handleTouchStart" @touchend="handleTouchEnd">
      <!-- 导航项 -->
    </div>

4. 完整代码

完整代码如下(可直接复制使用):

        <template>
          <div class="nav-container">
            <div class="indicator" :style="indicatorStyle"></div>
            <div 
              class="nav" 
              @touchstart="handleTouchStart" 
              @touchend="handleTouchEnd"
            >
              <div 
                v-for="(tab, index) in tabs" 
                :key="tab.value"
                class="nav-item" 
                :class="{ active: activeTab === index }"
                @click="changeTab(index)"
              >
                <span>{{ tab.label }}</span>
              </div>
            </div>
          </div>
        </template>
        <script setup>
        import { ref, computed } from 'vue'
        import { useRouter } from 'vue-router'
        const router = useRouter()
        // 导航数据(支持路由)
        const tabs = [
          { label: '首页', value: 1, path: '/' },
          { label: '分类', value: 2, path: '/category' },
          { label: '购物车', value: 3, path: '/cart' },
          { label: '我的', value: 4, path: '/mine' }
        ]
        const activeTab = ref(0)
        const startX = ref(0)
        // 指示器样式
        const indicatorStyle = computed(() => ({
          left: `${activeTab.value * (100 / tabs.length)}%`,
          width: `${100 / tabs.length}%`
        }))
        // 切换导航(含路由跳转)
        const changeTab = (index) => {
          activeTab.value = index
          router.push({ path: tabs[index].path })
        }
        // 触摸滑动逻辑(移动端适配)
        const handleTouchStart = (e) => {
          startX.value = e.touches[0].clientX
        }
        const handleTouchEnd = (e) => {
          const endX = e.changedTouches[0].clientX
          const diffX = endX - startX.value
          if (diffX > 50 && activeTab.value > 0) {
            activeTab.value--
            router.push({ path: tabs[activeTab.value].path })
          } else if (diffX < -50 && activeTab.value < tabs.length - 1) {
            activeTab.value++
            router.push({ path: tabs[activeTab.value].path })
          }
        }
        </script>
        <style scoped>
        .nav-container {
          position: relative;
          width: 90%; /* 缩小宽度,避免贴边 */
          margin: 20px auto; /* 居中显示 */
          height: 44px;
          background: #fff;
          border-radius: 35px;
          box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
          overflow: hidden;
        }
        .nav {
          position: relative;
          display: flex;
          width: 100%;
          height: 100%;
          z-index: 1;
        }
        .nav-item {
          flex: 1;
          display: flex;
          justify-content: center;
          align-items: center;
          color: #555;
          font-size: 14px;
          font-weight: 500;
          cursor: pointer;
          transition: all 0.3s ease;
          z-index: 2;
        }
        .nav-item.active {
          color: #fff;
        }
        .nav-item:hover:not(.active) {
          color: #4facfe;
          transform: scale(1.05);
        }
        .indicator {
          position: absolute;
          top: 0;
          height: 100%;
          background: linear-gradient(45deg, #ff9a9e 0%, #fecfef 100%); 
          border-radius: 35px;
          transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
          z-index: 0;
        }
        </style>

5. 总结

最后总结一下:导航栏实现的核心思路就是是通过动态计算指示器样式,配合transition实现动画效果。

如有错误,请指正O^O!