Detecting if the user is idle with JavaScript and YUI 3

Web developers have been interested in whether or not a user is idle since the Ajax explosion hit. With the introduction of more dynamic, highly interactive web interfaces came the desire to know if the user was actually doing anything at any point in time. Thus, the quest for determining if the user is idle began.

This problem has been solved, though I would argue inelegantly, in a lot of web applications: Facebook, WordPress, and Gmail all try to figure out when the user has stopped interacting with the page in order to perform some action. The usual JavaScript solution for this involves monitoring the mousemove event and, if there has been no mouse movement in a specific period of time, indicate that the user is idle. There is one major flaw in this approach and that is reliance on mouse events to indicate if the user is active or idle. This is problematic because there are, of course, two primary input devices (keyboard and mouse) attached to a computer so you’re losing 50% of the picture. If a user is typing a long email or blog post, does that mean they are idle simply because they haven’t moved the mouse? Of course not. What about those users who aren’t capable of using a mouse due to a disability, are they always idle? Once again, the answer is no.

With this background in mind, I set out to create an idle timer in JavaScript that is fitting of the complex web applications that might want to use it. I built this implementation on top of YUI 3 because it has, in a short period of time, become my favorite JavaScript library. The features I wanted to implement were:

  1. Allow the idle timer to be started and stopped for proper cleanup of resources.
  2. Fire an event when the user becomes idle.
  3. Fire an event when the user becomes active after having been idle.
  4. Provide a function so I could determine, at any point in time, if the user is idle.

These features led me to a basic interface:

Y.IdleTimer = {

    isRunning: function(){
    },

    isIdle: function(){
    },

    start: function(newTimeout){
    },

    stop: function(){
    }

};

I decided to use the YUI 3 Event utility to provide custom event support for this implementation. This is done by augmenting the Y.IdleTimer object with Y.Event.Target:

//inherit event functionality
Y.augment(Y.IdleTimer, Y.Event.Target);

This line adds basic event methods, such as fire(), subscribe(), and unsubscribe(). Using Y.Event.Target, the creation and management of custom event objects are done for you, freeing you up to focus on implementation details.

Next, I created a couple of flags: idle, which indicates if the user is idle, and enabled, which is indicates if the timer is running. These are used internally to manage the state of the timer and are returned in isIdle() and isRunning(), respectively. I also created tId, which is a place to store the timer ID when using setTimeout() and timeout, which indicates the default amount of time to wait before declaring the user idle (set to 30,000ms initially, this can be overidden by passing a value into start()).

To manage the user’s idle state, you need to attach an event handler for both mousemove and keydown. Since both of these methods bubble, I can attach the handler at the document level to manage the entire page (of course, this presupposes that no one stops bubbling before it reaches the document level). The event handler should be the same for both events so there’s no duplication of code and the handler will have to manage the timeout process. I ended up creating two functions for this purpose:

//event handler
function handleUserEvent(){

    //clear any existing timeout
    clearTimeout(tId);

    //if the idle timer is enabled
    if (enabled){

        //if it's idle, that means the user is no longer idle
        if (idle){
            toggleIdleState();
        } 

        //set a new timeout
        tId = setTimeout(toggleIdleState, timeout);
    }
}

//helper to fire events
function toggleIdleState(){

    //toggle the state
    idle = !idle;

    //fire appropriate event
    Y.IdleTimer.fire(idle ? "idle" : "active");
}

The first function handleUserEvent() is assigned to be the event handler for mousemove and keydown. It doesn’t actually use the event object for anything, so I didn’t bother defining it as an argument. Whenever the user does something, the last timer should be cleared and then an appropriate action should be taken. If the timer is stopped, then nothing happens; if it’s running, then the action is determined based on the user’s current idle state. If the user is idle, then toggleIdleState() state is called immediately to indicate that the user is not active. Then, a timer is used to delay calling toggleIdleState() because the next toggle would be back to idle.

The toggleIdleState() function simply toggles the idle flag and then fires an appropriate event. If the user is idle after the toggle, then “idle” is fired, otherwise “active” is fired. These events end up being fired exactly when the user’s idle state has changed and only once until the state changes again.

To finish up the implementation, I just filled out the existing interface skeleton to make use of these functions:

Y.IdleTimer = {
    isRunning: function(){
        return enabled;
    },

    isIdle: function(){
        return idle;
    },

    start: function(newTimeout){

        //set to enabled
        enabled = true;

        //set idle to false to begin with
        idle = false;

        //assign a new timeout if necessary
        if (typeof newTimeout == "number"){
            timeout = newTimeout;
        }

        //assign appropriate event handlers
        Y.on("mousemove", handleUserEvent, document);
        Y.on("keydown", handleUserEvent, document);

        //set a timeout to toggle state
        tId = setTimeout(toggleIdleState, timeout);
    },

    stop: function(){

        //set to disabled
        enabled = false;

        //clear any pending timeouts
        clearTimeout(tId);

        //detach the event handlers
        Y.detach("mousemove", handleUserEvent, document);
        Y.detach("keydown", handleUserEvent, document);
    }

};

//inherit event functionality
Y.augment(Y.IdleTimer, Y.Event.Target);

Basic usage of the idle timer is as follows:

Y.IdleTimer.subscribe("idle", function(){
    //handle when the user becomes idle
});

Y.IdleTimer.subscribe("active", function(){
     //handle when the user becomes active
});

//start the timer with a default timeout of 30s
Y.IdleTimer.start();

Because of the power of YUI 3, this implementation of an idle timer is very small in size and pretty straightforward to use. You can get the full source code up on GitHub, and there’s an example to play with as well.

Update (6-June-09): Updated logic per Paul’s feedback.

**Update (21-June-09):**YUI 2 and generic versions of the idle timer are now available at my GitHub project.

Update (28-Oct-09): YUI 3 IdleTimer is now part of YUI Gallery (more info).

Comments

  1. Paul Irish

    Good show! I've wanted to capture idle-ness for a while. Might be fun to use this in combo with Andrea's CPU usage script to estimate machine/user idle-ness.

    Btw, I made a quick and dirty jQuery adaptation. Hope that's cool.

  2. Mike Lee

    Good article! I like that you started out with a basic interface. Starting off with a good design is a technique every solid programmer should follow.

    I never looked into the code on Facebook, WordPress, and Gmail - but do they all monitor mousemove as the basis of detecting whether a user is idle?

  3. Nicholas C. Zakas

    @Paul - Of course I don't mind porting to jQuery. It's MIT-licensed, so have at it!

    @Mike - Glad you enjoyed it. I've not looked at the code on those sites, mostly because I have better things to do then reverse-engineer someone else's code to figure out what's going on. However, a quick search for this type of functionality turns up a lot of solutions based on mousemove.

  4. Sean O Shea

    Hey Nick,

    Great post - really useful stuff. I poll quite a bit on the site I work on, so this will be useful for saving all those unnecessary network calls. Gave a try at porting the logic to dojo too.

    Sean

  5. Nicholas C. Zakas

    Hi Sean, very cool! Looks like we now have this in YUI, jQuery, and Dojo. This is the beauty of MIT-licensed code, very exciting!

  6. Paul Irish

    Btw a user found a logic bug in mine that is likely in yours as well.

    On the example, wait for idle then hit a single keyboard key. The setTimeout isn't restarted so it never goes idle again. My fix just took the setTimeout call outside of the else. Cheers.

  7. Nicholas C. Zakas

    Thanks for the update, Paul, I'll fix that in my version as well.

  8. Qasim

    Good post with great thought on the issue with gmail and facebook user idle detection. Highlighting your point of detecting if user id idle or not, what if user is reading something and moving mouse around as some people have this habbit of moving mouse while reading. What if you have used keyboard and mouse click combination.

  9. Nicholas C. Zakas

    @Qasim - the definition of "idle" in this case refers to the user having the page open but is not actively using it. If someone is reading and moving their mouse around, then I wouldn't consider this user to be "idle"...because they're not. They're still using the page. Using mouse clicks instead of mouse moves changes the definition of "active" to be more restrictive and I believe to restrictive to be useful.

  10. John

    Just out of curiosity...what's tilted the scales in making YUI 3 your favorite Javascript library? I know DOM manipulation seems much easier in YUI 3 than 2. Do you like it better than jQuery?

  11. Nicholas C. Zakas

    @John - To be honest, I've never really been a fan of jQuery for enterprise JavaScript application development. Even before I worked at Yahoo!, I preferred YUI's approach to grouping functionality rather than jQuery's DOM-wrapping and selector-based approach. That being said, I like YUI 3 better than YUI 2. :)

  12. Sandeep

    Any plans for YUI 2.6.*?
    We have been using YUI for quite a while. We don't want to switch to YUI 3 soon.

    Regards,
    Sandeep

  13. Nicholas C. Zakas

    Sandeep - I've just uploaded a YUI 2 version to my GitHub project. Enjoy!

Understanding JavaScript Promises E-book Cover

Demystify JavaScript promises with the e-book that explains not just concepts, but also real-world uses of promises.

Download the Free E-book!

The community edition of Understanding JavaScript Promises is a free download that arrives in minutes.