Playing with CSS3 Animations

I would like to share with you a personal experience I had while trying to build advanced visual animations with CSS keyframes. The topic we’re going to address is animating several HTML elements simultaneously and taking advantage of keyframe flexibility. To explain this subject, we will create a simple fluid menu based on four squares. Clicking on one of them will display an overlay showing related content, closing the overlay will reverse the process showing the default menu. All delays will be managed directly with CSS — no javascript timeout() here.

The example described below is the first of four demos, all of which are using the same JS file, CSS class names, and are based on the same operating principles. All demos have been tested on Chrome, Safari, Firefox, Opera, IE11 and IE10 (also on iOS). They are not intended for production; they are simply provided as tangible case studies. Additionally, you may have to implement some fallbacks for users (especially those using old IE browsers), something beyond the scope of this article.

Playing with CSS3 Animations

To improve readability, CSS rules used throughout this article are without vendor prefixes. Make sure you add prefixes accordingly (e.g. -webkit- and -moz-). Opera and IE10+ do not need older prefixes anymore. You can read official W3C documentation for more information about vendor prefixes.

CSS3 Animation Basics

Let’s start by going over some elementary concepts. CSS3 keyframes are powerful rules that let you control CSS styles of an element during a time scale. You can specify when a change will occur in percent where “0%” is the start of the animation and “100%” is the end. The keywords “from” and “to” are equivalent to “0%”” and “100%.” For compatibility reasons, make sure to always specify “0%” and “100%” values.

Are you looking for WordPress Theme?

Get Startup Framework for WordPress now!

CSS keyframes syntax:

/* Percentage */
@keyframes moveTop {
    0%      { top: 0px; }
    100%    { top: 100px; }
}

/* From -> To */
@keyframes moveTop {
    from    { top: 0px; }
    to      { top: 100px; }
}

Once declared, you can apply them to any element with the animation CSS rule as well as setting up some parameters. For detailed options please check the official W3C documentation concerning CSS3 animations.

CSS animation syntax:

.animated { animation: moveTop 2s 3; }

This rule would play “moveTop” keyframes three times on a timescale of 2 seconds.

The Markup

First we’re going to write our HTML. We start with a navigation that includes four button elements. Below there are four section elements which will be used as overlays.

<!-- Navigation -->
<nav class="nav clearfix">

    <button class="nav-el" id="el-topleft" data-id="ov-topleft">
        <span class="icon-heart"></span>
    </button>

    <button class="nav-el" id="el-topright" data-id="ov-topright">
        <span class="icon-cloud"></span>
    </button>

    <button class="nav-el" id="el-btmleft" data-id="ov-btmleft">
        <span class="icon-location"></span>
    </button>

    <button class="nav-el" id="el-btmright" data-id="ov-btmright">
        <span class="icon-search"></span>
    </button>

</nav>

<!-- Overlays -->
<section class="overlay" id="ov-topleft">

    <div class="wrap">

        <h1>Section 1</h1>
        <p>Lorem ipsum dolor sit amet...</p>

    </div>

    <button class="close"><span class="mfg-cancel"></span></button>

</section>

<section class="overlay" id="ov-topright">

    <div class="wrap">

        <h1>Section 2</h1>
        <p>Lorem ipsum dolor sit amet...</p>

    </div>

    <button class="close"><span class="mfg-cancel"></span></button>

</section>

<section class="overlay" id="ov-btmleft">

    <div class="wrap">

        <h1>Section 3</h1>
        <p>Lorem ipsum dolor sit amet...</p>

    </div>

    <button class="close"><span class="mfg-cancel"></span></button>

</section>

<section class="overlay" id="ov-btmright">

    <div class="wrap">

        <h1>Section 4</h1>
        <p>Lorem ipsum dolor sit amet...</p>

    </div>

    <button class="close"><span class="mfg-cancel"></span></button>

</section>

As you can see, we are using a custom data attribute data-id on our buttons to “link” them to their respective overlay containers. We will handle this later with Javascript. For now, focus on the fact that each button gets a specific id, same thing for overlays.

Default CSS Styles

Now our HTML is ready, let’s define our layout appearance with some basic CSS rules for the nav and each .nav-el. Remember, we’re building four fluid squares with a big icon inside. We’re also adding rules for .active.active_reverse and .inactive, CSS classes we will use later.

