使用JavaScript和CSS创建动态高亮导航栏

本文详细介绍了两种使用原生JavaScript和CSS实现动态高亮导航栏的方法,包括getBoundingClientRect技术和新兴的View Transition API,帮助开发者创建流畅的导航动画效果。

使用JavaScript和CSS创建"动态高亮"导航栏

在本教程中,Blake Lundquist将带领我们使用纯JavaScript和CSS创建"动态高亮"导航模式的两种方法。第一种技术使用getBoundingClientRect方法,在点击导航栏项目时明确地为边框添加动画效果。第二种方法使用新的View Transition API实现相同的功能。

初始标记

假设我们有一个单页面应用程序,内容在不重新加载页面的情况下发生变化。起始的HTML和CSS是标准的导航栏,额外包含一个id为#highlight的div元素。我们给第一个导航项添加.active类。

1
2
3
4
5
6
7
<nav>
  <div id="highlight"></div>
  <a href="#" class="active">Home</a>
  <a href="#services">Services</a>
  <a href="#about">About</a>
  <a href="#contact">Contact</a>
</nav>

对于这个版本,我们将#highlight元素定位在具有.active类的元素周围以创建边框。我们可以利用绝对定位并在导航栏上为元素添加动画来创建所需的效果。我们通过添加left: -200px将其初始隐藏到屏幕外,并为所有属性包含过渡样式,以便元素位置和大小的任何变化都会逐渐发生。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#highlight {
  z-index: 0;
  position: absolute;
  height: 100%;
  width: 100px;
  left: -200px;
  border: 2px solid green;
  box-sizing: border-box;
  transition: all 0.2s ease;
}

添加点击交互的基本事件处理程序

我们希望高亮元素在用户更改.active导航项时产生动画效果。让我们向nav元素添加点击事件处理程序,然后筛选仅由匹配我们所需选择器的元素引起的事件。在这种情况下,我们只想在用户点击不具有.active类的链接时更改.active导航项。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const navbar = document.querySelector('nav');

navbar.addEventListener('click', function (event) {
  // 如果点击的元素没有正确的选择器则返回
  if (!event.target.matches('nav a:not(active)')) {
    return;
  }
  
  console.log('click');
});

打开浏览器控制台并尝试点击导航栏中的不同项目。你应该只在选择导航栏中的新项目时看到"click"被记录。

现在我们知道事件处理程序在正确的元素上工作,让我们添加代码将.active类移动到被点击的导航项。我们可以使用传递给事件处理程序的对象来找到初始化事件的元素,并在从先前活动的项中移除后给该元素添加.active类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const navbar = document.querySelector('nav');

navbar.addEventListener('click', function (event) {
  // 如果点击的元素没有正确的选择器则返回
  if (!event.target.matches('nav a:not(active)')) {
    return;
  }
  
  document.querySelector('nav a.active').classList.remove('active');
  event.target.classList.add('active');
});

移动高亮元素

我们的#highlight元素需要在导航栏上移动并将自身定位在活动项周围。让我们编写一个函数来计算新的位置和宽度。由于#highlight选择器应用了过渡样式,当位置改变时它会逐渐移动。

使用getBoundingClientRect,我们可以获取元素位置和大小的信息。我们计算活动导航项的宽度及其相对于父元素左边界的偏移量。然后,我们将样式分配给高亮元素,使其大小和位置匹配。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 移动高亮的处理程序
const moveHighlight = () => {
  const activeNavItem = document.querySelector('a.active');
  const highlighterElement = document.querySelector('#highlight');
  
  const width = activeNavItem.offsetWidth;

  const itemPos = activeNavItem.getBoundingClientRect();
  const navbarPos = navbar.getBoundingClientRect()
  const relativePosX = itemPos.left - navbarPos.left;

  const styles = {
    left: `${relativePosX}px`,
    width: `${width}px`,
  };

  Object.assign(highlighterElement.style, styles);
}

让我们在点击事件触发时调用新函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
navbar.addEventListener('click', function (event) {
  // 如果点击的元素没有正确的选择器则返回
  if (!event.target.matches('nav a:not(active)')) {
    return;
  }
  
  document.querySelector('nav a.active').classList.remove('active');
  event.target.classList.add('active');
  
  moveHighlight();
});

