Introduction
Have you ever wondered what these components are and how components like these are made?
These are called timeline components. In this blog post, we'll explore what exactly these components are and how it's built. Instead of going with the usual React or other famous frameworks, we're gonna build this component with a little twist!
We are going to build it with web components technology. Yes, you heard me right web components!!. But don’t worry we will have a brief look into what web components are and then jump right into building our timeline component.
So, without any more delay, let's get right into it!
What is a timeline component?
A timeline component is a component that helps us to indicate an event’s summary at a particular time interval like below:
Features of our timeline component
Let’s first establish the essential features of our timeline component:
- Our component will be able to align the entire timeline either to the left or right.
- Each dot in the timeline can be changed from size and color perspective
- Help component consumers add an arbitrary number of children elements for each timeline item.
What are web components?
Now that we know what we are building, let us understand how we are going to build it.
Web component is a standard that helps you to build re-useable custom elements. These custom elements have their own scope and styles, thus enabling encapsulation. With this, they can be used multiple times in your codebase. This leads to less code collision and your custom element code is unaffected from the rest of your code.
So essentially you will get to do something like the below in your HTML:
<body>
<mycustomcomponent>Hello DOM</mycustomcomponent>
</body>
Web components are built based on three main pillars:
- Custom elements
- Shadow DOM
- HTML templates
To understand what these concepts are, I would highly recommend you guys to please refer to the MDN docs since these topics are out of the scope of this blog post. They have done a fantastic job of explaining them.
Proposed Architecture
Now we know what web components are and how they can be used to create our own custom element, therefore let us get started with the architecture of our component. This section will outline what we are trying to build.
The entire architecture of this component is as follows(please refer to the above diagram):
- We are going to consider our timeline component to be an unordered list i.e.
ul
HTML element. We extend the features oful
. This HTML element serves exactly our purpose i.e. to show a list. We are just going to change some appearance of thisul
element to achieve what we want. - Similar to the original unordered list which has
li
elements inside it, we are going to follow the same approach. Each item in the timeline component will make use ofli
element internally. - The
ul
element is going to be called astimeline-component
- It takes in the following props:
-
mode
=left
|right
- This prop helps to align all the items to the
left
orright.
- This means that all the dots can appear to the left or to the right
- This prop helps to align all the items to the
-
- It takes in the following props:
- The
li
elements will be represented by the component:timeline-item
- Each timeline item can take an arbitrary number of children elements in it.
- Each item will implicitly come with a div element that acts as a
dot
. - This component takes in the following attributes:
-
dotcolor
: Can be any RGB value or hex value as string. -
dotsize
:larget
medium
small
-
So essentially what we want to do is something like below:
<body>
<timeline-component mode="left">
<timeline-item dotcolor="red" dotsize="large">
<span>Event 1</span>
</timeline-item>
<timeline-item dotcolor="red" dotsize="large">
<span>Event 2</span>
</timeline-item>
</body>
Now we got to know the basic architecture of the entire component let us start building it 😃
Create scaffoldings for the project
Before we start let us create a simple project. This project will have the following files:
-
src
index.js
styles.css
index.html
We should create the above files and directories in the project directory of your choice or you can simply create a codesandbox of vanilla Javascript and start hacking
Implementation details
Timeline-component
First, let us define the structure of our index.html
file. Place the following content inside it:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link type="stylesheet" href="./src/styles.css"></link>
</head>
<body>
<div id="app">
</div>
<script src="src/index.js"></script>
</body>
</html>
It is a common practice that for creating a web component we use the template element. The advantage of using it is that we can write it inside our HTML file but it won’t get rendered immediately on the actual DOM. We need to define it using javascript APIs.
Let us create a template element like below:
<template id="timeline-component">
<style>
ul {
list-style: none;
padding-left: 0px;
margin: 0px;
position: relative;
}
:host([mode="left"]) ul {
direction: ltr;
border-left: 2px solid #cacaca;
border-right: 0;
}
:host([mode="right"]) ul {
border-right: 2px solid #cacaca;
border-left: 0;
direction: rtl;
}
</style>
<ul>
<slot></slot>
</ul>
</template>
This template element is for our timeline-component
. From the proposed architecture section, we discussed that we are going to use an unordered list tag ul
for this component. So we are going to do the same.
We used the ul
element inside the template element and we have placed the slot
element inside this tag. Slots are placeholders that take in the markups from light DOM. You can read more about slots from the MDN docs.
We make use of default slots here. Default slots are the slots that don’t have a name to it. It gets all of its nodes from the light DOM that are not slotted elsewhere.
Inside this template element, we add a style tag as well. In its style, we define the styles for the ul
element. This style will get loaded whenever the timeline-component
is defined. Styles defined here will remain inside the scope of the timeline-component
itself. Thus styles from outside do not affect these styles. Hence custom elements help us to achieve scoped styles.
This ul
element has base styles as follows:
ul {
list-style: none;
padding-left: 0px;
margin: 0px;
position: relative;
}
The style of the ul
element is also based on the mode
attribute. Whenever this attribute is set as left
we want the following CSS to get applied to the ul
element:
:host([mode="left"]) ul {
direction: ltr;
border-left: 2px solid #cacaca;
border-right: 0;
}
And when mode
is set to right
we want it to have the following CSS:
:host([mode="right"]) ul {
border-right: 2px solid #cacaca;
border-left: 0;
direction: rtl;
}
This helps us to move the content along with the dot to either left or right. We define these styles in the template component itself. We make use of the :host CSS selector. This selector is used to select the host node i.e. the <timeline-component>
itself.
Now it's time to define this template element. Let us create a file named: index.js
inside the src folder and place the below content in it:
class TimelineComponent extends HTMLElement {
constructor() {
super();
const template = document.querySelector("#timeline-component").content;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(template.cloneNode(true));
}
}
To define the timeline-component
we need to create a class inside the index.js
file. The responsibility of this class is to:
- Find the template element during initialization/page load
- Create a Shadow root.
- Attach the template node to this shadow root.
Let us first understand what we will do on page load or during the initialization of the component. As we created a new class called: TimelineComponent
we made use of its constructor to do the following things:
- We extend this component from
HTMLElement
class. - Then we make use of the
super
function to call all the properties and functions of theHTMLElement
. -
Next, we grab all the content of the template component we created with id:
timeline-component
.
const template = document.querySelector("#timeline-component").content;
-
Next, we create an open
shadowRoot
using the below line:
const shadowRoot = this.attachShadow({ mode: "open" });
-
Finally, we append all the contents of the template element to this shadow root:
shadowRoot.appendChild(template.cloneNode(true));
Now that we have our component ready, let us define this custom element. You can do this via calling the define
method of the customElements
API. This method takes in the name of the component that you or consumer of the component will use inside his HTML and maps it to the above class that we created. We do this as follows:
customElements.define("timeline-component", TimelineComponent);
Timeline-item
This component is used to represent the li
element as seen in our proposed architecture. The component will help the consumer to display an arbitrary number of children elements. This element will consist of a div element that corresponds to the dot
that we saw earlier. This component has the capability to control the behavior of this dot element.
timeline-item
will take in the following props:
-
dotsize
:large
medium
small
-
dotcolor
: Can be any RGB value or hex value as string.
Before we start creating and defining this custom element first let us create a template element for the same.
<template id="timeline-item">
<style>
li {
margin-left: 1rem;
margin-bottom: 2.5rem;
}
.dot {
border: 7px solid #cacaca;
display: inline-block;
border-radius: 50%;
position: absolute;
}
</style>
<li>
<div class="dot"></div>
<slot></slot>
</li>
</template>
This template element consists of the li
element as we have seen in the proposed architecture. This element has a div
element that acts as a dot for the timeline-item
component. We change the size and color of this dot element with the dotsize
and dotcolor
attribute.
We again make use of the style element to have scoped styles for this element.
We make use of the default slot mechanism to place all the children elements of timeline-item
component in the slot defined above. For example,
<timeline-item dotcolor="blue" dotsize="large">
<h1>23rd Jan 2023</h1>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Est
aliquid nesciunt reiciendis esse accusamus laudantium, assumenda
praesentium illo nemo ratione at itaque voluptatum cum. Veniam at
magnam itaque harum expedita.
</p>
</timeline-item>
Here the h1
and the p
elements will be part of the slot that we defined above.
Now that we have created the template
element for this component, let us start off with creating and defining this custom element.
Our pathway to create this component will be similar to that of the . In the same index.js
file, create a new class named: TimelineItem
and paste the following content inside it:
const DOT_SIZE = {
small: "7px",
medium: "10px",
large: "12px"
};
const DOT_SIZE_TO_POS_MAP = {
small: "-0.5rem",
medium: "-0.7rem",
large: "-0.8125rem"
};
class TimelineItem extends HTMLElement {
static get observedAttributes() {
return ["dotcolor", "dotsize"];
}
constructor() {
super();
const template = document.querySelector("#timeline-item").content;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(template.cloneNode(true));
}
attributeChangedCallback(e, oldValue, newValue) {
const dotElement = this.shadowRoot.querySelector(".dot");
const modeAttribute = this.parentElement.getAttribute("mode"); // get parent element attribute;
if (e === "dotcolor") {
dotElement.style.borderColor = newValue;
}
if (e === "dotsize") {
if (modeAttribute === "left") {
dotElement.style.borderWidth = DOT_SIZE[newValue];
dotElement.style.left = DOT_SIZE_TO_POS_MAP[newValue];
} else if (modeAttribute === "right") {
dotElement.style.borderWidth = DOT_SIZE[newValue];
dotElement.style.left = "";
dotElement.style.right = DOT_SIZE_TO_POS_MAP[newValue];
}
}
}
}
Similar to what we did in the timeline-component
we do the following in the TimelineItem
class:
-
We make use of the constructor, to do the following:
- Call the
super
method to initialize all the elements of its base class. -
Get all the content of the template element we created like below:
const template = document.querySelector("#timeline-item").content;
-
We create an open shadowRoot node and append all the content of the template node in this shadowRoot:
const shadowRoot = this.attachShadow({ mode: "open" }); shadowRoot.appendChild(template.cloneNode(true));
- Call the
-
In order for the
timeline-item
to observe thedotsize
anddotcolor
attributes, we need to make use ofobservedAttribute
static method that returns the array of attributes to observe. We have to do this as follows:
static get observedAttributes() { return ["dotcolor", "dotsize"]; }
We make use of this static getter method because we want to perform certain actions when these attributes changes. To perform these actions we make use of the
attributeChangedCallback
callback. This callback will get executed whenever the observed attributes changes which in this case aredotcolor
anddotsize
.
static get observedAttributes() { return ["dotcolor", "dotsize"]; } attributeChangedCallback(e, oldValue, newValue) { const dotElement = this.shadowRoot.querySelector(".dot"); const modeAttribute = this.parentElement.getAttribute("mode"); // get parent element attribute; if (e === "dotcolor") { dotElement.style.borderColor = newValue; } if (e === "dotsize") { if (modeAttribute === "left") { dotElement.style.borderWidth = DOT_SIZE[newValue]; dotElement.style.left = DOT_SIZE_TO_POS_MAP[newValue]; } else if (modeAttribute === "right") { dotElement.style.borderWidth = DOT_SIZE[newValue]; dotElement.style.left = ""; dotElement.style.right = DOT_SIZE_TO_POS_MAP[newValue]; } } }
- One thing to note here is that we access the parent element’s
mode
attribute to style our component. Here the parent component will betimeline-component
- Whenever the
dotcolor
changes we set the dot element’s border color to the attribute value. -
Whenever the
dotsize
changes we do the following changes:- If the parent element’s
mode
attribute isleft
then, we set the dot elements border width and its left CSS property. - We make use of the
DOT_SIZE
andDOT_SIZE_TO_POS_MAP
maps to manage the borderWidth and position of the element. -
These maps have the following default values:
const DOT_SIZE = { small: "7px", medium: "10px", large: "12px" }; const DOT_SIZE_TO_POS_MAP = { small: "-0.5rem", medium: "-0.7rem", large: "-0.8125rem" };
- If the parent element’s
- One thing to note here is that we access the parent element’s
-
Finally, we make use of the
define
method of thecustomElements
API to define this element like below:
customElements.define("timeline-item", TimelineItem);
Usage
Now that we have implemented these components, it is time for us to use these components. Place the below code in your index.html
file:
<timeline-component mode="left">
<timeline-item dotcolor="blue" dotsize="large">
<h1>23rd Jan 2023</h1>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Est
aliquid nesciunt reiciendis esse accusamus laudantium, assumenda
praesentium illo nemo ratione at itaque voluptatum cum. Veniam at
magnam itaque harum expedita.
</p>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Est
aliquid nesciunt reiciendis esse accusamus laudantium, assumenda
praesentium illo nemo ratione at itaque voluptatum cum. Veniam at
magnam itaque harum expedita.
</p>
</timeline-item>
<timeline-item dotcolor="orange" dotsize="medium">
<h2>Event 2</h2>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Est
aliquid nesciunt reiciendis esse accusamus laudantium, assumenda
praesentium illo nemo ratione at itaque voluptatum cum. Veniam at
magnam itaque harum expedita.
</p>
</timeline-item>
</timeline-component>
You can find all the code and the working example mentioned in this blog on this codesandbox.
Summary
So in this way you can build your own timeline component by leveraging the already existing ul
HTML element. We also touched some basics of the web component technology: custom elements, slots and templates. But I would highly recommend you guys to go through the MDN documentation for these concepts.
Future work and further reading:
- You can think about extending this component with functionality like:
- Creating a custom dot element that accepts a SVG icon, something similar to what MUI did here.
- Align the timeline component in center.
- Opposite content functionality.
Thank you for reading!
Top comments (1)