/* Main Nav */
.nav {
    width: 80%;
    max-width: 420px;
    margin: 90px auto 90px;
    font-size: 33px;
}
/* Nav elements */
.nav-el {
    position:relative;
    display: inline-block;
    float: right;
    width: 47.5%;
    padding-bottom: 47.5%;
    color: white;
    background-color: #16a085;
    transition: background-color .3s ease-in;
    z-index: 10;
}

.nav-el:hover, .nav-el.active {
    background-color: #107360;
}

.nav-el.active_reverse {
    background-color: transparent;
}

/* Active button always on top */
.nav-el.active, .nav-el.active_reverse {
    z-index: 11;
}

/* Prevent click/hover on inactive buttons */
.nav-el.inactive {
    pointer-events: none;
    cursor: default;
}

/* Specific floating and margin */
.nav-el:nth-of-type(2n+1) { float: left; }
.nav-el:nth-of-type(n+3) { margin-top: 5%; }

/* Icons are horizontally/vertically centered */
[class^="icon-"], [class*=" icon-"] {
    position: absolute;
    display: inline-block;
    top: 50%;
    left: 50%;
    line-height: 0;
    width: 1em;
    margin-left: -0.5em;
}

Each overlay is set with a fixed position, and is visibly hidden by default.

/* Overlay */
.overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    overflow: auto;
    z-index: 9999;
    visibility: hidden;
}

.close {
    position: absolute;
    top: 50px;
    right: 50px;
    font-size: 36px;
}

An additional class to hide overflow on body, you’ll understand its purpose later.

.noscroll {
    overflow: hidden;
    height: 100%;
    width: 100%;
}

CSS3 Animations

Active States

This is the heart of our experience, we define one-time animations on a 1.6 second timescale for all needed elements of our page:

  • Each button when clicked
  • Each button when an other one is clicked
  • Overlay related to the clicked button

When moving an element with CSS animation or even transition, always use translate3d so your users will take advantage of their device’s hardware acceleration. If you move some elements with left/right and absolute positioning instead, you will probably encounter performance losses.

Let’s start with animations we add to buttons when clicked. Each button moves two times with translate3d, then the icon inside disappears while the button scales up.

@keyframes fx-el_topleft-active {
    0%   {}
    16%  { transform: translate3d(-27.5%, -27.5%, 0); }
    50%  { transform: translate3d(55.1%, 55.1%, 0) scale(1); color: #FFF;}
    62%  { color: transparent;  }
    100% { transform: translate3d(55.1%, 55.1%, 0) scale(20); color: transparent; }
}
@keyframes fx-el_topright-active {
    0%   {}
    16%  { transform: translate3d(27.5%, -27.5%, 0); }
    50%  { transform: translate3d(-55.1%, 55.1%, 0) scale(1); color: #FFF;}
    62%  { color: transparent;  }
    100% { transform: translate3d(-55.1%, 55.1%, 0) scale(20); color: transparent; }
}
@keyframes fx-el_btmleft-active {
    0%   {}
    16%  { transform: translate3d(-27.5%, 27.5%, 0); }
    50%  { transform: translate3d(55.1%, -55.1%, 0) scale(1); color: #FFF;}
    62%  { color: transparent;  }
    100% { transform: translate3d(55.1%, -55.1%, 0) scale(20); color: transparent;}
}
@keyframes fx-el_btmright-active {
    0%   {}
    16%  { transform: translate3d(27.5%, 27.5%, 0); }
    50%  { transform: translate3d(-55.1%, -55.1%, 0) scale(1); color: #FFF;}
    62%  { color: transparent;  }
    100% { transform: translate3d(-55.1%, -55.1%, 0) scale(20); color: transparent; }
}

#el-topleft.active {
    animation: fx-el_topleft-active 1.6s 1 ease-in-out;
}
#el-topright.active {
    animation: fx-el_topright-active 1.6s 1 ease-in-out;
}
#el-btmleft.active {
    animation: fx-el_btmleft-active 1.6s 1 ease-in-out;
}
#el-btmright.active {
    animation: fx-el_btmright-active 1.6s 1 ease-in-out;
}

We follow with inactive button animations. They are moving the same way before ultimately disappearing with opacity: 0.