最后,让我们也立即调用该函数,以便在页面首次加载时边框移动到我们初始活动项的后面:

1
2
3
4
5
6
7
// 移动高亮的处理程序
const moveHighlight = () => {
 // ...
}

// 页面加载时显示高亮
moveHighlight();

现在,当选择新项目时,边框会在导航栏上移动。尝试点击不同的导航链接来为导航栏添加动画效果。

使用View Transition API

View Transition API提供了在网站视图之间创建动画过渡的功能。在底层,API创建"之前"和"之后"视图的快照,然后处理它们之间的过渡。视图过渡对于在文档之间创建动画很有用,提供了像Astro这样的框架中类似原生应用的用户体验。然而,API还提供了用于SPA风格应用程序的处理程序。我们将使用它来减少实现中所需的JavaScript,并更轻松地创建回退功能。

对于这种方法,我们不再需要单独的#highlight元素。相反,我们可以直接使用伪选择器为.active导航项设置样式,并让View Transition API在处理新导航项被点击时的前后UI状态之间的动画。

我们首先去掉#highlight元素及其相关的CSS,并用nav a::after伪选择器的样式替换它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
nav a::after {
  content: " ";
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border: none;
  box-sizing: border-box;
}

对于.active类,我们包含view-transition-name属性,从而解锁View Transition API的魔力。一旦我们触发视图过渡并更改DOM中.active导航项的位置,将拍摄"之前"和"之后"快照,浏览器将在整个栏上为边框添加动画。我们将给视图过渡命名为highlight,但理论上我们可以给它任何名称。

1
2
3
4
nav a.active::after {
  border: 2px solid green;
  view-transition-name: highlight;
}

一旦我们有了包含view-transition-name属性的选择器,剩下的唯一步骤就是使用startViewTransition方法触发过渡,并传入回调函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const navbar = document.querySelector('nav');

// 点击时更改活动导航项
navbar.addEventListener('click', async  function (event) {

  if (!event.target.matches('nav a:not(.active)')) {
    return;
  }
  
  document.startViewTransition(() => {
    document.querySelector('nav a.active').classList.remove('active');

    event.target.classList.add('active');
  });
});

上面是点击处理程序的修订版本。我们不再需要自己计算移动边框的大小和位置,View Transition API为我们处理所有这些。我们只需要调用document.startViewTransition并传入回调函数来更改具有.active类的项!

调整视图过渡

此时,当点击导航链接时,你会注意到过渡有效,但会出现一些奇怪的尺寸问题。

这种尺寸不一致是由视图过渡过程中的纵横比变化引起的。我们不会在这里详细说明,但Jake Archibald有详细的解释可供阅读。简而言之,为了确保边框的高度在整个过渡过程中保持统一,我们需要为::view-transition-old和::view-transition-new伪选择器声明明确的高度,这些选择器分别代表旧视图和新视图的静态快照。

1
2
3
4
5
6
7
::view-transition-old(highlight) {
  height: 100%;
}

::view-transition-new(highlight) {
  height: 100%;
}

让我们进行一些最终的重构,通过将回调移动到单独的函数并为不支持视图过渡的情况添加回退来整理我们的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const navbar = document.querySelector('nav');

// 更改应用了.active类的项
const setActiveElement = (elem) => {
  document.querySelector('nav a.active').classList.remove('active');
  elem.classList.add('active');
}

// 开始视图过渡并在点击时传入回调
navbar.addEventListener('click', async  function (event) {
  if (!event.target.matches('nav a:not(.active)')) {
    return;
  }

  // 不支持View Transitions的浏览器的回退:
  if (!document.startViewTransition) {
    setActiveElement(event.target);
    return;
  }
  
  document.startViewTransition(() => setActiveElement(event.target));
});

结论

网站UI状态之间的动画和过渡曾经需要许多千字节的外部库,以及冗长、混乱和容易出错的代码,但原生JavaScript和CSS后来包含了实现类似原生应用交互的功能,而不会造成太大负担。我们通过使用两种方法实现了"动态高亮"导航模式来证明了这一点:结合getBoundingClientRect()方法的CSS过渡和View Transition API。

资源

  • getBoundingClientRect()方法文档
  • View Transition API文档
  • Jake Archibald的"视图过渡:处理纵横比变化"
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计