Thursday, November 30, 2023
HomeVideo EditingConstructing a Horizontal Timeline With CSS and JavaScript

Constructing a Horizontal Timeline With CSS and JavaScript


1. HTML Markup

The markup is equivalent to the markup we outlined for the vertical timeline, aside from three small issues:

  • We use an ordered listing as a substitute of an unordered listing as that’s extra semantically right.
  • There’s an additional listing merchandise (the final one) that’s empty. In an upcoming part, we’ll talk about the explanation.
  • There’s an additional factor (i.e. .arrows) that’s accountable for the timeline navigation.

Right here’s the required markup:

1
<part class="timeline">
2
  <ol>
3
    <li>
4
      <div>
5
        <time>1934</time>
6
        Some content material right here
7
      </div>
8
    </li>
9
     
10
    <!-- extra listing gadgets right here -->
11
    
12
    <li></li>
13
  </ol>
14
  
15
  <div class="arrows">
16
    <button class="arrow arrow__prev disabled" disabled>
17
      <img src="arrow_prev.svg" alt="prev timeline arrow">
18
    </button>
19
    <button class="arrow arrow__next">
20
      <img src="arrow_next.svg" alt="subsequent timeline arrow">
21
    </button>
22
  </div>
23
</part>

The preliminary state of the timeline appears like this:

2. Including Preliminary CSS Kinds

After some primary font types, coloration types, and so on. which I’ve omitted right here for the sake of simplicity, we specify some structural CSS guidelines:

1
.timeline {
2
  white-space: nowrap;
3
  overflow-x: hidden;
4
}
5

6
.timeline ol {
7
  font-size: 0;
8
  width: 100vw;
9
  padding: 250px 0;
10
  transition: all 1s;
11
}
12

13
.timeline ol li {
14
  place: relative;
15
  show: inline-block;
16
  list-style-type: none;
17
  width: 160px;
18
  peak: 3px;
19
  background: #fff;
20
}
21

22
.timeline ol li:last-child {
23
  width: 280px;
24
}
25

26
.timeline ol li:not(:first-child) {
27
  margin-left: 14px;
28
}
29

30
.timeline ol li:not(:last-child)::after {
31
  content material: '';
32
  place: absolute;
33
  prime: 50%;
34
  left: calc(100% + 1px);
35
  backside: 0;
36
  width: 12px;
37
  peak: 12px;
38
  rework: translateY(-50%);
39
  border-radius: 50%;
40
  background: #F45B69;
41
}

Most significantly right here, you’ll discover two issues:

  • We assign massive prime and backside paddings to the listing. Once more, we’ll clarify why that occurs within the subsequent part. 
  • As you’ll discover within the following demo, at this level, we can not see all of the listing gadgets as a result of the listing has width: 100vw and its mum or dad has overflow-x: hidden. This successfully “masks” the listing gadgets. Due to the timeline navigation, nevertheless, we’ll be capable of navigate by means of the gadgets later.

With these guidelines in place, right here’s the present state of the timeline (with none precise content material, to maintain issues clear):

3. Timeline Component Kinds

At this level we’ll model the div parts (we’ll name them “timeline parts” any longer) that are a part of the listing gadgets in addition to their ::earlier than pseudo-elements.

Moreover, we’ll use the :nth-child(odd) and :nth-child(even) CSS pseudo-classes to distinguish the types for the odd and even divs.

Listed here are the widespread types for the timeline parts:

1
.timeline ol li div {
2
  place: absolute;
3
  left: calc(100% + 7px);
4
  width: 280px;
5
  padding: 15px;
6
  font-size: 1rem;
7
  white-space: regular;
8
  coloration: black;
9
  background: white;
10
}
11

12
.timeline ol li div::earlier than {
13
  content material: '';
14
  place: absolute;
15
  prime: 100%;
16
  left: 0;
17
  width: 0;
18
  peak: 0;
19
  border-style: strong;
20
}

Then some types for the odd ones:

1
.timeline ol li:nth-child(odd) div {
2
  prime: -16px;
3
  rework: translateY(-100%);
4
}
5

6
.timeline ol li:nth-child(odd) div::earlier than {
7
  prime: 100%;
8
  border-width: 8px 8px 0 0;
9
  border-color: white clear clear clear;
10
}

And eventually some types for the even ones:

1
.timeline ol li:nth-child(even) div {
2
  prime: calc(100% + 16px);
3
}
4

