1. Web Design
  2. UX/UI
  3. Responsive Design

用CSS和一点JavaScript写一个垂直时间线

Scroll to top

Chinese (Simplified) (中文(简体)) translation by Songfeng Li(李松峰) (you can also view the original English article)

在本教程中,我们将从零开始学习如何构建响应式垂直时间线。 首先,创建基本的标记结构,然后应用CSS伪元素魔法。 接着,使用JavaScript添加向下滚动页面时的过渡效果。

先来看看最终完成后的时间线效果(通过CodePen来看更大的版本

Please accept marketing cookies to load this content.

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>

此时的页面布局如下所示:

Please accept marketing cookies to load this content.

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
}

为了清楚起见,这里删除了列表项里的内容,结果如下:

Please accept marketing cookies to load this content.

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),时间线的效果就有了雏形:

Please accept marketing cookies to load this content.

奇数(odd)和偶数(even)“时间线元素”在样式上的差别主要是定位。 前者为left:45px;,后者为left:-439px;。 不理解这些值是怎么来的?好,下面是计算过程:

每个“时间线元素”的宽度+留白-每个列表项的宽度=400px+45px-6px=439px

其次,是各自伪元素上生成的箭头。 “奇数”伪元素生成左箭头,“偶数”伪元素生成右箭头。

4. 交互

时间线的基本结构完成了,下面看看新的需求:

  • 默认情况下,时间线元素应该隐藏;
  • 它们应该在父元素进入视口时出现。

第一个需求简单。 第二个还挺复杂的。 因为需要检测目标元素是否完全进入了当前视口,如果是则显示其子元素。 要实现这个功能,不需要使用任何JavaScript库(比如WOW.jsScrollReveal.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都应该隐藏。 为此我们visibilityopacity这两个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
}

下面的示意图展示了时间线的初始状态。 能看到时间线元素,是为了显示它们的初始位置而为它们应用了一点不透明度。

The default appearance of the timeline before the animationsThe default appearance of the timeline before the animationsThe default appearance of the timeline before the animations

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

The final appearance of the timeline after the animationsThe final appearance of the timeline after the animationsThe final appearance of the timeline after the animations

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库。

希望大家能从这篇教程中学到新东西,并利用这个时间线实现一些有意思的效果。 如果大家有什么问题,欢迎留言。