This tutorial explains some basic HTML5, CSS3 and JavaScript concepts. We will discuss data attribute, positioning, perspective, transitions, flexbox, event handling, timeouts and ternaries. You are not expected to have much prior knowledge in programming. If you know what HTML, CSS and JS are for, itâs more than enough!
The game has 12 cards. Each card consists of a container div named .memory-card, which holds two img elements. The first one represents the card front-face and the second its back-face.
The box-sizing: border-box property includes padding and border values into elementâs total width and height, so we can skip the math.
By setting display: flex to the body and margin: auto to the .memory-game container, it will be centered both vertically and horizontally.
.memory-game will also be a flex-container. By default, the items are set to shrink in width to fit the container. By setting flex-wrap to wrap, flex-items wrap along multiple lines, accordingly to their size.
Each card width and height is calculated with calc() CSS function. Letâs make three rows, four card each by setting width to 25% and height to 33.333% minus 10px from margin.
To position .memory-card children, letâs add position: relative so we can position the children absolutely, relative to it.
The property position: absolute set to both front-face and back-face, will remove the elements from the original position, and stack them on top of each other.
Letâs also add a click effect. The :active pseudo class will be triggered every time the element gets clicked. It will apply a .2s transition to its size:
To flip the card when clicked, a class flip is added to the element. For that, letâs select all memory-card elements with document.querySelectorAll. Then loop through them with forEach and attach an event listener. Every time a card gets clicked flipCard function will be fired. The this variable represents the card that was clicked. The function accesses the elementâs classList and toggles the flip class:
In the CSS the flip class rotates the card 180deg:
.memory-card.flip { transform:rotateY(180deg);}
To produce the 3D flip effect, we will add the perspective property to .memory-game. That property sets how far in the z plane the object is from the user. The lower the value the bigger the perspective effect. For a subtle effect, letâs apply 1000px:
To the .memory-card elements letâs add transform-style: preserve-3d, to position them in the 3D space created in the parent, instead of flattening it to the z = 0 plane (transform-style).
So, we got the card to 3D flip, yay! But why isnât the card face showing up? Right now, both .front-face and .back-face are stacked up onto each other, because they are absolutely positioned. Every element has a back face, which is a mirror image of its front face. The property backface-visibility defaults to visible, so when we flip the card, what we get is the JS badge back face.
To reveal the image underneath it, letâs apply backface-visibility: hidden to .front-face and .back-face.
If we refresh the page and flip a card, itâs gone!
Since weâve hidden both images back face, there is nothing in the other side. So now we have to turn the .front-face 180 degrees:
.front-face { transform:rotateY(180deg);}
And now, thereâs the desired flip effect!
Match card
Now that we have flipping cards, letâs handle the matching logic.
When we click the first card, it needs to wait until another card is flipped. The variables hasFlippedCard and flippedCard will manage the flip state. In case there is no card flipped, hasFlippedCard is set to true and flippedCard is set to the clicked card. Letâs also switch the toggle method to add:
So now, when the user clicks the second card, we will fall into the else block in our condition. We will check to see if itâs a match. In order to do that, letâs identify each card.
Whenever we feel like adding extra information to HTML elements, we can make use of data attributes. By using the following syntax: data-*, where, * can be any word, that attribute will be inserted in the elementâs dataset property. So, letâs add a data-framework to each card:
So now we can check for a match by accessing both cards dataset. Letâs extract the matching logic to its own method checkForMatch() and also set hasFlippedCard back to false. In case of a match, disableCards() is invoked and the event listeners on both cards are detached, to prevent further flipping. Otherwise, unflipCards() will turn both cards back by a 1500ms timeout that removes the .flip class:
A more elegant way of writing the matching condition is to use a ternary operator. Itâs composed by three blocks. The first block is the condition to be evaluated. The second block is executed if the condition returns true, otherwise the executed block is the third:
So now that we have the matching logic covered, we need to lock the board. We lock the board to avoid two sets of cards being turned at the same time, otherwise the flipping will fail.
Letâs declare a lockBoard variable. When the player clicks the second card, lockBoard will be set to true and the condition if (lockBoard) return; will prevent any card flipping before the cards are hidden or match:
The is still the case where the player can click twice on the same card. The matching condition would evaluate to true, removing the event listener from that card.
To prevent that, letâs check if the current clicked card is equal to the firstCard and return if positive.
if (this=== firstCard) return;
The firstCard and secondCard variables need to be reset after each round, so letâs extract that to a new method resetBoard(). Letâs place the hasFlippedCard = false; and lockBoard = false there too. The es6 destructuring assignment[var1, var2] = ['value1', 'value2'], allows us to keep the code super short:
Our game looks pretty good, but there is no fun if the cards are not shuffled, so letâs take care of that now.
When display: flex is declared on the container, flex-items are arranged by the following hierarchy: group and source order. Each group is defined by the order property, which holds a positive or negative integer. By default, each flex-item has its order property set to 0, which means they all belong to the same group and will be laid out by source order. If there is more than one group, elements are firstly arranged by ascending group order.
There is 12 cards in the game, so we will iterate through them, generate a random number between 0 and 12 and assign it to the flex-item order property:
In order to invoke the shuffle function, letâs make it a Immediately Invoked Function Expression (IIFE), which means it will execute itself right after its declaration. The scripts should look like this: