skip to content

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.


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: // 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);["z-index"] = "1000";["left"] = card_left[id] + "px";["top"] = card_top[id] + "px";["z-index"] = "0"; } function hideCard(id) { var el = document.getElementById("card_" + id); el.firstChild.src = "/images/cards/back.png";["WebkitTransform"] = "scale(1.0)"; } function moveToPack(id) { hideCard(id); var el = document.getElementById("card_" + id);["z-index"] = "1000";["left"] = "-140px";["top"] = "100px";["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";["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>

expand code box

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: // 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"; 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); } }

expand code box

Launch standalone demo »

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.


< JavaScript

User Comments

Post your comment or question

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

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.

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.


14 December, 2013


I try to create a simple effect in a toolbar button for firefox,
where, on mouse hover the icon of the button will be
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;
} image {
transform: rotate(360deg);

And your JavaScript something like:

var replayButton = document.getElementById("replayButton");
replayButton.addEventListener("mouseover") = function() {
replayButton.className = "active";
}, false);

22 January, 2013

Please include complete html in the downloads.

I've included that on the page now - for both versions of the code.

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.

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