用CSS和一点JavaScript写一个垂直时间线
Chinese (Simplified) (中文(简体)) translation by Songfeng Li(李松峰) (you can also view the original English article)
在本教程中,我们将从零开始学习如何构建响应式垂直时间线。 首先,创建基本的标记结构,然后应用CSS伪元素魔法。 接着,使用JavaScript添加向下滚动页面时的过渡效果。
先来看看最终完成后的时间线效果(通过CodePen来看更大的版本)
1. HTML标记
标记很简单,就是一个无序列表,然后每个列表项包含一个div元素。 因为时间线上要显示事件,所以为每个列表项添加time元素以显示时间。
此外,把整个列表都放到一个section元素里,给它一个类叫timeline:
1 |
<section class="timeline"> |
2 |
<ul>
|
3 |
<li>
|
4 |
<div>
|
5 |
<time>1934</time> |
6 |
Some content here |
7 |
</div>
|
8 |
</li>
|
9 |
|
10 |
<!-- more list items here -->
|
11 |
</ul>
|
12 |
</section>
|
此时的页面布局如下所示:
2. 添加CSS样式
先添加一些颜色(下面CSS代码的上半部分),再为列表项应用结构化的CSS规则。 同时,也为这些项目的::after伪元素添加样式:
1 |
.timeline ul li { |
2 |
list-style-type: none; |
3 |
position: relative; |
4 |
width: 6px; |
5 |
margin: 0 auto; |
6 |
padding-top: 50px; |
7 |
background: #fff; |
8 |
}
|
9 |
|
10 |
.timeline ul li::after { |
11 |
content: ''; |
12 |
position: absolute; |
13 |
left: 50%; |
14 |
bottom: 0; |
15 |
transform: translateX(-50%); |
16 |
width: 30px; |
17 |
height: 30px; |
18 |
border-radius: 50%; |
19 |
background: inherit; |
20 |
}
|
为了清楚起见,这里删除了列表项里的内容,结果如下:
3. 时间线元素样式
现在为列表项中的div元素(我们叫它“时间线元素”)添加样式。 同样,也给这些元素的::before伪元素添加样式。
一会儿我们会看到,并不是所有“时间线元素”的样式都完全一样。 这就需要用到:nth-child(odd)和:nth-child(even)这两个伪类了,通过它们可以实现样式差异化。
以下是相应的CSS规则:
1 |
.timeline ul li div { |
2 |
position: relative; |
3 |
bottom: 0; |
4 |
width: 400px; |
5 |
padding: 15px; |
6 |
background: #F45B69; |
7 |
}
|
8 |
|
9 |
.timeline ul li div::before { |
10 |
content: ''; |
11 |
position: absolute; |
12 |
bottom: 7px; |
13 |
width: 0; |
14 |
height: 0; |
15 |
border-style: solid; |
16 |
}
|
然后是奇数个元素的样式:
1 |
.timeline ul li:nth-child(odd) div { |
2 |
left: 45px; |
3 |
}
|
4 |
|
5 |
.timeline ul li:nth-child(odd) div::before { |
6 |
left: -15px; |
7 |
border-width: 8px 16px 8px 0; |
8 |
border-color: transparent #F45B69 transparent transparent; |
9 |
}
|
最后是偶数个元素的样式:
1 |
.timeline ul li:nth-child(even) div { |
2 |
left: -439px; |
3 |
}
|
4 |
|
5 |
.timeline ul li:nth-child(even) div::before { |
6 |
right: -15px; |
7 |
border-width: 8px 0 8px 16px; |
8 |
border-color: transparent transparent transparent #F45B69; |
9 |
}
|
有了这些样式(以及补充好内容的HTML),时间线的效果就有了雏形:
奇数(odd)和偶数(even)“时间线元素”在样式上的差别主要是定位。 前者为left:45px;,后者为left:-439px;。 不理解这些值是怎么来的?好,下面是计算过程:
每个“时间线元素”的宽度+留白-每个列表项的宽度=400px+45px-6px=439px
其次,是各自伪元素上生成的箭头。 “奇数”伪元素生成左箭头,“偶数”伪元素生成右箭头。
4. 交互
时间线的基本结构完成了,下面看看新的需求:
- 默认情况下,时间线元素应该隐藏;
- 它们应该在父元素进入视口时出现。
第一个需求简单。 第二个还挺复杂的。 因为需要检测目标元素是否完全进入了当前视口,如果是则显示其子元素。 要实现这个功能,不需要使用任何JavaScript库(比如WOW.js或ScrollReveal.js)。 甚至都不需要我们自己写太复杂的代码。 哈哈,StackOvewrflow上对这个功能有一个非常热门的讨论。 我们先用上面建议的方式测试某个元素是否完全进入了当前视口。
这是我们使用的一个简单的函数:
1 |
function isElementInViewport(el) { |
2 |
var rect = el.getBoundingClientRect(); |
3 |
return ( |
4 |
rect.top >= 0 && |
5 |
rect.left >= 0 && |
6 |
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && |
7 |
rect.right <= (window.innerWidth || document.documentElement.clientWidth) |
8 |
);
|
9 |
}
|
进入视口时添加类
接下来为当前视口中可见的列表项添加in-view类。
注意:测试它们在以下情况下是否可见很重要。
- 页面加载时
- 向下滚动时
有必要的话,还可以再测试更多的情况(比如浏览器窗口缩放时)。
我们这里使用的代码如下:
1 |
var items = document.querySelectorAll(".timeline li"); |
2 |
|
3 |
// code for the isElementInViewport function
|
4 |
|
5 |
function callbackFunc() { |
6 |
for (var i = 0; i < items.length; i++) { |
7 |
if (isElementInViewport(items[i])) { |
8 |
items[i].classList.add("in-view"); |
9 |
}
|
10 |
}
|
11 |
}
|
12 |
|
13 |
window.addEventListener("load", callbackFunc); |
14 |
window.addEventListener("scroll", callbackFunc); |
添加完JavaScript,再刷新页面,应该看到如下结果:



隐藏或显示
先回顾一下我们的需求。 记住,默认情况下,所有DIV都应该隐藏。 为此我们visibility和opacity这两个CSS属性。 另外,我们使用translated3d()把它们从原始位置移开200px。 只要它们的父元素可见,就显示它们并删除预先设置的位移。 这样,就可以实现漂亮的滑入特效。
最后还要做一件事,就是当li元素在视口中时,要修改其::before伪元素的背景颜色。
以下就是实现以上需要的所有样式:
1 |
.timeline ul li::after { |
2 |
background: #fff; |
3 |
transition: background .5s ease-in-out; |
4 |
}
|
5 |
|
6 |
.timeline ul li.in-view::after { |
7 |
background: #F45B69; |
8 |
}
|
9 |
|
10 |
.timeline ul li div { |
11 |
visibility: hidden; |
12 |
opacity: 0; |
13 |
transition: all .5s ease-in-out; |
14 |
}
|
15 |
|
16 |
.timeline ul li:nth-child(odd) div { |
17 |
transform: translate3d(200px,0,0); |
18 |
}
|
19 |
|
20 |
.timeline ul li:nth-child(even) div { |
21 |
transform: translate3d(-200px,0,0); |
22 |
}
|
23 |
|
24 |
.timeline ul li.in-view div { |
25 |
transform: none; |
26 |
visibility: visible; |
27 |
opacity: 1; |
28 |
}
|
下面的示意图展示了时间线的初始状态。 能看到时间线元素,是为了显示它们的初始位置而为它们应用了一点不透明度。



而以下是时间线的最终状态:



5. 响应式
终于快完成了。 最后一件事就是实现响应式的时间线。
首先,在所谓的“中等屏幕”(>600px且≤900px)上,我们只做少许改动。 特别是要修改DIV的宽度。
以下是要修改的规则:
1 |
@media screen and (max-width: 900px) { |
2 |
.timeline ul li div { |
3 |
width: 250px; |
4 |
}
|
5 |
.timeline ul li:nth-child(even) div { |
6 |
left: -289px; /*250+45-6*/ |
7 |
}
|
8 |
}
|
此时的时间线效果如下:



而在小屏幕(≤600px)上,所有时间线元素看起来都一样,无论是奇数个还是偶数个。 同样,需要覆盖一些CSS规则:
1 |
@media screen and (max-width: 600px) { |
2 |
.timeline ul li { |
3 |
margin-left: 20px; |
4 |
}
|
5 |
|
6 |
.timeline ul li div { |
7 |
width: calc(100vw - 91px); |
8 |
}
|
9 |
|
10 |
.timeline ul li:nth-child(even) div { |
11 |
left: 45px; |
12 |
}
|
13 |
|
14 |
.timeline ul li:nth-child(even) div::before { |
15 |
left: -15px; |
16 |
border-width: 8px 16px 8px 0; |
17 |
border-color: transparent #F45B69 transparent transparent; |
18 |
}
|
19 |
}
|
在更小一些的屏幕上,时间线应该是这样的:



注意:在小屏幕上,我们使用了vw单位指定时间线的宽度。 使用这个单位并没有什么特别的原因。 使用百分比或像素也是一样的。
浏览器支持
以上示例可以在大多数较新的浏览器和设备中运行。 但是在iOS设备上,时间线元素始终会保持可见,而不是在它们的父元素进入视口时才出现。
根据我的测试,我发现在那些设备上window.innerHeight和document.documentElement.clientHeight返回的并非实际的视口高度。 它们返回的值要大很多。 由于存在这种不一致的问题,所有列表项都会在页面加载后获得in-view类。
虽然这不算是个大问题(通常我们都希望在大屏幕上看到动画),但假如你知道这个问题,或者以前也碰到过,请在评论中留言吧。
小结
本教程教大家写了一个响应式的垂直时间线。 涉及的知识点不少,我们来回顾一下:
- 使用简单的无序列表和CSS伪元素,我们实现了时间线的主框架。 但也有一个不足,就是CSS伪元素并非百分之百地可用,要注意。
- 我们利用了StackOverflow上的一段代码来检测列表项是否进入了视口。 然后,我们又写了CSS实现它们子元素的进入动画。 当然,这里也可以利用JavaScript库。
希望大家能从这篇教程中学到新东西,并利用这个时间线实现一些有意思的效果。 如果大家有什么问题,欢迎留言。