5
.timeline ol li:nth-child(even) div::earlier than {
6
  prime: -8px;
7
  border-width: 8px 0 0 8px;
8
  border-color: clear clear clear white;
9
}

Right here’s the brand new state of the timeline, with content material added once more:

As you’ve most likely observed, the timeline parts are completely positioned. Meaning they’re faraway from the conventional doc movement. With that in thoughts, so as to be certain that the entire timeline seems, we now have to set massive prime and backside padding values for the listing. If we don’t apply any paddings, the timeline might be cropped:

How the timeline looks like without paddingsHow the timeline looks like without paddingsHow the timeline looks like without paddings

4. Timeline Navigation Kinds

It’s now time to model the navigation buttons. Do not forget that by default we disable the earlier arrow and provides it the category of disabled.

Listed here are the related CSS types:

1
.timeline .arrows {
2
  show: flex;
3
  justify-content: heart;
4
  margin-bottom: 20px;
5
}
6

7
.timeline .arrows .arrow__prev {
8
  margin-right: 20px;
9
}
10

11
.timeline .disabled {
12
  opacity: .5;
13
}
14

15
.timeline .arrows img {
16
  width: 45px;
17
  peak: 45px;
18
}

The foundations above give us this timeline:

5. Including Interactivity

The essential construction of the timeline is prepared. Let’s add some interactivity to it!

Variables

First issues first, we arrange a bunch of variables that we’ll use later. 

1
const timeline = doc.querySelector(".timeline ol"),
2
  elH = doc.querySelectorAll(".timeline li > div"),
3
  arrows = doc.querySelectorAll(".timeline .arrows .arrow"),
4
  arrowPrev = doc.querySelector(".timeline .arrows .arrow__prev"),
5
  arrowNext = doc.querySelector(".timeline .arrows .arrow__next"),
6
  firstItem = doc.querySelector(".timeline li:first-child"),
7
  lastItem = doc.querySelector(".timeline li:last-child"),
8
  xScrolling = 280,
9
  disabledClass = "disabled";

Initializing Issues

When all web page property are prepared, the init perform known as.

1
window.addEventListener("load", init);

This perform triggers 4 sub-functions:

1
perform init() {
2
  setEqualHeights(elH);
3
  animateTl(xScrolling, arrows, timeline);
4
  setSwipeFn(timeline, arrowPrev, arrowNext);
5
  setKeyboardFn(arrowPrev, arrowNext);
6
}

As we’ll see in a second, every of those capabilities accomplishes a sure process.

Equal-Top Timeline Parts

When you bounce again to the final demo, you’ll discover that the timeline parts don’t have equal heights. This doesn’t have an effect on the principle performance of our timeline, however you may choose it if all parts had the identical peak. To realize this, we may give them both a set peak through CSS (straightforward answer) or a dynamic peak that corresponds to the peak of the tallest factor through JavaScript.

The second choice is extra versatile and secure, so right here’s a perform that implements this conduct:

1
perform setEqualHeights(el) {
2
  let counter = 0;
3
  for (let i = 0; i < el.size; i++) {
4
    const singleHeight = el[i].offsetHeight;
5
    
6
    if (counter < singleHeight) {
7
      counter = singleHeight;
8
    }
9
  }
10
  
11
  for (let i = 0; i < el.size; i++) {
12
    el[i].model.peak = `${counter}px`;
13
  }
14
}

This perform retrieves the peak of the tallest timeline factor and units it because the default peak for all the weather.

Right here’s how the demo’s trying:

6. Animating the Timeline

Now let’s deal with the timeline animation. We’ll construct the perform that implements this conduct step-by-step.

First, we register a click on occasion listener for the timeline buttons:

1
perform animateTl(scrolling, el, tl) {
2
  for (let i = 0; i < el.size; i++) {
3
    el[i].addEventListener("click on", perform() {
4
      // code right here
5
    });
6
  }
7
}

Every time a button is clicked, we examine the disabled state of the timeline buttons, and in the event that they aren’t disabled, we disable them. This ensures that each buttons might be clicked solely as soon as till the animation finishes.

So, when it comes to code, the press handler initially accommodates these strains:

1
if (!arrowPrev.disabled) {
2
  arrowPrev.disabled = true;
3
}
4

5
if (!arrowNext.disabled) {
6
  arrowNext.disabled = true;
7
}