@keyframes fx-el_topleft-inactive {
0% { transform: translate3d(0%, 0%, 0); opacity: 1; }
16% { transform: translate3d(-27.5%, -27.5%, 0);opacity: 1; }
40% { opacity: 0;}
50% { transform: translate3d(55.1%, 55.1%, 0); }
100% { transform: translate3d(55.1%, 55.1%, 0); opacity: 0; }
}
@keyframes fx-el_topright-inactive {
0% { transform: translate3d(0%, 0%, 0); opacity: 1; }
16% { transform: translate3d(27.5%, -27.5%, 0); opacity: 1; }
40% { opacity: 0;}
50% { transform: translate3d(-55.1%, 55.1%, 0); }
100% { transform: translate3d(-55.1%, 55.1%, 0); opacity: 0; }
}
@keyframes fx-el_btmleft-inactive {
0% { transform: translate3d(0%, 0%, 0); opacity: 1; }
16% { transform: translate3d(-27.5%, 27.5%, 0); opacity: 1; }
40% { opacity: 0;}
50% { transform: translate3d(55.1%, -55.1%, 0); }
100% { transform: translate3d(55.1%, -55.1%, 0); opacity: 0; }
}
@keyframes fx-el_btmright-inactive {
0% { transform: translate3d(0%, 0%, 0); opacity: 1; }
16% { transform: translate3d(27.5%, 27.5%, 0); opacity: 1; }
40% { opacity: 0;}
50% { transform: translate3d(-55.1%, -55.1%, 0); }
100% { transform: translate3d(-55.1%, -55.1%, 0); opacity: 0; }
}

#el-topleft.inactive {
animation: fx-el_topleft-inactive 1.6s 1 ease-in-out;
}
#el-topright.inactive {
animation: fx-el_topright-inactive 1.6s 1 ease-in-out;
}
#el-btmleft.inactive {
animation: fx-el_btmleft-inactive 1.6s 1 ease-in-out;
}
#el-btmright.inactive {
animation: fx-el_btmright-inactive 1.6s 1 ease-in-out;
}

Then the overlay, easy to understand.

