使用 CSS 选择器创建动画菜单指示器

Avatar of James Nowland
James Nowland

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

以下文章由 James Nowland 撰写,他是 Headjam 的前端开发人员,Headjam 是一家位于澳大利亚纽卡斯尔的创意机构。James 在这里创建了一个相当简单的效果,但您可能会认为它需要一点 JavaScript。相反,它使用了一些巧妙的选择器用法。

在这篇文章中,我将介绍使用 兄弟选择器伪元素 来创建仅使用 CSS 的菜单指示器(通常使用 JavaScript 实现)的创造性方法。

以下是我们将要制作的内容

查看 CodePen 上 CSS-Tricks (@css-tricks) 编写的 步骤 3

我们将将其分解为三个步骤

  • 基本结构和样式
  • 构建指示器
  • 使指示器移动

我们还将在整个示例中利用 SCSS,以利用 Sass 提供的变量和函数,从而使长期维护变得更加容易。

步骤 1:基本结构和样式

首先,让我们使用基本的无序列表结构设置菜单的 HTML。我们还可以标记基本类名称以开始。

<ul class="PrimaryNav">
  <li class="Nav-item">Home</li>
  <li class="Nav-item">About</li>
  <li class="Nav-item is-active">Writing</li>
  <li class="Nav-item">Clients</li>
  <li class="Nav-item">Contact</li>
</ul>

到目前为止,没什么特别的。我们有带有 <ul> 元素的 PrimaryNav 类名称,它充当其内部列表项的容器,每个列表项都有一个 Nav-item 类。

定义变量

此导航的关键功能之一是最大宽度,该宽度根据其中的菜单项数量填充容器的空间。在这种情况下,我们将在 SCSS 中设置一个 $menu-items 变量,然后将其用于计算标记中每个 .Nav-item$width 值。

我们还添加了一个 $indicator-color 变量来定义(您猜对了)菜单悬停指示器将使用的颜色。

// Menu Item Variables
// The number of items in the menu
$menu-items: 5;
// We multiply it by 1% to get the correct % unit
$width: (100/$menu-items) * 1%;

// Colors
$background-color: #121212;
$indicator-color: #e82d00;

设置样式

从这里,我们可以为菜单创建基本样式

// The parent container
.PrimaryNav {
  // Remove the bullet points by default
  list-style: none;
  // Center all the things!
  margin: 50px auto;
  // The nav will never exceed this width and what our calculated percentages related back to 
  max-width: 720px;
  padding: 0;
  width: 100%;
}

// The menu items
.Nav-item {
  background: #fff;
  display: block;
  float: left;
  margin: 0;
  padding: 0;
  text-align: center;
  // Our current calculation of 5 items will generate 20%
  width: $width;

  // The first item in the menu
  &:first-child {
    border-radius: 3px 0 0 3px;
  }

  // The last item in the menu
  &:last-child {
    border-radius: 0 3px 3px 0;
  }

  // If the menu item is active, give it the same color as the indicator
  &.is-active a {
    color: $indicator-color;
  }

  a {
    color: $background-color;
    display: block;
    padding-top: 20px;
    padding-bottom: 20px;
    text-decoration: none;

    &:hover {
      color: $indicator-color;
    }
  }
}

查看 CodePen 上 CSS-Tricks (@css-tricks) 编写的 步骤 1

步骤 2:构建指示器

我们将以一种使用多个类的方式来标记它。我们可以仅使用 .PrimaryNav 类来完成相同的事情,但添加另一个类名称将在将来提供更大的灵活性。

我们已经拥有包含主要导航样式的 .PrimaryNav 类。现在让我们创建 .with-indicator 来构建指示器

<ul class="PrimaryNav with-indicator">

</ul>

在这里,我们可以使用 CSS 来代替通常在 JavaScript 中完成的操作。我们知道,在悬停时向元素添加类属于 JavaScript 的领域,但让我们看看如何在 CSS 中单独完成此操作。

棘手的部分是让菜单项相互通信。在无序列表中,第一个列表项 (:first-child) 可以通过兄弟选择器 +~ 与第二个子项通信,但第二个子项列表项无法与第一个子项通信(在 CSS 中无法像那样在 DOM 中向后移动)。

查看 CodePen 上 CSS-Tricks (@css-tricks) 编写的 步骤 2

事实证明,列表项中最好的监听器是 :last-child。最后一个子项可以听到其所有兄弟姐妹的所有:hover:active 状态。这使其成为设置指示器的完美候选者。

