How to? Building a flexible, animated timeline in WordPress
Posted on 27 February 2019 by David Hide
In updating our About Us page we wanted to have an interesting way to represent a brief history of BrightMinded. Annika came up with a fantastic design for a timeline which is inspired by the jagged lines inside the lightbulb of our BrightMinded logo. What was particularly interesting about this project was that we wanted this component to be as flexible as possible so that we could add or remove as many milestones as we wanted over time and it would scale well to all screen sizes. You can see the final product on our About us page.
ACF and PHP
Our site is built on WordPress and, along with the tremendously popular and invaluable Advanced Custom Fields (ACF) plugin, this allows us for a great deal of flexibility in the content displayed on a page. To allow site administrators to add as many milestones as they want to the timeline and edit or delete existing ones, I created an ACF custom field group for the timeline component.
<div class="”timeline__container”"> // Step into PHP $count = 0; foreach ($timeline_milestones as $milestone) increment the count add a div containing the $milestone content with a class of “js-milestone-count-” followed by the $count value </div>
- Vertical lines
- Horizontal lines
- Node heads
There are several values which will be useful in many of the functions we’ll create later, so it would make sense to have them as global variables.
milestoneCount // the number of milestones milestoneWidth // the width of the milestone elements
In order to position these lines later on it will be handy to know the width of the space available, so we’ll assign a lineSpace – a global variable – which refers to the corridor of space in which the lines live inside the container. We calculate this value by subtracting the width of a milestone from the width of the container.
lineSpace // the width of the corridor within which the lines will be positioned verticalLineLength // the height of the container divided by the number of milestones + 1 for the end node horizontalLineLength // two-thirds of the lineSpace
Adding the nodes
First we need to add the vertical and horizontal lines and the node heads, which we’ll refer to collectively as nodes, to the HTML. Each node has a vertical line, a horizontal line and a node head. We’ll use a for loop to create one for each milestone. Additionally, there’s a node head at the start and another vertical line and node head at the end. Just as we did in the PHP, we’ll give each element a class with a number relating to the milestone it will be connected to.
addNodes function pseudocode:
add a node head for the start for (i = 1; i <= milestoneCount; i++) add a vertical line with class “js-vert-line-” + i add a horizontal line with class “js-horiz-line-” + i add a node head with class “js-node-head-” + i add a vertical line for the end add a node head for the end
Positioning the Milestones
positionMilestones function pseudocode:
count = 0 for each milestone element increment the count if (count % 2 === 0) set the “left” CSS property of element to milestoneWidth + lineSpace else set the “left” CSS property to 0 Set the “top” CSS property to count * verticalLineLength
Positioning the Nodes
Positioning the nodes (the vertical and horizontal lines and node heads) is very similar to positioning the milestones, but slightly more challenging. Positioning the lines vertically is as simple as multiplying the line number by the length of a vertical line. For horizontal positioning we can once again make use of the modulus operator to determine if the element belongs on the left or the right, but how far to the left or right should the lines be? If it’s a line belonging to a right-sided node, then it should be horizontally positioned one-third of the width of the lineSpace, otherwise it should be two-thirds across.
Animating the Elements
At this point we have a cool looking flexible timeline, but wouldn’t it look cooler if it were animated? We want each node and milestone to animate in as the user scrolls it into view, that way the user won’t miss the animation happening whilst they’re looking at a different part of the page. We can add an event listener which listens for the window scroll event and have it call a function which will handle our animations. Debouncer functions are very useful in preventing the function being called too frequently (more about debouncers here). We’d better also call the function when the page loads in case the timeline is already in view before any scrolling happens. We’ll apply some keyframe animations to some CSS classes and add those classes to the elements when they scroll into view.
There’s a handy “jquery-visible” plugin which we can make use of to check whether an element is visible or not. Each time the scroll event fires we can loop through our milestones and check if they’re visible. If they are, we can add an animation class to the matching lines and node head. We want the vertical line to animate in first, followed by the horizontal line, and then the node head and milestone can appear together. In order to have the animations fire sequentially in this way, we can make use of the ‘animationend’ event which is fired whenever a CSS animation reaches completion. We add an animating class to the vertical line animation, then once the animationend event fires for that we add the animating class to the horizontal line, then once that ends we add the animating classes to the node head and milestone.
animationHandler function pseudocode:
For each milestone If milestone is visible Get the milestone number from the milestone’s data attribute Add the animation class to the vertical line of the same number Listen for animationend event Add the animation class to the horizontal line of the same number Listen for animationend event Add animation class to node head of same number Add animation class to milestone of same number
Resizing with Screen Width
Similarly to what we did with the animationHandler, we can also listen for a window resize event and call a resize function when it fires. The resize function doesn’t need to do anything new, since we’ve already written all the functions needed to adjust the sizes line lengths and positions, so we can just call them again. Once again, we’d better use a debouncer function to prevent the callback function being called too often and hindering performance.
Like most fun programming challenges, I found this was a task best solved by thinking the problem through using a pen and pad first, drawing diagrams and writing pseudocode, and then writing the code later once I knew I had an idea that made sense.
The key steps that help simplify this task are:
- Give the milestones a class and/or data attribute that indicate its number
- Use the modulus operator to determine if the element belongs on the left or the right
- Divide the available space into the relevant chunks
This has been a great project to work on and I hope this blogpost has been useful for you!