Cloning hey pin menu using CSS and JavaScript
Medhat Dawoud
Posted on October 7, 2020
Note: this post was originally published on my personal blog
The best way to strengthen the basics of web development or precisely to be a good frontend developer is to practice HTML, CSS, and JavaScript a lot, and the best way to learn these technologies is by making a lot of side projects using them, that's exactly why I decided to start a new series of blogs implementing simple ideas for building small pieces of working UI components, in most cases will be using vanilla JavaScript, vanilla CSS, and normal HTML markups.
In this article I'm starting my very first challenge by cloning the pin menu (set aside menu) of hey.com website, I made the free trial and after exploring it, I found quite a lot of good design decisions over there, but what pokes me most was the pin menu they implemented for emails as a shortcut to reach them, regardless the purpose of creating that as you can see it in the example above, but I liked the idea and decided to clone it and create a replica to it.
Assumptions
Gived an Array of data loaded in pure JavaScript as follow:
const listOfItems = [
"semicolon.academy",
"twitter@SemicolonA",
"FB.com/semicolonAcademy",
"YT.com/SemicolonAcademy",
"twitter@med7atdawoud",
"IG/medhatdawoud",
"medhatdawoud.net",
]
const stack = document.getElementById("stack")
for (let i = 0; i < listOfItems.length; i++) {
let div = document.createElement("div")
div.classList.add("single-block")
div.innerHTML = `
<div class="content">
<img src="${path / to / heyLogo}" />
<div><h3>${listOfItems[i]}</h3><p>description</p></div>
</div>`
stack.append(div)
}
From line 11 till 20 we have a normal for-statement, and inside it, we create every time a new div element and give it a class single-block
that will be used to apply the styling of the block of data so it can be similar to the hey.com menu item.
Keep in mind that stack
that is selected at line 11
is a div in the HTML file that will hold the whole design, and all CSS is provided later in the full example (don't worry about it for now).
Now we got the following result
This could be considered the start state, and from here we need to understand the challenge as follow.
Challenge
1- make the above list of items look like a stack.
2- make the behavior of expanding on click.
3- on click on anything other than the stack it should turn back in (collapsed).
Implementation
1- let's start by turning it from a list of divs into a stack.
To turn that list of divs into a stack view we need to play around with CSS, first of all, I can imagine the stack look to be a list of divs that are in different layers and each has a different position, so here is the list of CSS we are supposed to add in general
body {
font-family: sans-serif;
font-size: 16px;
}
#stack {
position: absolute;
height: 80vh;
bottom: 30px;
left: 40%;
text-align: center;
cursor: pointer;
}
.single-block {
position: absolute;
bottom: 0;
background: #fff;
box-shadow: 0 0 10px #eee;
border-radius: 10px;
transition: ease-in-out 0.2s;
}
.single-block .content {
display: flex;
padding: 11px 20px 9px;
}
.single-block .content img {
width: 2.25rem;
height: 2.25rem;
border-radius: 50%;
}
.single-block .content > div {
text-align: left;
margin-left: 10px;
width: 180px;
}
.single-block .content h3 {
margin: 0;
font-size: 16px;
font-weight: normal;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.single-block .content p {
color: #aaa;
margin: 0;
}
Now with the transform
property in css we can play around, as using transform we can scale the items up and down, and we can translate them in the Y axis which is exactly what we want to do, but we should add these styles inline in JavaScript because that will depend on the order of the item anyway.
basically we will start the first item with a scale of 1
and translate of 0
as follow
transform: scale(1) translateY(0);
using the above CSS we can inject it into each item in the list but with a different value of the scale and transform, I did the math and the conclusion is that the scaling down value is 0.033333333
so we that value each item should be smaller than the one before it, also for the translation on Y-axis value will be 0.7
same as scaling for each item.
also as per our referent (hey.com pin menu) items should be shown in reverse so we need to do that effect in reverse, and to get that reverse index we need the following line inside the for-statement.
const reverseIndex = listOfItems.length - 1 - i
Now we can use it as a variant for each item, the final added code before appending the item will be
const baseScaleChange = 0.033333333
const baseTranslateChange = 0.7
const reverseIndex = listOfItems.length - 1 - i
div.style.transform = `scale(${1 - reverseIndex * baseScaleChange})
translateY(-${baseTranslateChange * reverseIndex}rem)`
after applying that to the code we have before the append line we should get the following result
2- Now let's start the next task to expand on clicking on the stack
To do that we need basically to make a small change to the transform property, we need in this case to make each item translate in both axis' X and Y with a specific value, and also we need to use rotate
function provided in the transform property to make that curve in the list of items.
I did the Math again and the rotation change will be 1.1
and the translation on x change will be 0.4
per item, and finally the translation on Y will be 4
per item, and BTW we can use the function translate that takes first parameter is the value for X and the second one is for Y, to apply all of this we need to listen on click event for that stack div and loop on the current available list to apply that as follow:
stack.addEventListener("click", function(e) {
const blocks = this.querySelectorAll(".single-block")
blocks.forEach((block, i) => {
const reverseindex = blocks.length - 1 - i
block.style.transform = `rotate(${reverseindex * 1.1}deg)
translate(-${reverseindex * 0.2}rem, -${reverseindex * 4}rem)`
})
})
I think that the code explains itself, so I'm not going to spend a lot of time on this part, but now after adding this code and clicking on the stack div all items should expand just the same as the behavior in hey.com menu.
3- Now we reached the last task to collapse the menu again.
we can do that easily by doing the same thing we did before for items in the first place, by listening to click event on document
, as follow:
document.addEventListener("click", function(e) {
const blocks = document.querySelectorAll(".single-block")
blocks.forEach((block, i) => {
const reverseIndex = listOfItems.length - 1 - i
block.style.transform = `scale(${1 - reverseIndex * baseScaleChange})
translateY(-${baseTranslateChange * reverseIndex}rem)`
})
})
after adding the above code actually, the menu expands itself would not work anymore, and the reason is the Event Bubbling which is applying the click event on the stack div and then bubble the event to be applied on its parent and then on its parent until the document element which has already an implementation for the click event as well that we just implemented but what it is doing is exactly the opposite of what we are doing on clicking on the stack, so we need to stop that bubbling from happening, a very simple solution is by adding the following line at the beginning of event handing for stack div click.
e.stopPropagation()
that makes the final code in the js file will be as follow:
const heyLogoSrc =
"https://production.haystack-assets.com/assets/avatars/defaults/hey-84b6169bf4060a76a94a072fe96b5fef7970b02d19507e2ab3952c042c21b154.svg"
const listOfItems = [
"semicolon.academy",
"twitter@SemicolonA",
"FB.com/semicolonAcademy",
"YT.com/SemicolonAcademy",
"twitter@med7atdawoud",
"IG/medhatdawoud",
"medhatdawoud.net",
]
const baseScaleChange = 0.033333333
const baseTranslateChange = 0.7
document.addEventListener("DOMContentLoaded", function() {
const stack = document.getElementById("stack")
for (let i = 0; i < listOfItems.length; i++) {
let div = document.createElement("div")
div.classList.add("single-block")
div.innerHTML = `
<div class="content">
<img src="${heyLogoSrc}" />
<div><h3>${listOfItems[i]}</h3><p>description</p></div>
</div>`
const reverseIndex = listOfItems.length - 1 - i
div.style.transform = `scale(${1 -
reverseIndex * baseScaleChange}) translateY(-${baseTranslateChange *
reverseIndex}rem)`
stack.append(div)
}
stack.addEventListener("click", function(e) {
e.stopPropagation()
const blocks = this.querySelectorAll(".single-block")
blocks.forEach((block, i) => {
const reverseindex = blocks.length - 1 - i
block.style.transform = `rotate(${reverseindex *
1.1}deg) translate(-${reverseindex * 0.2}rem, -${reverseindex * 4}rem)`
})
})
document.addEventListener("click", function(e) {
const blocks = document.querySelectorAll(".single-block")
blocks.forEach((block, i) => {
const reverseIndex = listOfItems.length - 1 - i
block.style.transform = `scale(${1 -
reverseIndex * baseScaleChange}) translateY(-${baseTranslateChange *
reverseIndex}rem)`
})
})
})
That's pretty much it, what we implemented is very close to what is implemented on hey.com website.
Result and conclusion
Now we completed the challenge and the final result is as follow:
The final code can be found on challenges Github repo, and you can reach out to me if you have any suggestion in code or maybe another challenge 😉 on twitter @med7atdawoud, I hope that you learned anything useful today and if you do please share it with others.
Tot ziens 👋
Side Note: If you are an arabic speaker or understand arabic, you can also watch me coding that in an arabic tutorials in the following video on my technical arabic channel on Youtube
https://www.youtube.com/watch?v=4TPszCQt8nk
Posted on October 7, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.