@keyframes fx-overlay {
    0%   { visibility: visible; color: transparent; }
    50%  { background-color: #107360; color: white; }
    100% { visibility: visible; background-color: #107360; color: #FFF; }
}

.overlay.active {
    animation: fx-overlay .8s 1.25s 1 ease-in-out forwards;
}

We delay the overlay’s animation to 1.25 seconds, to let its content appear at the end of our button animations. We are also using forwards, an additional parameter, which will apply the property values for the time the animation ended (in this case, keep the overlay visible).

Active_reverse States

At this point, we defined how our buttons and their respective overlays will be animated when clicking on a buttonelement. Now when a user clicks on the .close icon inside the current overlay, we will repeat the same process in reverse. The current overlay will disappear and the menu will go back to its initial position.

If you read the CSS animation specifications, you may be thinking about defining .active_reverse classes, using previous animations with an extra reverse parameter. reverse allows us to start a keyframe from the end (100%) to the start (0%). The problem is that we would re-use an animation that should happen once (remember our “1” next to the “1.6s”) and that has already happened. Sadly, this would not produce any result.

To work around this problem, define new keyframes and simply reverse previous rules within.

Active button reverse animation:

@keyframes fx-el_topleft-active_reverse {
    0%   { transform: translate3d(55.1%, 55.1%, 0) scale(20);
            color: transparent; background-color: #107360; }
    38%  { color: transparent;}
    50%  { transform: translate3d(55.1%, 55.1%, 0) scale(1); color: #FFF; background-color: #107360; }
    82%  { transform: translate3d(-27.5%, -27.5%, 0); background-color: #16a085;}
    100% { transform: translate3d(0%, 0%, 0); }
}
@keyframes fx-el_topright-active_reverse {
    0%   { transform: translate3d(-55.1%, 55.1%, 0) scale(20);
            color: transparent; background-color: #107360; }
    38%  { color: transparent; }
    50%  { transform: translate3d(-55.1%, 55.1%, 0) scale(1); color: #FFF; background-color: #107360;}
    82%  { transform: translate3d(27.5%, -27.5%, 0); background-color: #16a085;}
    100% { transform: translate3d(0%, 0%, 0); }
}
@keyframes fx-el_btmleft-active_reverse {
    0%   { transform: translate3d(55.1%, -55.1%, 0) scale(20);
            color: transparent; background-color: #107360; }
    38%  { color: transparent; }
    50%  { transform: translate3d(55.1%, -55.1%, 0) scale(1); color: #FFF; background-color: #107360;}
    82%  { transform: translate3d(-27.5%, 27.5%, 0); background-color: #16a085;}
    100% { transform: translate3d(0%, 0%, 0); }
}
@keyframes fx-el_btmright-active_reverse {
    0%   { transform: translate3d(-55.1%, -55.1%, 0) scale(20);
            color: transparent; background-color: #107360; }
    38%  { color: transparent; }
    50%  { transform: translate3d(-55.1%, -55.1%, 0) scale(1); color: #FFF; background-color: #107360;}
    82%  { transform: translate3d(27.5%, 27.5%, 0); background-color: #16a085;}
    100% { transform: translate3d(0%, 0%, 0); }
}

#el-topleft.active_reverse {
    animation: fx-el_topleft-active_reverse 1.6s 1 ease-in-out;
}
#el-topright.active_reverse {
    animation: fx-el_topright-active_reverse 1.6s 1 ease-in-out;
}
#el-btmleft.active_reverse {
    animation: fx-el_btmleft-active_reverse 1.6s 1 ease-in-out;
}
#el-btmright.active_reverse {
    animation: fx-el_btmright-active_reverse 1.6s 1 ease-in-out;
}

Inactive button reverse animation:

@keyframes fx-el_topleft-inactive_reverse {
    0%   { transform: translate3d(55.1%, 55.1%, 0);  opacity: 0; }
    50%  { transform: translate3d(55.1%, 55.1%, 0); }
    82%  { transform: translate3d(-27.5%, -27.5%, 0); }
    45%  { opacity: 0; }
    100% { transform: translate3d(0%, 0%, 0); opacity: 1; }
}
@keyframes fx-el_topright-inactive_reverse {
    0%   { transform: translate3d(-55.1%, 55.1%, 0);  opacity: 0; }
    50%  { transform: translate3d(-55.1%, 55.1%, 0); }
    82%  { transform: translate3d(27.5%, -27.5%, 0); }
    45%  { opacity: 0; }
    100% { transform: translate3d(0%, 0%, 0);opacity: 1; }
}
@keyframes fx-el_btmleft-inactive_reverse {
    0%   { transform: translate3d(55.1%, -55.1%, 0); opacity: 0; }
    50%  { transform: translate3d(55.1%, -55.1%, 0); }
    82%  { transform: translate3d(-27.5%, 27.5%, 0); }
    45%  { opacity: 0; }
    100% { transform: translate3d(0%, 0%, 0);opacity: 1; }
}
@keyframes fx-el_btmright-inactive_reverse {
    0%   { transform: translate3d(-55.1%, -55.1%, 0); opacity: 0; }
    50%  { transform: translate3d(-55.1%, -55.1%, 0); }
    82%  { transform: translate3d(27.5%, 27.5%, 0); }
    45%  { opacity: 0; }
    100% { transform: translate3d(0%, 0%, 0); opacity: 1; }
}

#el-topleft.inactive_reverse {
    animation: fx-el_topleft-inactive_reverse 1.6s 1 ease-in-out;
}
#el-topright.inactive_reverse {
    animation: fx-el_topright-inactive_reverse 1.6s 1 ease-in-out;
}
#el-btmleft.inactive_reverse {
    animation: fx-el_btmleft-inactive_reverse 1.6s 1 ease-in-out;
}
#el-btmright.inactive_reverse {
    animation: fx-el_btmright-inactive_reverse 1.6s 1 ease-in-out;
}

Overlay reverse animation:

@keyframes fx-overlay-reverse {
    0% { visibility: visible; background-color: #107360; color: #FFF;}
    40% { background-color: #107360; color: transparent;}
    85% {background-color: transparent; }
    100% {visibility: invisible; color: transparent; background-color: transparent; }
}

.overlay.active_reverse {
    animation: fx-overlay-reverse .8s 1 ease-in backwards;
}

Backwards is similar to forwards as the animation will apply the values you defined in the first keyframe as soon as it is applied to the target.

The Javascript

All needed components are now set up and ready to use. We just need to add our previous CSS classes for defined events. I used jQuery for that but you can do it with pure javascript to keep it light. On a button click event, we add .activeclasses on the current button and its related .overlay, then .inactive classes on the other buttons using siblings(). We also hide all overflow on body produced by our scaled up button with the .noscroll class. When closing the overlay, we repeat the same process using our *_reverse classes which brings back users to the default menu, then remove .noscroll class on body.

To link each button with its overlay, we simply store the data-id value in a variable, then add the .active class on the overlay with a corresponding id.

Did you know that an event is fired each time an animation start or complete? We will listen to animationend event to remove the .noscroll class on body, and .inactive classes on button elements. This prevents scrollbar from appearing when a button scales from 20x to 1x and becomes temporarily larger than the body size, creating something messy in the browser. It also prevents a button from being clicked when the menu isn’t in its initial state.

var AnimEnd   = 'animationend webkitAnimationEnd mozAnimationEnd MSAnimationEnd oAnimationEnd';
var nav       = $('.nav');
var navButton = $('.nav-el');
var overlay   = $('.overlay');

$(navButton).click(function() {

/* Remove old previous classes */
    $(navButton).removeClass('inactive_reverse active_reverse');
    $(overlay).removeClass('active active_reverse');

/* Add classes on menu elements */
    $(this).siblings().addClass('inactive');
    $(this).addClass('active');

/* Activate related overlay */
    var o_target = $(this).data('id');
    $('#'+o_target).addClass('active');

/* Prevent scrolling */
    $('body').addClass('noscroll');

});

$('.close').click(function(){

/* Remove old classes and add new ones on menu elements */
    $('.active', nav).removeClass('active').addClass('active_reverse');
    $('.inactive', nav).addClass('inactive_reverse');

/* Bring back our overlay to its default state */
    $(this).parent().addClass('active_reverse');

/* Remove .noscroll and .inactive when reverse animation is finished */
    $('.inactive_reverse').bind(AnimEnd, function(){
        $('body').removeClass('noscroll');
        $(navButton).removeClass('inactive');
        $('.inactive_reverse').unbind(AnimEnd);
    });

});

That’s all. You can re-use this process in a lot of different ways, with your own layouts and other kinds of effects. So be creative!

Notes

Vincent Tantardini

Vincent Tantardini is a french UI/UX designer and front-end developer fascinated about minimal and clean pixels as well as lightweight and portable code. He works on amazing websites and apps projects, keeping an eye on fresh innovations. Check out his work on dribbble, vtcreative.fr, Google+ or follow @vtcreative.

Newsletter

16 Comments
  1. Egorkubasov Mar 28, 11:42 am

    Sweet!
    Thank you, Vincent!

    Reply
    +2
  2. Bionik6 Mar 28, 12:04 pm

    Merci pour ce beau travail Vincent, Well Done ;)

    Reply
    +2
  3. Eduardo Grillo Mar 28, 2:35 pm

    Wow, this is amazing :) Thank you!

    Reply
    0
  4. Marcos Mar 28, 6:25 pm

    Muy bueno, gracias!

    Reply
    0
  5. Tanya Mar 28, 9:00 pm

    This is awesome, thanks for the wonderful write up!

    Reply
    0
  6. Vincent Tantardini Mar 29, 1:48 am

    I’m glad you guys appreciate it, thanks for your words.

    Reply
    +6
  7. Michael Seidl Mar 29, 6:03 am

    Beautiful work, thanks Vincent – greetings from Australia :)

    Reply
    0
  8. Martin Mar 29, 12:15 pm

    Hi,

    This is a great article, however you can improve the javascript:
    1. Use a single var declaration
    2. Once you assigned the var navButton = $(‘.nav-el’); no need to converting to jquery object again:
    $(navButton).removeClass(‘inactive_reverse active_reverse’);
    Use it like that:
    navButton.removeClass(‘inactive_reverse active_reverse’);

    Keep up with these great articles.
    Regards,
    Martin

    Reply
    +3
  9. Vincent Tantardini Mar 30, 3:35 am

    Thanks to both of you. I appreciate your good feedbacks Martin. Have a nice end of week.

    Reply
    +2
  10. Suarez Fabian Apr 1, 1:33 pm

    Excellent article, very good. To practice

    Reply
    0
  11. Oliver Chalmers Apr 2, 3:30 am

    Great article. Lovely clean and clever CSS. Keep them coming!

    Reply
    0
  12. Subodh Kumar Apr 3, 10:53 pm

    Loved the way you explained..awesome!!!

    Reply
    0
  13. miguel Apr 10, 7:09 pm

    Testing on an android device… 2gb ram and a powerful gpu… Animations has a very noticeable lag

    Reply
    0
  14. KJ Apr 12, 6:02 am

    Is it possible to instead of the icons have my own pictures? How do I achieve this?

    Reply
    0
  15. Andra Apr 12, 6:30 am

    how to add redirect page after the animation?

    Reply
    +1
  16. Jak Jun 4, 9:30 pm

    In love with these – they’re great!

    Intrigued as to how much work it’d take to make this work dynamically.

    Reply
    +1

Leave a Reply

*
* Minimum length: 20 characters