Simple Tooltip for Huge Number of Elements

There are many, many jQuery tooltip plugins out there, and some of them are very good. But when someone on the jQuery Google Group asked (a year ago) which plugin could handle displaying tooltips for 2,000 links on a page, I wasn't able to find one. So, I decided to throw together a quick little plugin myself and was surprised by how easy it was.

Event Delegation, Again

The key to having JavaScript handle hundreds, or even thousands, of elements on a page is to use event delegation. As Louis-Rémi Babé described in Working with Events, Part 3: More Event Delegation with jQuery, jQuery's .live() method makes event delegation dead easy. A simple tooltip script using .live() might look something like this:

[js] $('
').hide().appendTo('body'); var tipTitle = ''; $('a').live('mouseover', function(event) { var $link = $(this); tipTitle = this.title; this.title = ''; $('#livetip') .css({ top: event.pageY + 12, left: event.pageX + 12 }) .html('
' + tipTitle + '
' + this.href + '
') .show(); }).live('mouseout', function(event) { this.title = tipTitle; $('#livetip').hide(); }); [/js]

This script does the following:

  • Creates a single tooltip element that will be shown and hidden as the user mouses over links (line 1)
  • Hides the tooltip and appends it to the body (line 1)
  • Changes the tooltip's contents according to the moused-over link's title and href attributes (lines 7 and 14)
  • Places the tooltip on the page 12px below and to the right of the cursor position at the time that it entered the link (lines 9–13)
  • Shows the tooltip (line 15)
  • Sets the link's title attribute to an empty string when the user mouses over the link; this prevents the browser's default tooltip from appearing (line 8)
  • Sets the link's title back to what it was originally and hides the tooltip when the user mouses out of the link (lines 16–19)

A Little Style

At minimum, the tooltip needs to have the position: absolute style declaration for it to be positioned correctly on the page, but I threw in a little extra CSS to make it look more appealing:

[css] #livetip { position: absolute; background-color: #cfc; border: 2px solid #c9c; border-radius: 4px; -webkit-border-radius: 4px; -moz-border-radius: 4px; } [/css]

Typically I like to include live demos directly in the blog entry, but since this one involves 2,000 links, I've set up separate demo pages. Check out the tooltip demo using .live(), or download the zip.

A Little Less Simple, A Little More Speedy

Using .live() in this way avoids binding event handlers directly to the thousands of links on the page. However, it doesn't prevent jQuery from searching the entire document for all of those links. After all, I'm still using the $('a') jQuery function. Also, at least for now, .live() binds events to document, forcing event bubbling all the way up the DOM each time. If there is a noticeable performance lag when the script is initially executed, it may help to use custom event delegation.

Update

As of jQuery 1.4.2, the .delegate() method can take the place of custom event delegation. As of jQuery 1.7, the .on() method can take the place of all other event methods, including .live() and .delegate().

Using custom event delegation, I bind the events to a containing table element:

[js]$('
').hide().appendTo('body'); var tipTitle = ''; $('#mytable').bind('mouseover', function(event) { var $link = $(event.target).closest('a'); if ($link.length) { var link = $link[0]; tipTitle = link.title; link.title = ''; $('#livetip') .css({ top: event.pageY + 12, left: event.pageX + 12 }) .html('
' + tipTitle + '
' + link.href + '
') .show(); } }).bind('mouseout', function(event) { var $link = $(event.target).closest('a'); if ($link.length) { $link.attr('title', tipTitle); $('#livetip').hide(); } });[/js]

With this approach, I needed to first make sure I was dealing with a link before I tried to do anything with it. Line 5 uses the .closest() method to select the <a> that is either the event.target or one of its ancestors. Line 6 checks the length property to see if any elements are included in the variable declared in line 5.

Note the use of .bind() instead of .live() and event.target instead of this. I could have used the .mouseover() and .mouseout() shortcut methods, but since I used .live() in the first example, it was easier to simply replace "live" with "bind." Also, you may be wondering why I used mouseover / mouseout when I could have used mouseenter / mouseleave or even the .hover() method (which uses mouseenter and mouseleave internally). The reason is that mouseenter and mouseleave prevent the event bubbling that this script relies on for its event delegation. Those two events would be triggered only when the mouse enters or leaves the full table, while mouseover and mouseout are triggered whenever the mouse enters or leaves any of the table's descendant elements, as well.

View the demo using custom event delegation, or download the zip.

One more Enhancement

Some people like having the tooltip move along with the cursor, as long as the mouse is over the element. Sometimes it's nice to be able move the tooltip a little if it initially overlaps something important. Adding the basic functionality is pretty straightforward. Just bind the mousemove event to the same table element:

[js]$('
').hide().appendTo('body'); var tipTitle = ''; $('#mytable').bind('mouseover', function(event) { var $link = $(event.target).closest('a'); if ($link.length) { var link = $link[0]; tipTitle = link.title; link.title = ''; $('#livetip') .css({ top: event.pageY + 12, left: event.pageX + 12 }) .html('
' + tipTitle + '
' + link.href + '
') .show(); } }).bind('mouseout', function(event) { var $link = $(event.target).closest('a'); if ($link.length) { $link.attr('title', tipTitle); $('#livetip').hide(); } }).bind('mousemove', function(event) { if ($(event.target).closest('a').length) { $('#livetip').css({ top: event.pageY + 12, left: event.pageX + 12 }); } });[/js]

With the addition of lines 25 through 32, as the mouse moves over the links, the tooltip's top and left style properties are continually updated based on the mouse's position.

Try the demo and watch the tooltip move along with the mouse position, or download the zip.

The Plugin

The plugin doesn't offer much more than the scripts in this post, but feel free to try it out. One difference is that it doesn't rely on the .closest() method, so it can be used with jQuery 1.2.6 if you're stuck with that version for some reason. The syntax is a little funky:

[js]$(document).ready(function() { $(containerElement).eztip(invokingElement, [optionsMap]); });[/js]

The containerElement selector provides context so you can specify where exactly the event handlers are bound (unlike .live(), which binds to the document). The first argument of .eztip() is required. It should be a selector that matches the elements for which you wish to show a tooltip. The second argument is the typical options object, allowing for a handful of objects that you might expect to find in a stripped down tooltip plugin.

Let's Make Better Mistakes Tomorrow

The tooltip scripts in this post work fine, but they repeat code where they really shouldn't. Stay tuned tomorrow for a brief tutorial on one way to avoid such code repetition.