Collision Detection

Avatar of Chris Coyier
Chris Coyier on

I posted about jQuery UI’s position feature years ago, but I was just thinking of how useful the collision detection part of that feature is. In a nutshell: you can position an element where you want them to go, but if it calculates that where you’re putting it would be offscreen or otherwise hidden, it will adjust the positioning to fix it.

Here’s an example from Disqus, where the default for this user popup is to open upwards, but if it would be hidden by the top of the screen, it opens downward instead.

I have no idea if they use jQuery UI or not (probably not).

I recently just did the same thing on CodePen, to ensure the settings dropdown area didn’t open below the bottom edge of the page, and instead opens upward.

We are using jQuery UI, because we are using it for a few other things anyway (dragging, resizing, potentially reordering in future).

Anytime an element opens directionally on a page, and you aren’t 100% sure that direction won’t be hidden, collision detection is a good idea.

Tooltips are a classic example. You might design a tooltip to open above and to the right of a link.

$("a[data-tooltip]")
  .on("mouseenter", function() {
     var whichTip = $(this).data("tooltip");
     $(".tooltip[data-tooltip=" + whichTip + "]")
       .position({
         my: "left bottom",
         at: "left top",
         of: $(this)
       })
       .show();
  });

(p.s. wouldn’t it be nice if we could do tooltips with HTML in them somehow without JS?)

But if that link was near the top of the page, that could easily open above the edge and be pretty useless:

Also could be dangerous along an edge, depending on how it’s designed:

jQuery UI actually defaults to having collision detection on, and it will “flip”, meaning:

Flips the element to the opposite side of the target and the collision detection is run again to see if it will fit. Whichever side allows more of the element to be visible will be used.

You have to explicitly turn that off if you want. Collision has a few other values as well, including fit which nudges the position until the element fits onto the page/area, or fitflip which does flip first then fit as needed.

That would look like this, with jQuery UI position:

$("a[data-tooltip]")
  .on("mouseenter", function() {
     var whichTip = $(this).data("tooltip");
     $(".tooltip[data-tooltip=" + whichTip + "]")
       .position({
         my: "left bottom",
         at: "left top",
         of: $(this),
         collision: "flipfit"
       })
       .show();
  });

Now, if the tooltip would open and be hidden by the top or edge, it’ll flip or move to fit:

How does it work? Math!

I’m sure you can imagine how. JavaScript knows the dimensions and position of all elements, and the viewport itself. So it can do a little check like: if an element of this size was at this position, does that fit within this area?

If you didn’t want to use jQuery UI, you could surely roll your own. One thing that I might recommend if you do: add class names to the element when it does a flip or fit. That’s lacking right now in jQuery UI, and you might need those class names to effect other elements in the tooltip. Say, to make sure a pointer arrow points to the right place.

Simple Demo

The container the collision detection works against doesn’t have to be the viewport, it can be a specified container. That’s what’s going on here, just to demonstrate:

See the Pen collision detection by Chris Coyier (@chriscoyier) on CodePen.

And the JS that hides the tooltips too:

$("a[data-tooltip]")
  .on("mouseenter", function() {
     var whichTip = $(this).data("tooltip");
     $(".tooltip[data-tooltip=" + whichTip + "]")
       .position({
         my: "left bottom",
         at: "left top",
         of: $(this),
         collision: "flip",
         within: ".page-wrap"
       })
       .show();
  })
  .on("mouseleave", function() {
    var whichTip = $(this).data("tooltip");
    $(".tooltip[data-tooltip=" + whichTip + "]")
      .stop()
      .fadeOut(function() {
        $(this).removeAttr("style");
      });
  });