Table of Contents
Since we got our JavaScript loops down, let’s cement what we learned in a mini-project. This tutorial will guide you through the basics of using JavaScript loops with a fun twist!
Instead of looping through standard arrays and objects, I’ll show you how to use the for-loop to iterate through the members of the globally-renowned K-pop group, BTS.
Why BTS? Because (a) DM dared ask “who’s BTS?” 🤦♀️ and (b) I feel duty-bound to educate the masses about important matters (like BTS) 😉
Plus, this hands-on approach to learning will help you understand the concepts of loops and keep you culturally current.
Fun fact: It came down to either BTS or Snow White’s dwarves…clearly BTS was the winner.
We’ll be creating this:
See the Pen JavaScript Loops With BTS by Klea Merkuri (@thehelpfultipper) on CodePen.
Though straightforward, this project packs a punch with a number of things such as:
- creating a one-page scroll effect
- animating down arrows
- setting a layout with CSS Grid
- building flip cards
Without further ado, let us commence.
Setting the stage with HTML
I’ll start by creating two main components — main and footer — with main holding the intro and grid pages.
The objective is to create a flip card for each BTS member on the grid page that’ll look like this:
<div id="grid">
<div id="start">
<div class="card-content">
<p>Click on a tile</p>
</div>
</div>
<div class="member"></div>
<div class="member"></div>
<div class="member"></div>
<div class="member"></div>
<div class="member"></div>
<div class="member"></div>
<div class="member"></div>
</div>
But I want to avoid HTML code repetition that occurs with the member divs to keep my code lean and DRY.
Luckily, we can avoid repetition easily with the JavaScript for-loop!
Since we’ll use JavaScript to dynamically add each of the divs in the code above, our HTML code truncates considerably.
<div id="main">
<div id="intro-content">
<h1>
<span>Meet</span>
<span>BTS</span>
</h1>
<div id="animated-arrow">
<i class="gg-chevron-down"></i>
<i class="gg-chevron-down"></i>
<i class="gg-chevron-down"></i>
</div>
</div>
<div id="main-content">
<div id="grid">
<div id="start">
<div class="card-content">
<p>Hover over tiles</p>
</div>
</div>
<!-- Rest of cards added using JS -->
</div>
</div>
</div>
<div id="footer">
<div id="blog">
<a href="<https://thehelpfultipper.com/>">The Helpful Tipper</a>
</div>
<div id="sign-off">Demo Project</div>
</div>
- intro-content: The intro page contains the header and down chevron icon with a fade-out animation inspired by this svg scroll-down animation.
Note 👉 The special syntax of the down arrows is similar to that used in our Font Awesome sample project. You’ll need to take the additional step of importing the required CSS asset at the top of your CSS file like so:
@import url('https://css.gg/chevron-down.css')
.
- main-content: The second page that holds the grid display of flip cards. It’s super short on HTML content since, other than the first card, JavaScript handles the rest.
Styling the intro page
I’ll start by adding some basic styles to the body and the main container followed by centering and enlargement of the title.
The
@import
brings in the down arrows and is required unless using a different library or you’re going the extra leg and totally customizing.
@import url('https://css.gg/chevron-down.css');
body {
margin: 0;
padding: 0;
height: 100vh;
font-family: Helvetica, Arial, sans-serif;
}
#main {
height: 100%;
}
/* Intro content */
#intro-content {
height: 100%;
text-align: center;
display: flex;
flex-direction: column;
position: relative;
}
h1 {
margin: 0;
position: relative;
top: 30%;
display: flex;
flex-direction: column;
letter-spacing: .2rem;
text-shadow: 0 0 200px #33333;
}
h1 span:nth-of-type(1) {
font-size: 3.5rem;
text-transform: lowercase;
font-family: sans-serif;
font-weight: 400;
}
h1 span:nth-of-type(2) {
font-size: 8rem;
}
Next, I’ll move onto the footer solely because I want a fixed footer.
Since working with fixed elements requires some workaround in the general layout, especially in our case where we’ll implement a one-page scroll, including it now is a good idea.
Read: CSS Visual Formatting: Floating, Positioning, & Layout
/* Footer content */
#footer {
background: #ffff;
height: 50px;
max-width: calc(250px*4);
width: 100%;
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
display: flex;
justify-content: space-between;
padding: 20px;
box-sizing: border-box;
}
#footer div {
display: inline-block;
}
#footer a {
color: black;
}
/* end Footer */
Hint: I’m using CSS transform to translate the footer container horizontally so it’s centered on the page. Using CSS translate is one workaround to centering fixed elements 😉
Creating a down arrow animation
Moving onto the down arrow animation, I’ll start by positioning the animated-arrow wrapper on the page before positioning the arrows themselves.
/* Down arrow animation */
#animated-arrow {
position: relative;
top: 34%;
}
.gg-chevron-down {
position: absolute;
left: calc(100vw/2 - 11px);
top: 34%;
transform: scale(2);
animation: pointdown 2s ease-out infinite;
}
Each arrow takes an absolute position so we overlay all three on top of one another. Then move two of the arrows further down an equal distance like so:
.gg-chevron-down:nth-of-type(2) {
top: calc(34% + 15px);
animation-delay: .5s;
}
.gg-chevron-down:nth-of-type(3) {
top: calc(34% + 30px);
animation-delay: 1s;
}
This positioning will help create the illusion of a single arrow moving downwards.
Notice how I add a 0.5 seconds delay on the second and third arrow to await the arrival of the previous one so they join and move together.
Let’s define the pointdown animation to see the positioning in action.
@keyframes pointdown {
0%{
opacity: 0;
}
33% {
opacity: 1;
}
66% {
opacity: 0;
}
100% {
opacity: 0;
}
}
/* end Intro */
I animate opacity here to create the downward trail.
You can opt to animate position instead to create a more fluid downward movement. In fact, I challenge you to do just that!
For more CSS animation in action see How To Make A CSS Loading Dot Animation.
We’re now ready to move onto the meat of this project, aka BTS flip cards 🙂
Building a page layout with CSS Grid
Time to build the main-content page that will house the flip cards!
I begin by creating a CSS grid container with a maximum of four columns and two rows. I say maximum because on smaller devices the Grid will be responsive and contract accordingly.
/* Main content */
#main-content {
height: 100%;
padding-top: 50px;
background: pink;
}
/* Create CSS Grid, 4x2 */
#grid {
max-width: calc(250px*4);
width: 100%;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px,calc( calc(250px*4)/4)));
grid-template-rows: auto;
justify-content: center;
margin-bottom: 50px;
background: green;
}
Although I’m leaving the number of rows up to the browser by setting them to auto, I’m being pickier with the columns.
I’d like to have each column a steady width of 250px – no bigger and no smaller than that. However, I want the columns to wrap once the screen width changes which is why I use auto-fit.
Tip: I’m manipulating minmax() to set a max number of columns to four so we don’t end up with more then four cards on large screen sizes.
There are various ways of approaching the construction of the CSS Grid so don’t get bogged down by this one. Feel free to construct it in a different way like setting a different parent wrapper width and working with spatial distribution.
But if following along, visually this is what we’ve got:
Note: 1D vs 2D CSS Layouts
When creating a layout it often comes down to flexbox vs CSS grid.
Is flexbox 1D or 2D?
Flexbox is 1D because you create either a row or a column at a time.
Is CSS Grid 1D or 2D?
CSS Grid is 2D because you create both rows and columns simultaneously.
Since CSS Grid is 2D, it’s best for defining a layout while flexbox is best for placing items and components inside of that layout.
Ideally, you’re best off using CSS Grid and flexbox together taking advantage of their different dimensionalities.
Fact: This was interesting to me because I thought the choice of which CSS layout property to use depended on personal preference and comfort but it appears to have a logical implementation 😅
More CSS Grid: How To Build An Eye-Catching Personal Portfolio Webpage
Adding the cards to the grid
We’re now ready to tackle the cards that make up the grid layout.
I’ll begin by declaring some common styles for all cards and then proceed to style the start card.
#start,
.member {
width: 250px;
height: 250px;
background: #EEF6F9;
}
/* Start card */
.card-content { position: relative; }
.card-content p {
padding: 0 20px;
font-size: 3.5rem;
position: relative;
margin: 0; /* Get rid of sizing font default margins */
top: 25px;
}
.card-content p:after {
content: '\21FE';
font-size: 1.2em;
margin-left: 15px;
position: absolute;
bottom: -8px;
}
There are two things to note for the start card:
margin: 0
is applied to the resized<p>
tag to account for the default font marginsp:after
is a pseudo-element that assigns an arrow-like icon using CSS content code
Meanwhile, for the rest of the cards, we’ll be using a JavaScript for-loop to iterate over a list of objects where each object represents a BTS member.
I’ll be displaying three properties for each BTS member – name, main, and image.
To serve the images, I hosted them on GitHub pages and pulled each image via its respective URL.
Hence the start code looks like this:
// Doc elements
const grid = document.querySelector('#grid');
// Image path
const domain = 'https://thehelpfultipper.github.io/loops_with_bts/img/';
// Create object instance => { [] }
const BTS = [
{
name: 'Jung Kook',
main: 'Main Vocals',
url: domain + 'jungkook_bts.PNG'
},
{
name: 'V',
main: 'Vocals',
url: domain + 'v_bts.PNG'
},
{
name: 'Jimin',
main: 'Lead Vocals',
url: domain + 'jimin_bts.PNG'
},
{
name: 'SUGA',
main: 'Main Rapper',
url: domain + 'suga_bts.PNG'
},
{
name: 'Jin',
main: 'Vocals',
url: domain + 'jin_bts.PNG'
},
{
name: 'RM',
main: 'Rapper',
url: domain + 'rm_bts.PNG'
},
{
name: 'j-hope',
main: 'Rapper',
url: domain + 'jhope_bts.PNG'
}
];
Now onto the loop! We’ll be going over each object in the BTS list displaying each of the three properties in our cards.
for( let i = 0; i < BTS.length; i++ ) {
console.log({
name: BTS[i].name,
main: BTS[i].main,
img: BTS[i].url
});
}
Tip 🤓
Console logging the result of a logical loop helps determine if it’s structure works and no errors are present. Plus, you can see if you’re hitting the correct values!
I’m using BTS[i]
to reference the object because recall BTS is a list of objects. To get the individual property value of a single object, you need to point to the property.
Of course, as we discussed in a prior post about JavaScript loops, working with object iterations is easier if choosing an alternative loop, like for-of or for-in.
But there’s a reason I chose the regular for-loop here 💁♀️
We need the index since we’ll be generating the actual HTML for the cards based on each item of the BTS list.
If you opt to use an alternative loop, make sure you have an index reference or similar counter variable to create the HTML elements!
Since the console is spitting out exactly what I want, the next move is to create the cards.
Building flip cards using CSS
For the flip card functionality, I’ll be borrowing the ready code W3Schools provides to avoid going on an unrelated tangent.
What we need to do is integrate the flip card functionality into our own code which, mind you, is a challenge in itself.
I’ll start by inspecting the HTML structure of a single card to get an idea of how many elements I’ll need to create.
I see that there are three containers:
- parent card wrapper with a single child
- inner card wrapper with two children
- front wrapper with content for the front of the card
- back wrapper with content for the flipside of the front
Inside the loop, create the four necessary wrapper divs, assign some CSS classes to add styles, and add them to the grid.
// Create div elements
let divCard = document.createElement('div'),
divInner = document.createElement('div'),
divFront = document.createElement('div'),
divBack = document.createElement('div');
// Assign class(es) to divs
divCard.setAttribute('class', 'member');
divInner.setAttribute('class', 'flip-card-inner');
divFront.setAttribute('class', 'flip-card-front');
divBack.setAttribute('class', 'flip-card-back');
// Nest divs under respective parents
divInner.appendChild(divFront);
divInner.appendChild(divBack);
divCard.appendChild(divInner);
grid.appendChild(divCard);
Each card now displays the given width, height, and background color defined in the member class on our stylesheet. I haven’t yet defined styles for the inner, front, and back flip card classes.
I gave each card a border for display so we can visualize what is going on! I remove this in a bit.
Adding content to the flip card sides
First, I’ll grab the flip card code to generate the member cards.
/* ****** Member cards ****** */
/* This container is needed to position the front and back side */
.flip-card-inner {
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.8s;
transform-style: preserve-3d;
}
/* Do an horizontal flip when you move the mouse over the flip box container */
.member:hover .flip-card-inner {
transform: rotateY(180deg);
}
/* Position the front and back side */
.flip-card-front, .flip-card-back {
position: absolute;
width: 100%;
height: 100%;
-webkit-backface-visibility: hidden; /* Safari */
backface-visibility: hidden;
}
/* Style the front side (fallback if image is missing) */
div.member:nth-of-type(odd) .flip-card-front {
background-color: #EEF6F9;
}
div.member:nth-of-type(even) .flip-card-front {
background-color: #B3C6E0;
}
/* Style the back side */
.flip-card-back {
background-color: #B3C6E0;
color: #002145;
transform: rotateY(180deg);
text-align: center;
}
There aren’t too many adjustments to make. A couple I’d like to bring to your attention are:
- The fallback card background colors form a checkered pattern for the front card wrappers. I do this using
nth-of-type(odd/even)
. - Declaring
perspective: 1000px
for each card based on the existing code from the source (I thought to bring this to your attention).
Second, display the image on the front card. This entails creating an image element inside the loop, setting its src attribute to the URL from our data list, and appending it as a child to the front card wrapper.
// Create
img = document.createElement('img')
// Set
img.setAttribute('src', BTS[i].url)
// Append
divFront.appendChild(img)
And we get something that looks artsy and abstract but really is a mild mess. Even my pet, Fishy, was rather baffled 🐟
This is all adjustable with some CSS. The distortion we see is due to the images getting thrown in each div without any dimension specifications.
So assign a class on each image element, img.setAttribute('class', 'member-img')
, and set each image to take on the full width and height of its container.
It’s that easy.
.member-img {
width: 100%;
height: 100%;
}
Now we have a lovely flip card front – time to move onto the back.
Same drill as before we create the elements, set classes and/or attributes, and append to containers.
memberInfo = document.createElement('div')
name = document.createElement('p')
main = document.createElement('p')
memberInfo.setAttribute('class', 'member-info');
name.setAttribute('class', 'member-name');
main.setAttribute('class', 'member-main');
name.innerHTML = BTS[i].name;
main.innerHTML = BTS[i].main;
memberInfo.appendChild(main);
memberInfo.appendChild(name);
divBack.appendChild(memberInfo);
As we did with images, style the back card’s content completing the grid in its entirety…sort of 😬
.member-info {
position: relative;
top: 40px;
}
.member-name {
font-size: 3.25em;
font-weight: 800;
margin: 0;
}
/* end Main */
There are still a few things left to polish like correcting the overflow of the main content page and adding a one-page scroll.
Creating a one-page scroll and working with overflow
I’m going to tackle a one-page scroll first and deal with the last few polishing touches later on.
Let’s create a one-page scroll effect with CSS scroll-snap properties. Set overflow-y and scroll-snap-type on the main div.
overflow-y: auto;
scroll-snap-type: y mandatory;
With scroll-snap-type
we’re enforcing a vertical scroll based on a set breakpoint.
Without the next step, the current properties don’t enforce a one-page scroll effect!
What breakpoint is that you ask? It’s scroll-snap-align which is set on each of the elements we want to snap on scroll.
In this case, it’s our two-page containers within the main wrapper or, in other words, all children of main.
#main > * {
scroll-snap-align: start;
}
The value of “start” basically says snap the two containers within main at the top position within the snap box (aka the main container).
Congrats, we’re now snapping queens and kings but take a second from the jubilation to observe what’s wrong on the second page.
- We have content overflow
- We can’t scroll to view the entire content because we snap back to the top of the page
- We’re unable to see RM, my bias, because he’s in the second to last card 😭
Long story short, our second page is slightly screwed up and we gotta fix it.
Correcting the overflow of the second page
To make a container scrollable we usually set overflow to auto thus enabling the scrollbar.
But if we set overflow: scroll
on the main-content div, the result is an undesired second scrollbar once we scroll within that container. It’s subtle but definitely there.
Luckily, there’s an easy way to hide the scrollbar but keep the scroll functionality across different browsers which is exactly what I’m going to do.
/* Add to main-content selector directly */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
/* Hide scrollbar for Chrome, Safari and Opera */
#main-content::-webkit-scrollbar {
display: none;
}
Making our application responsive
Fantastic! We’re nearly done with the last step being responsiveness.
CSS Grid makes our main-content page (the one with the bulk of the content) responsive without the need for media queries.
The only issue I see is with the arrow on our start card.
It makes little sense to have the arrow point to the right when the grid collapses into a single column. To adjust for this, I’ll use a different CSS content code for an alternative down-pointing arrow on smaller screens.
/* Responsive styles for arrow */
@media (max-width: 555px) {
.card-content p:after {
content: '\2935';
font-size: .85em;
margin-left: 20px;
bottom: -15px;
}
}
The project is now complete 🙌
As a little recap: While practicing the use of a JavaScript for-loop, we created a checkered grid layout that contains flip cards displaying the info of each BTS team member. As a little bonus, we incorporated an arrow animation and a one-page scroll.
Aren’t we awesome my dear THT-ers?
Grab the code. Give this a try, make it your own, and make it challenging and absolutely fish-baffling 🙃