Monday, April 21, 2014

Using CSS3 Transitions to Draw a User's Attention to Change

I've been pondering how to draw a user's attention to a specific area of the page for a little while. I have a application that displays a wealth of information on the screen and receives real-time updates over a Socket.IO connection. The data isn't sorted by recent activity and the user is not initiating the reload of the data so how do you guide a user's eye to something that is currently important? Momentarily highlighting the changed area in color may be a good way to approach the problem. However, I found that it was not always obvious, especially, when color was already being utilized in several other ways to highlight importance. What I really wanted was everything else to fade out a bit to single out the updated item and, to emphasize the change more, apply some kind of effect to that item (color, animation, etc).

As a simple demonstration of what I'm trying to achieve, the following example organizing several content boxes of information. Each item is selected randomly to receive an "update" which simply changes the text and resets the time-since-last-updated counter. A timer generates consistent events and initiates the updating logic:







The key part the solution is using a overlay div with an opacity set to "dim" out the content that did not update while adjusting the z-index of the item that did update to pull it out behind the overlay. From there, its a matter of adding any transitions you'd like to create the extra effects on the updated item.

Setup

The first step is to get everything in an initial, normal mode. The markup is pretty straight forward with a container, content boxes, and the overlay:

<div class="container">
<div class="content"></div>
<div class="content"></div>
<div class="content"></div>
<div class="content"></div>
<div class="content"></div>
<div class="content"></div>
<div class="content"></div>
<div class="content"></div>

<div class="blocker"></div>
</div>


In this example, I kept it simple and created eight content boxes. The next step is to style the markup - the important part here is how the blocker element is defined:

.container {
   position: relative;
}

.container:before, .container:after {
   content: " ";
   display: table;
}

.container:after {
   clear: both;
}

.content {
   position: relative;
   float: left;
   background-color: #FFFFFF;
   border: 1px solid;
   margin: 10px;
   padding: 5px 10px;
   width: 250px;
   transition: transform 400ms;
}

.blocker {
   display: none;
   position: absolute;
   top: 0;
   left: 0;
   right: 0;
   bottom: 0;
   background-color: #fff;
   opacity: 0.7;
   transition: opacity 400ms;
}


The overlay starts hidden and at the desired opacity. Note also that I've defined the transitions on both the content and blocker classes. These will cause the properties identified in the style to animate when they get changed by adding other classes to the elements later.

The final setup requires a little Javascript to initialize the content blocks, cache the updater element, and record the current time so it can be used to calculate and set the time-since-last-updated information:


   var timer = [];

   $( '.container' )
      .children( '.content' ).html( '<div>'+words[0]+'</div><div>Now</div>' )
         .each(function( z ) {

               var $el = $( this );

               timer[z] = {
                  $time: $el.children().last(),
                  last: new Date() 
               };

            });



Since I'll be updating the time every second, caching the element makes a lot of sense to keep things snappy.

Transition 1: Highlight the update

The next step is to simulate a stream of updates and, as they are received, highlight the item that updated. Using setInterval, I randomly select some text and an item to update. I then add classes to the selected item and overlay element to dim the other content and highlight the updated element:


// Bring the updated item over the overlay element
.standout {
   z-index: 10;
}

// Effect - animate this
.bigger {
   transform: scale(1.1);
}

// Show the overlay (.blocker)
.in {
   display: block;
}


The challenge with the effect is timing the animation sequences with showing the elements and then eventually resetting back to the normal state. While the transitions can be defined in the CSS, you don't want the overlay hovering over the content all the time. It will act as a click trap making it impossible for your user to interact with the content. For the opacity transition to work, the overlay has to be visible and the transition has to finish before removing it. The Javascript acts as the glue to coordinate the effects and trigger them at the correct time:

   
   // Fictitious update stream
   setInterval(function() {
      
      // Randomize
      var i = Math.floor( Math.random() * 10 % 8 ),
          j = Math.floor( Math.random() * 10 % 5 );
      
      // Update the content and apply classes to bring forward and
      // animate the effect
      $( '.container' )
         .children().eq( i ).addClass( 'standout ' + effect )
            .children().first().html( words[j] );
      
      // Show the overlay
      $( '.blocker' ).addClass( 'in' );
      
      // The updated since text is handled in another timer
      // just reset the last time here
      timer[i].last = new Date();
      
      // Step 2 will go here ...
   }, 2600);


At this point, the overlay is in place and the updated content has transitioned through its effect and is visible above the overlay element. Next, we need to transition back to the initial state.

Transition 2: Remove the highlight

Returning the content box back to its start state is straight forward since I only want to reverse what I did to highlight it. Those classes can simply be removed and it will transition back to its original state. However, the overlay started with its opacity set to the desired opaqueness. Instead of just removing it, I want it to fade out first. This means that I have to trigger the transition by adding another class, then wait for the transition to complete, and finally remove all the classes to reset it back to the initial state. Adding this class will cause the overlay to fade out:

.out {
   opacity: 0;
}


And this Javascript ties everything together:


    // Inside the setInterval 
    // at the "Step 2 will go here" point above
      setTimeout(function() {
         // Kick off the fade out transition
         $( '.blocker' ).addClass( 'out' );
         
         // Animate back to normal
         $( '.container' )
            .children()
                  .eq( i ).removeClass( 'standout ' + effect );
         
         // Allow the fade to complete
         // and reset the overlay to be hidden
         setTimeout(function() {
            $( '.blocker' ).removeClass( 'in out' );
         }, 400);

      }, 800);



The only missing piece is the updates to the time since last update content. This runs inside a separate interval to read the last property that is set in the "update stream" interval to calculate and update the HTML:

   // Update the since-last-updated timer
   setInterval(function() {
         var now = new Date();

         $.each( timer, function() { this.$time.html( Math.round((now - this.last) / 1000) + 's ago' ); });
   }, 1000);


What's nice about this approach is the content remains inline with the surrounding content. It will be responsive and there's no extra effort required to clone the HTML, position it, and then remove it when effect is complete. I created a jsFiddle using a shake effect I found from this Gist. If you want some other fancy shaking, take a look at the CSS Shake project. You can drop in any animation CSS you'd like and then swap out the class set on the effect variable near the top of the JS to use it.