JavaScript: Controlling CSS Animations
This page presents a simple game of Concentration using JavaScript events and CSS transforms to provide real-time effects. The mechanics of the game are not so important, the aim is mainly to show how we can trigger animations using JavaScript events instead of just using the :hover event or keyframes.
The example below is working in WebKit browsers (Safari, Chrome, iPhone/iPad), Mozilla browsers (Firefox) and Opera. It may also work in Internet Explorer 10 though that has yet to be confirmed.
Working Example (v1.1)
Here you can see the final product. It's not flashy, but surprisingly smooth given how simple the code is. Just click on the card to the left to get started and read below for technical details.
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
![[]](/images/cards/back.png)
The goal of the game, as you probably already know, is to turn over two cards with the same value to make a pair. Once you've matched all eight pairs the game board will be back in the original state and you can start again with a new card order.
You can find an upgraded version further down the page, including a standalone demo.
Setting up the table
The HTML code for this is actually very simple:
<div id="stage">
<div id="felt">
<div id="card_0"><img onclick="cardClick(0);" src="/images/cards/back.png"></div>
<div id="card_1"><img onclick="cardClick(1);" src="/images/cards/back.png"></div>
<div id="card_2"><img onclick="cardClick(2);" src="/images/cards/back.png"></div>
...
<div id="card_15"><img onclick="cardClick(15);" src="/images/cards/back.png"></div>
</div>
</div>
These elements are styled using the following CSS:
<style type="text/css">
#stage {
border: 1px solid #ccc;
}
#felt {
position: relative;
margin: 0 0 0 200px;
height: 500px;
background: green;
}
#felt div {
position: absolute;
left: -140px;
top: 100px;
}
</style>
This creates the layout you see above. The #stage element is just the outline box. The #felt element creates the green background and allows for the sixteen card DIVs to be placed relative to it. The starting position for all sixteen cards is in a pile to the left of the #felt.
Enabling Transitions
Transitions are simply a way to have changes to CSS values affecting positioning or appearance carried out over a period of time rather than instantly.
In this case the main effect is that the cards will glide across the screen rather than just flicking instantly from one place to another. When clicked they will also change size smoothly.
Before CSS transitions became available such effects were only possible using Flash or complicated JavaScript libraries, the most popular being jQuery and MooTools.
The following additional CSS style is all we need to define our transitions:
<style type="text/css">
#felt div {
-webkit-transition: 0.5s ease-in-out;
-moz-transition: 0.5s ease-in-out;
-o-transition: 0.5s ease-in-out;
transition: 0.5s ease-in-out;
}
</style>
Any style changes applied to the card elements will now take place smoothly over half a second. For more information read our article on Transition Timing Functions.
Shuffling, Dealing and Playing
To start with we define the board position of all sixteen cards relative to the #felt element to form a 4x4 grid. The start/end position to the left of the board is defined in the CSS.
Each card on the board is then assigned a value from the card_values array ranging from '1C' (ace of clubs) to '8H' (8 of hearts). The card images are named to match.
From then it's just a matter of modifying the position (left, top) and zoom value (scale()) of the cards as the game is played. Because of the transition settings in the CSS these changes are seen (in modern browsers) as a smooth animation.
In the code below the sections responsible for the animations has been highlighted:
<script type="text/javascript">
// Original JavaScript code by Chirp Internet: chirpinternet.eu
// Please acknowledge use of this code by including this header.
var card_value = ['1C','2C','3C','4C','5C','6C','7C','8C','1H','2H','3H','4H','5H','6H','7H','8H'];
// set default positions
var card_left = [];
var card_top = [];
for(var i=0; i < 16; i++) {
card_left[i] = 70 + 100 * (i%4);
card_top[i] = 15 + 120 * Math.floor(i/4);
}
var started = false;
var cards_turned = 0;
var matches_found = 0;
var card1 = false;
var card2 = false;
function moveToPlace(id)
{
var el = document.getElementById("card_" + id);
el.style["z-index"] = "1000";
el.style["left"] = card_left[id] + "px";
el.style["top"] = card_top[id] + "px";
el.style["z-index"] = "0";
}
function hideCard(id)
{
var el = document.getElementById("card_" + id);
el.firstChild.src = "/images/cards/back.png";
el.style["WebkitTransform"] = "scale(1.0)";
}
function moveToPack(id)
{
hideCard(id);
var el = document.getElementById("card_" + id);
el.style["z-index"] = "1000";
el.style["left"] = "-140px";
el.style["top"] = "100px";
el.style["z-index"] = "0";
}
// flip over card and check for match
function showCard(id)
{
if(id === card1) return;
var el = document.getElementById("card_" + id);
el.firstChild.src = card_value[id] + ".png";
el.style["WebkitTransform"] = "scale(1.2)";
if(++cards_turned == 2) {
card2 = id;
// check whether both cards have the same value
if(parseInt(card_value[card1]) == parseInt(card_value[card2])) {
setTimeout("moveToPack(" + card1 + "); moveToPack(" + card2 + ");", 1000);
if(++matches_found == 8) {
// game over
matches_found = 0;
started = false;
}
} else {
setTimeout("hideCard(" + card1 + "); hideCard(" + card2 + ");", 800);
}
card1 = card2 = false;
cards_turned = 0;
} else {
card1 = id;
}
}
function cardClick(id)
{
if(started) {
showCard(id);
} else {
// shuffle and deal cards
card_value.sort(function() { return Math.round(Math.random()) - 0.5; });
for(i=0; i < 16; i++) setTimeout("moveToPlace(" + i + ")", i * 100);
started = true;
}
}
</script>
For some of the effects to work in other browsers you need to set not just the WebkitTransform property, but also MozTransform, OTransform and msTransform to the same value where it appears in the code.
Source Code (v1.1)
Here you can see the complete source for this example, inlucing links to download the CSS and JavaScript files. There are some slight changes from the code presented, mainly to cater for more browsers:
<html>
<head>
<title>JavaScript Card Game v1.1 | The Art of Web</title>
<link rel="stylesheet" type="text/css" href="/css-animation.css">
</head>
<body>
<div id="stage">
<div id="felt">
<div id="card_0"><img onclick="cardClick(0);" src="/images/cards/back.png"></div>
<div id="card_1"><img onclick="cardClick(1);" src="/images/cards/back.png"></div>
<div id="card_2"><img onclick="cardClick(2);" src="/images/cards/back.png"></div>
<div id="card_3"><img onclick="cardClick(3);" src="/images/cards/back.png"></div>
<div id="card_4"><img onclick="cardClick(4);" src="/images/cards/back.png"></div>
<div id="card_5"><img onclick="cardClick(5);" src="/images/cards/back.png"></div>
<div id="card_6"><img onclick="cardClick(6);" src="/images/cards/back.png"></div>
<div id="card_7"><img onclick="cardClick(7);" src="/images/cards/back.png"></div>
<div id="card_8"><img onclick="cardClick(8);" src="/images/cards/back.png"></div>
<div id="card_9"><img onclick="cardClick(9);" src="/images/cards/back.png"></div>
<div id="card_10"><img onclick="cardClick(10);" src="/images/cards/back.png"></div>
<div id="card_11"><img onclick="cardClick(11);" src="/images/cards/back.png"></div>
<div id="card_12"><img onclick="cardClick(12);" src="/images/cards/back.png"></div>
<div id="card_13"><img onclick="cardClick(13);" src="/images/cards/back.png"></div>
<div id="card_14"><img onclick="cardClick(14);" src="/images/cards/back.png"></div>
<div id="card_15"><img onclick="cardClick(15);" src="/images/cards/back.png"></div>
</div>
</div>
<script type="text/javascript" src="css-animation.js"></script>
</body>
</html>
If you have any problems or comments, or have managed to get something working in Internet Explorer, please let us know using the Feedback form below.
Advanced version (v2.0)
Here is a more 'professional' version of the game using a JavaScript class and requiring only a single HTML element on the page with the rest being created dynamically during initialisation:
<html>
<head>
<title>JavaScript Card Game v2.0 | The Art of Web</title>
<link rel="stylesheet" type="text/css" href="css-animation.css">
</head>
<body>
<div id="stage"><!-- --></div>
<script type="text/javascript" src="css-animation2.js"></script>
<script type="text/javascript">
var game = new CardGame("stage");
</script>
</body>
</html>
The CSS is identical to that presented above, but the JavaScript has been rewritten to use an object instead of polluting the global namespace. A couple of bugs have also been fixed.
Here is the replacement JavaScript code in full:
// Original JavaScript code by Chirp Internet: chirpinternet.eu
// Please acknowledge use of this code by including this header.
var CardGame = function(targetId)
{
// private variables
var cards = []
var card_value = ["1C","2C","3C","4C","5C","6C","7C","8C","1H","2H","3H","4H","5H","6H","7H","8H"];
var started = false;
var matches_found = 0;
var card1 = false, card2 = false;
var hideCard = function(id) // turn card face down
{
cards[id].firstChild.src = "/images/cards/back.png";
with(cards[id].style) {
transform = "scale(1.0) rotate(" + (Math.floor(Math.random() * 5) + 178) + "deg)";
}
};
var moveToPack = function(id) // move card to pack
{
hideCard(id);
cards[id].matched = true;
with(cards[id].style) {
zIndex = "1000";
top = "100px";
left = "-140px";
transform = "rotate(0deg)";
zIndex = "0";
}
};
var moveToPlace = function(id) // deal card
{
cards[id].matched = false;
with(cards[id].style) {
zIndex = "1000";
top = cards[id].fromtop + "px";
left = cards[id].fromleft + "px";
transform = "rotate(" + (Math.floor(Math.random() * 5) + 178) + "deg)";
zIndex = "0";
}
};
var showCard = function(id) // turn card face up, check for match
{
if(id === card1) return;
if(cards[id].matched) return;
cards[id].firstChild.src = "/images/cards/" + card_value[id] + ".png";
with(cards[id].style) {
// Firefox doesn't know which way to rotate
if(matches = transform.match(/^rotate\((\d+)deg\)$/)) {
if(matches[1] <= 180) {
transform = "scale(1.2) rotate(175deg)";
} else {
transform = "scale(1.2) rotate(185deg)";
}
} else {
transform = "scale(1.2) rotate(185deg)";
}
}
if(card1 !== false) {
card2 = id;
if(parseInt(card_value[card1]) == parseInt(card_value[card2])) { // match found
(function(card1, card2) {
setTimeout(function() { moveToPack(card1); moveToPack(card2); }, 1000);
})(card1, card2);
if(++matches_found == 8) { // game over, reset
matches_found = 0;
started = false;
}
} else { // no match
(function(card1, card2) {
setTimeout(function() { hideCard(card1); hideCard(card2); }, 800);
})(card1, card2);
}
card1 = card2 = false;
} else { // first card turned over
card1 = id;
}
};
var cardClick = function(id)
{
if(started) {
showCard(id);
} else {
// shuffle and deal cards
card_value.sort(function() { return Math.round(Math.random()) - 0.5; });
for(i=0; i < 16; i++) {
(function(idx) {
setTimeout(function() { moveToPlace(idx); }, idx * 100);
})(i);
}
started = true;
}
};
// initialise
var stage = document.getElementById(targetId);
var felt = document.createElement("div");
felt.id = "felt";
stage.appendChild(felt);
// template for card
var card = document.createElement("div");
card.innerHTML = "<img src=\"/images/cards/back.png\">";
for(var i=0; i < 16; i++) {
var newCard = card.cloneNode(true);
newCard.fromtop = 15 + 120 * Math.floor(i/4);
newCard.fromleft = 70 + 100 * (i%4);
(function(idx) {
newCard.addEventListener("click", function() { cardClick(idx); }, false);
})(i);
felt.appendChild(newCard);
cards.push(newCard);
}
}
The next step would be to have the style sheet appended dynamically to the DOM using JavaScript, and being able to specify a path to the card images (currently they are in /images/cards/), but those are relatively minor details.
Please feel free to adapt use this code and let us know what you come up with using the Feedback form below. The game can be adapted for image recognition or language learning by changing the images used.
References
Related Articles - Transforms and Transitions
- CSS Animation Using CSS Transforms
- CSS Fading slideshow with a touch of JavaScript
- CSS 3D Transforms and Animations
- CSS Transition Timing Functions
- CSS Bouncing Ball Animation
- CSS Upgraded fading slideshow
- CSS Infinite Animated Photo Wheel
- CSS An actual 3D bar chart
- CSS Photo Rotator with 3D Effects
- JavaScript Animating objects over a curved path
- JavaScript CSS Animated Fireworks
- JavaScript Controlling CSS Animations
Fred 4 December, 2021
I try do change card's size but don't find where to do. Seems to me to be in CSS felt but not explicit. Thanks for your help.
In both examples the card size is entirely determined by the card PNG images
Tanner 17 March, 2018
@Mitch I wanted to do the same thing, it's pretty simple. All you have to do is scroll down in the .js page and change the card back.png to whatever you want the back to be.
Mitch 4 March, 2017
Hey! Thanks for the code! Modifying it for a student assignment. Is it possible to code a button that changes the deck? Like you could swap out the picture on the back? Let me know! I'm still a noob.
Thanks!
George 14 December, 2013
Hello,
I try to create a simple effect in a toolbar button for firefox,
where, on mouse hover the icon of the button will be
rotated.
Currently my implementation in css has as follows:
---------------------------
/* ToolbarButton */
#replayButton {
list-style-image: url("chrome://flickerfox/skin/replay.png");
}
#replayButton:hover image {
transform: rotate(360deg);
transition-delay: 0.4s;
transition-duration: 1s;
}
---------------------------
It works but not exactly as i wish.
I want to make the effect to be completed, even if the mouse
pointer is moved away of the button.
This is also because, if the mouse pointer is moved before the
completion of the effect, the effect is reset instantly in its initial
state. This is quite annoying for me..
Is this possible, even involving javascript?
Thank you very much
The pure CSS effects transition an element from it's default state (not rotated) to a modified state (rotated) only when the mouse is over the trigger element. They do not allow for the default state to be changed.
Instead of using :hover as the trigger you will need to use an "onmouseover" event handler to change the class which in turn can trigger a permanent transformation.
So your CSS needs to change to:
#replayButton image {
transform: rotate(0);
transition-delay: 0.4s;
transition-duration: 1s;
}
#replayButton.active image {
transform: rotate(360deg);
}
And your JavaScript something like:
var replayButton = document.getElementById("replayButton");
replayButton.addEventListener("mouseover") = function() {
replayButton.className = "active";
}, false);
Bill Harten 22 January, 2013
Please include complete html in the downloads.
I've included that on the page now - for both versions of the code.
noni gilz 3 September, 2012
i appreciate ur explanation, how can one use the code for a playing a card flip over game to be played by 2-8 people where the person with the highest sum of two card wins, with each person staking virtual points and the winner takes all points contibuted. each player is timed for 15 seconds after which his turn passes up to the next player, and he gets a second chance when all players are done if not the computer flips for him.
That would require an Ajax-based application and a central database to enable sharing of data. Not too difficult, but network delays could cause problems.
Jasmina Susak 18 May, 2012
Thank you for this stuff. Just wanted to ask you: is it possible to set 8 images instead of value of the cards. Thanks
You can use any images at all. The only proviso is that 'matching' images have name/filenames that start with the same number.
e.g. in the above example 7H.png is a 'match' for 7C.png