创建“移动高亮”导航栏的两种JavaScript和CSS方法
在本教程中,Blake Lundquist将带领我们使用纯JavaScript和CSS实现“移动高亮”导航模式的两种方法。第一种技术使用getBoundingClientRect方法在导航栏项目被点击时显式动画边框。第二种方法使用新的View Transition API实现相同的功能。
初始标记
假设我们有一个单页面应用,内容变化时页面不会重新加载。起始HTML和CSS是标准的导航栏,额外添加了一个id为#highlight的div元素。我们给第一个导航项添加.active类。
在这个版本中,我们将#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元素添加点击事件处理程序,然后筛选仅由匹配我们所需选择器的元素引起的事件。
1
2
3
4
5
6
7
8
9
|
const navbar = document.querySelector('nav');
navbar.addEventListener('click', function (event) {
if (!event.target.matches('nav a:not(active)')) {
return;
}
console.log('click');
});
|
现在我们知道事件处理程序在正确的元素上工作,让我们添加代码将.active类移动到被点击的导航项。
1
2
3
4
5
6
7
8
|
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元素需要在导航栏上移动并将自身定位在活动项周围。让我们编写一个函数来计算新位置和宽度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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
|
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
|
// 页面加载时显示高亮
moveHighlight();
|
使用View Transition API
View Transition API提供了在网站视图之间创建动画过渡的功能。该API创建"之前"和"之后"视图的快照,然后处理它们之间的过渡。
对于这种方法,我们不再需要单独的#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的魔力。
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
|
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-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
|
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已经包含了实现类似原生应用交互的功能,而不会造成过大开销。我们通过使用两种方法实现了"移动高亮"导航模式:CSS过渡结合getBoundingClientRect()方法和View Transition API。
资源
- getBoundingClientRect()方法文档
- View Transition API文档
- Jake Archibald的"视图过渡:处理宽高比变化"