我们使用最后一个子项的 :before:after 元素创建红色指示器。:before 元素将使用 CSS 三角形负边距将其居中

// The hover indicator
.with-indicator {
  // The menu is "relative" to the absolute position last-child pseudo elements.
  position: relative;

.Nav-item:last-child {
  &:before, &:after {
    content: '';
    display: block;
    position: absolute;
  }
  
  // The CSS Triangle
  &:before {
    width: 0;
    height: 0;
    border: 6px solid transparent;
    border-top-color: $color-indicator;
    top: 0;
    left: 12.5%;
    // Fix the offset - may vary per use
    margin-left: -3px;
  }

  // The block that sits behind the text
  &:after {
    width: $width;
    background: $indicator-color;
    top: -6px;
    bottom: -6px;
    left: 0;
    z-index: -1;
  }
}
}

步骤 3:使指示器移动

现在指示器已设置好,需要在光标悬停在菜单项上时能够四处移动。见证 ~ 选择器的强大功能,它将用于匹配标记中第一个和最后一个子项之间的任何元素。

现在,默认情况下在 <ul> 元素上设置了 position:relative,这意味着指示器与第一个项目齐平。我们可以通过修改 left 位置将指示器从一个项目移动到另一个项目,并且由于所有菜单的宽度都相等,因此我们知道要将其向下移动一个位置,:last-child 选择器对于 :before:after 必须具有等于 .Nav-item 宽度 的偏移量。还记得我们方便的 $width 变量吗?我们可以在 left 属性上使用它。

这是我们在原生 CSS 中设置它的方式

.with-indicator .Nav-item:nth-child(1).is-active ~ .Nav-item:last-child:after {
  left: 0;
}
.with-indicator .Nav-item:nth-child(2).is-active ~ .Nav-item:last-child:after {
  left: 20%;
}
.with-indicator .Nav-item:nth-child(3).is-active ~ .Nav-item:last-child:after {
  left: 40%;
}
.with-indicator .Nav-item:nth-child(4).is-active:after {
  left: 60%;
}
.with-indicator .Nav-item:nth-child(5).is-active:after {
  left: 80%;
}

让我们使用 Sass 使其动态化

// Menu Item Variables
// The number of items in the menu, plus one for offset
$menu-items: 5;
// The actual number of items in the menu
$menu-items-loop-offset: $menu-items - 1;
// We multiply it by 1% to get the correct % unit
$width: (100/$menu-items) * 1%;

.with-indicator {
  @for $i from 1 through $menu-items-loop-offset {
    // When the .Nav-item is active, make the indicator line up with the navigation item.
    .Nav-item:nth-child(#{$i}).is-active ~ .Nav-item:last-child:after {
      left:($width*$i)-$width;
    }
    
   .Nav-item:nth-child(#{$i}).is-active ~ .Nav-item:last-child:before {
      left:($width*$i)+($width/2)-$width; /* this ensures the triangle lines up to the menu. */
    }
  } // end @for loop

值得注意的是,三角形 :before 在此 left 偏移量的基础上还有额外的半宽偏移量。

现在让我们添加一些动画和另一个 Sass for 循环,以便我们可以根据我们所在的页面初始化指示器的位置。当您将鼠标悬停在项目上时,指示器将移动。但是,一旦您移出鼠标,它将返回到 is-active 状态。这是一种简洁且无需 JavaScript 的创建菜单指示器的方法。

// We had to use !important to make the hovers overide for when the :last-child is-active or hovered
@for $i from 1 through $menu-items-loop-offset {
  // When the menu is :hover make the indicator line up with it.
  .Nav-item:nth-child(#{$i}):hover ~ .Nav-item:last-child:after {
    left:($width*$i)-$width !important;
  }

  .Nav-item:nth-child(#{$i}):hover ~ .Nav-item:last-child:before{
    left:($width*$i)+($width/2)-$width !important;
  }
} // end @for loop

// make sure the last-child talks to itself
.Nav-item {
  &:last-child {
    &:hover, &.is-active {
      &:before {
        left: (100%-$width)+($width/2) !important;
      }
      &:after{
        left: 100%-$width !important;
      }
    }        
  }
}

最终结果

就是这样!一个无需 JavaScript 依赖的动画菜单指示器。

查看 CodePen 上 CSS-Tricks (@css-tricks) 编写的 步骤 3