Bubble Point Tooltips with CSS3 & jQuery

Published by Chris Coyier

I needed some tooltips for a thing. In looking around for a little inspiration, I didn't have to go further than my dock:


The inspiration: contextual menus from OS X dock

This is where I ended up:


View Demo   Download Files

The HTML: Keepin' it Clean

Links can have tooltips in HTML with no fancy coding whatsoever. Just give a link a title attribute.

<a href="#" title="Hi, I'm a tooltip thingy.">link</a>

Then when you hover over that link a second or two, you'll get the yellow box thingy:


Default tooltip in pretty much any browser.

Let's not mess with that. Our HTML will be exactly what is shown above.

The thing is, that yellow box thingy is absolutely unstyleable. Maybe someday it will be once shadow DOM stuff like that is specced out, but today, it's not possible in any browser. So we're gonna run some JavaScript which is going to yank out that attribute and make <div>'s with the same text, which we'll position/hide/show as needed.

The JavaScript: jQuery Plugin

As usual around these parts, we'll use jQuery. This is the perfect kind of thing to make into a plugin. You might want to use this functionality on an arbitrary collection of elements of your own choosing, as well as chain it, so the plugin pattern is ideal.

Calling it will be as simple as:

$("article a[title]").tooltips();

That will tooltip-ize all links with titles that happen to be inside <article> elements. You could make that selector whatever you want.

Our plugin will do this:

  1. Loop through each link
  2. Create a (hidden) div.tooltip for each of them, the text inside matching that links title attribute.
  3. Remove the links title attribute (only way to get rid of default yellow box popup
  4. When link is hovered on...
  5. Position the tooltip accordingly and slide it into visibility.
  6. When link is hovered off...
  7. Slide link out of visibility

Massive code dump, commented for your pleasure:

// IIFE to ensure safe use of $
(function( $ ) {

  // Create plugin
  $.fn.tooltips = function(el) {

    var $tooltip,
      $body = $('body'),
      $el;

    // Ensure chaining works
    return this.each(function(i, el) {

      $el = $(el).attr("data-tooltip", i);

      // Make DIV and append to page 
      var $tooltip = $('<div class="tooltip" data-tooltip="' + i + '">' + $el.attr('title') + '<div class="arrow"></div></div>').appendTo("body");

      // Position right away, so first appearance is smooth
      var linkPosition = $el.position();

      $tooltip.css({
        top: linkPosition.top - $tooltip.outerHeight() - 13,
        left: linkPosition.left - ($tooltip.width()/2)
      });

      $el
      // Get rid of yellow box popup
      .removeAttr("title")

      // Mouseenter
      .hover(function() {

        $el = $(this);

        $tooltip = $('div[data-tooltip=' + $el.data('tooltip') + ']');

        // Reposition tooltip, in case of page movement e.g. screen resize                        
        var linkPosition = $el.position();

        $tooltip.css({
          top: linkPosition.top - $tooltip.outerHeight() - 13,
          left: linkPosition.left - ($tooltip.width()/2)
        });

        // Adding class handles animation through CSS
        $tooltip.addClass("active");

        // Mouseleave
      }, function() {

        $el = $(this);

        // Temporary class for same-direction fadeout
        $tooltip = $('div[data-tooltip=' + $el.data('tooltip') + ']').addClass("out");

        // Remove all classes
        setTimeout(function() {
          $tooltip.removeClass("active").removeClass("out");
          }, 300);

        });

      });

    }

})(jQuery);

Couple things to note: 1) The final demo is going to have the tooltips animated a bit. None of that happens here in the JavaScript. Animations are design. Design is CSS. Thus, we do all that in the CSS. 2) One little design touch on these is that the tooltips slide in and slide out the same direction. In CSS, by just adding and removing a single class and using transitions, that's not really possible. The transition will run in reverse upon the classes removal and thus will slide out same direction it came in. By using a setTimeout, we can apply a temporary class, and animate that as well. Any more elegant ideas for that, let me know in the comments below.

A Semantic Bummer?

Adding divs to the bottom of the document feels like a bummer; just doesn't feel very semantic. They aren't connected to the links they came from in any meaningful way through HTML alone.

Also removing the title tag doesn't sit quite right with me. I've heard they don't do much for accessibility anyway, but still. I wish preventDefault() would prevent that from showing, but it does not.

Any ideas on that stuff, let's hear 'em in the comments below.

The CSS: Close Counts

Notice in the OS X screenshot at the top of this article, the background and borders themselves are a bit transparent. I was unable to achieve that here. Those things are possible in general, but the pointy arrow down is an additional element and how the borders and backgrounds connect to each other is just through overlapping and it looks bad with any transparency at all. So, solid colors, NBD.

Oftentimes pointy arrows like we have here are doable with no additional markup or images by using pseudo elements and CSS triangles. In our case, we are going to use a pseudo element, but we'll need a real element as well. The real element (the <span> that the JavaScript inserted in each tooltip <div>) serves as a positional box and does the cropping. The pseudo element is the actual pointer. A box styled the exact same way as the tooltip, only rotated 45deg and cropped out by it's parent. Here's the schematics:


The thing goes in the thing

And so:

.tooltip, .arrow:after {
  background: black;
  border: 2px solid white;
}

.tooltip {
  pointer-events: none;
  opacity: 0;
  display: inline-block;
  position: absolute;
  padding: 10px 20px;
  color: white;
  border-radius: 20px;
  margin-top: 20px;
  text-align: center;
  font: bold 14px "Helvetica Neue", Sans-Serif;
  font-stretch: condensed;
  text-decoration: none;
  text-transform: uppercase;
  box-shadow: 0 0 7px black;
}
.arrow {
  width: 70px;
  height: 16px;
  overflow: hidden;
  position: absolute;
  left: 50%;
  margin-left: -35px;
  bottom: -16px;
}
.arrow:after {
  content: "";
  position: absolute;
  left: 20px;
  top: -20px;
  width: 25px;
  height: 25px;
  -webkit-box-shadow: 6px 5px 9px -9px black,
                      5px 6px 9px -9px black;
  -moz-box-shadow: 6px 5px 9px -9px black,
                   5px 6px 9px -9px black;
  box-shadow: 6px 5px 9px -9px black,
              5px 6px 9px -9px black;
  -webkit-transform: rotate(45deg);
  -moz-transform:    rotate(45deg);
  -ms-transform:     rotate(45deg);
  -o-transform:      rotate(45deg);
}
.tooltip.active {
  opacity: 1;
  margin-top: 5px;
  -webkit-transition: all 0.2s ease;
  -moz-transition:    all 0.2s ease;
  -ms-transition:     all 0.2s ease;
  -o-transition:      all 0.2s ease;
}
.tooltip.out {
  opacity: 0;
  margin-top: -20px;
}

Note the .active class is what does the slide in animation and the .out class does the slide out.

Thanks

To Adrian Adkison for helping with some ideas and code along the way.

Fair Warning about Opera

Opera doesn't support pointer-events in CSS, so this whole demo will appear not to work. That's because the tooltips are actually positioned right on top of the links with just their opacity turned down to 0. If you want to fix it up in Opera, you'll need to literally display none/block them in the JavaScript, or use CSS to move them off the links far enough they don't impede clickability. If someone wants to make that change but have it otherwise function exactly as this demo is, I'm down to update it.

Demo / Download

View Demo   Download Files