The following steps are as follows:

  • We examine to see if it’s the primary time we’ve clicked on a button. Once more, understand that the earlier button is disabled by default, so the one button that may be clicked initially is the subsequent one.
  • If certainly it’s the primary time, we use the rework property to maneuver the timeline 280px to the suitable. The worth of the xScrolling variable determines the quantity of motion. 
  • Quite the opposite, if we’ve already clicked on a button, we retrieve the present rework worth of the timeline and add or take away to that worth, the specified quantity of motion (i.e. 280px). So, so long as we click on on the earlier button, the worth of the rework property decreases and the timeline is moved from left to proper. Nonetheless, when the subsequent button is clicked, the worth of the rework property will increase, and the timeline is moved from proper to left.

The code that implements this performance is as follows:

1
let counter = 0; 
2
for (let i = 0; i < el.size; i++) {
3
  el[i].addEventListener("click on", perform() {
4
    // different code right here
5
  
6
    const signal = (this.classList.accommodates("arrow__prev")) ? "" : "-";
7
    if (counter === 0) {
8
      tl.model.rework = `translateX(-${scrolling}px)`;
9
    } else {
10
      const tlStyle = getComputedStyle(tl);
11
      // add extra browser prefixes if wanted right here
12
      const tlTransform = tlStyle.getPropertyValue("-webkit-transform") || tlStyle.getPropertyValue("rework");
13
      const values = parseInt(tlTransform.break up(",")[4]) + parseInt(`${signal}${scrolling}`);
14
      tl.model.rework = `translateX(${values}px)`;
15
    }
16
    counter++;
17
  });
18
}

Nice job! We’ve simply outlined a means of animating the timeline. The following problem is to determine when this animation ought to cease. Right here’s our method:

  • When the primary timeline factor turns into absolutely seen, it implies that we’ve already reached the start of the timeline, and thus we disable the earlier button. We additionally be certain that the subsequent button is enabled.
  • When the final factor turns into absolutely seen, it implies that we’ve already reached the top of the timeline, and thus we disable the subsequent button. We additionally, due to this fact, be certain that the earlier button is enabled.

Do not forget that the final factor is an empty one with a width equal to the width of the timeline parts (i.e. 280px). We give it this worth (or a better one) as a result of we need to ensure that the final timeline factor might be seen earlier than disabling the subsequent button.

To detect whether or not the goal parts are absolutely seen within the present viewport or not, we’ll make the most of the identical code we used for the vertical timeline. The required code which comes from this Stack Overflow thread is as follows:

1
perform isElementInViewport(el)  doc.documentElement.clientHeight) &&
7
    rect.proper <= (window.innerWidth 

Past the perform above, we outline one other helper:

1
perform setBtnState(el, flag = true) {
2
  if (flag) {
3
    el.classList.add(disabledClass);
4
  } else {
5
    if (el.classList.accommodates(disabledClass)) {
6
      el.classList.take away(disabledClass);
7
    }
8
    el.disabled = false;
9
  }
10
}

This perform provides or removes the disabled class from a component based mostly on the worth of the flag parameter. As well as, it could actually change the disabled state for this factor.

Given what we’ve described above, right here’s the code we outline for checking whether or not the animation ought to cease or not:

1
for (let i = 0; i < el.size; i++) {
2
  el[i].addEventListener("click on", perform() {
3
    // different code right here
4
    
5
    // code for stopping the animation
6
    setTimeout(() => {
7
      isElementInViewport(firstItem) ? setBtnState(arrowPrev) : setBtnState(arrowPrev, false);
8
      isElementInViewport(lastItem) ? setBtnState(arrowNext) : setBtnState(arrowNext, false);
9
    }, 1100);
10
  
11
    // different code right here
12
  });
13
}

Discover that there’s a 1.1 second delay earlier than executing this code. Why does this occur?

If we return to our CSS, we’ll see this rule:

1
.timeline ol {
2
  transition: all 1s;
3
}

So, the timeline animation wants 1 second to finish. So long as it completes, we look forward to 100 milliseconds after which, we carry out our checks.

Right here’s the timeline with animations:

7. Including Swipe Help

To date, the timeline doesn’t reply to the touch occasions. It might be good if we might add this performance, although. To perform it, we will write our personal JavaScript implementation or use one of many associated libraries (e.g. Hammer.js, TouchSwipe.js) that exist on the market.

For our demo, we’ll hold this straightforward and use Hammer.js, so first, we embody this library in our pen:

How to include Hammerjs in our penHow to include Hammerjs in our penHow to include Hammerjs in our pen

Then we declare the related perform:

1
perform setSwipeFn(tl, prev, subsequent) {
2
  const hammer = new Hammer(tl);
3
  hammer.on("swipeleft", () => subsequent.click on());
4
  hammer.on("swiperight", () => prev.click on());
5
}

Contained in the perform above, we do the next:

  • Create an occasion of Hammer. 
  • Register handlers for the swipeleft and swiperight occasions. 
  • Once we swipe over the timeline within the left path, we set off a click on to the following button, and thus the timeline is animated from proper to left.
  • Once we swipe over the timeline in the suitable path, we set off a click on to the earlier button, and thus the timeline is animated from left to proper.

The timeline with swipe assist:

8. Including Keyboard Navigation

Let’s additional improve the consumer expertise by offering assist for keyboard navigation. Our objectives:

  • When the left or proper arrow key is pressed, the doc must be scrolled to the highest place of the timeline (if one other web page part is at the moment seen). This ensures that the entire timeline might be seen.
  • Particularly, when the left arrow key is pressed, the timeline must be animated from left to proper.
  • In the identical means, when the proper arrow key is pressed, the timeline must be animated from proper to left.

The related perform is the next:

1
perform setKeyboardFn(prev, subsequent) {
2
  doc.addEventListener("keydown", (e) => {
3
    if ((e.which === 37) || (e.which === 39)) {
4
      const timelineOfTop = timeline.offsetTop;
5
      const y = window.pageYOffset;
6
      if (timelineOfTop !== y) {
7
        window.scrollTo(0, timelineOfTop);
8
      } 
9
      if (e.which === 37) {
10
        prev.click on();
11
      } else if (e.which === 39) {
12
        subsequent.click on();
13
      }
14
    }
15
  });
16
}

The timeline with keyboard assist:

9. Going Responsive

We’re virtually completed! Final however not least, let’s make the timeline responsive. When the viewport is lower than 600px, it ought to have the next stacked structure:

resprespresp

As we’re utilizing a desktop-first method, listed here are the CSS guidelines that we now have to overwrite:

1
@media display screen and (max-width: 599px) {
2
  .timeline ol,
3
  .timeline ol li {
4
    width: auto; 
5
  }
6
  
7
  .timeline ol {
8
    padding: 0;
9
    rework: none !necessary;
10
  }
11
  
12
  .timeline ol li {
13
    show: block;
14
    peak: auto;
15
    background: clear;
16
  }
17
  
18
  .timeline ol li:first-child {
19
    margin-top: 25px;
20
  }
21
  
22
  .timeline ol li:not(:first-child) {
23
    margin-left: auto;
24
  }
25
  
26
  .timeline ol li div {
27
    place: static;
28
    width: 94%;
29
    peak: auto !necessary;
30
    margin: 0 auto 25px;
31
  }
32
  
33
  .timeline ol li:nth-child(odd) div {
34
    rework: none;
35
  }
36
  
37
  .timeline ol li:nth-child(odd) div::earlier than,
38
  .timeline ol li:nth-child(even) div::earlier than {
39
    left: 50%;
40
    prime: 100%;
41
    rework: translateX(-50%);
42
    border: none;
43
    border-left: 1px strong white;
44
    peak: 25px;
45
  }
46
  
47
  .timeline ol li:last-child,
48
  .timeline ol li:nth-last-child(2) div::earlier than,
49
  .timeline ol li:not(:last-child)::after,
50
  .timeline .arrows {
51
    show: none;
52
  }
53
}

For 2 of the principles above, we had to make use of the !necessary rule to override the associated inline types utilized by means of JavaScript. 

The ultimate state of our timeline:

Browser Help

The demo works nicely in all current browsers and units. Additionally, as you’ve probably observed, we use Babel to compile our ES6 code all the way down to ES5.

The one small subject I encountered whereas testing it’s the textual content rendering change that occurs when the timeline is being animated. Though I attempted numerous approaches proposed in several Stack Overflow threads, I didn’t discover a easy answer for all working techniques and browsers. So, hold in your thoughts that you simply may see small font rendering points because the timeline is being animated.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments