Moving: mouseover/out, mouseenter/leave

Let’s dive into more details about events that happen when mouse moves between elements.

Mouseover/mouseout, relatedTarget

The mouseover event occurs when a mouse pointer comes over an element, and mouseout – when it leaves.

These events are special, because they have a relatedTarget.

For mouseover:

  • event.target – is the element where the mouse came over.
  • event.relatedTarget – is the element from which the mouse came.

For mouseout the reverse:

  • event.target – is the element that mouse left.
  • event.relatedTarget – is the new under-the-pointer element (that mouse left for).

In the example below each face feature is an element. When you move the mouse, you can see mouse events in the text area.

Each event has the information about where the element came and where it came from.

Result
script.js
style.css
index.html
container.onmouseover = container.onmouseout = handler;

function handler(event) {

  function str(el) {
    if (!el) return "null"
    return el.className || el.tagName;
  }

  log.value += event.type + ': ' +
    'target=' + str(event.target) +
    ', relatedTarget=' + str(event.relatedTarget) + "\n";
  log.scrollTop = log.scrollHeight;

  if (event.type == 'mouseover') {
    event.target.style.background = 'pink'
  }
  if (event.type == 'mouseout') {
    event.target.style.background = ''
  }
}
body,
html {
  margin: 0;
  padding: 0;
}

#container {
  border: 1px solid brown;
  padding: 10px;
  width: 330px;
  margin-bottom: 5px;
  box-sizing: border-box;
}

#log {
  height: 120px;
  width: 350px;
  display: block;
  box-sizing: border-box;
}

[class^="smiley-"] {
  display: inline-block;
  width: 70px;
  height: 70px;
  border-radius: 50%;
  margin-right: 20px;
}

.smiley-green {
  background: #a9db7a;
  border: 5px solid #92c563;
  position: relative;
}

.smiley-green .left-eye {
  width: 18%;
  height: 18%;
  background: #84b458;
  position: relative;
  top: 29%;
  left: 22%;
  border-radius: 50%;
  float: left;
}

.smiley-green .right-eye {
  width: 18%;
  height: 18%;
  border-radius: 50%;
  position: relative;
  background: #84b458;
  top: 29%;
  right: 22%;
  float: right;
}

.smiley-green .smile {
  position: absolute;
  top: 67%;
  left: 16.5%;
  width: 70%;
  height: 20%;
  overflow: hidden;
}

.smiley-green .smile:after,
.smiley-green .smile:before {
  content: "";
  position: absolute;
  top: -50%;
  left: 0%;
  border-radius: 50%;
  background: #84b458;
  height: 100%;
  width: 97%;
}

.smiley-green .smile:after {
  background: #84b458;
  height: 80%;
  top: -40%;
  left: 0%;
}

.smiley-yellow {
  background: #eed16a;
  border: 5px solid #dbae51;
  position: relative;
}

.smiley-yellow .left-eye {
  width: 18%;
  height: 18%;
  background: #dba652;
  position: relative;
  top: 29%;
  left: 22%;
  border-radius: 50%;
  float: left;
}

.smiley-yellow .right-eye {
  width: 18%;
  height: 18%;
  border-radius: 50%;
  position: relative;
  background: #dba652;
  top: 29%;
  right: 22%;
  float: right;
}

.smiley-yellow .smile {
  position: absolute;
  top: 67%;
  left: 19%;
  width: 65%;
  height: 14%;
  background: #dba652;
  overflow: hidden;
  border-radius: 8px;
}

.smiley-red {
  background: #ee9295;
  border: 5px solid #e27378;
  position: relative;
}

.smiley-red .left-eye {
  width: 18%;
  height: 18%;
  background: #d96065;
  position: relative;
  top: 29%;
  left: 22%;
  border-radius: 50%;
  float: left;
}

.smiley-red .right-eye {
  width: 18%;
  height: 18%;
  border-radius: 50%;
  position: relative;
  background: #d96065;
  top: 29%;
  right: 22%;
  float: right;
}

.smiley-red .smile {
  position: absolute;
  top: 57%;
  left: 16.5%;
  width: 70%;
  height: 20%;
  overflow: hidden;
}

.smiley-red .smile:after,
.smiley-red .smile:before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0%;
  border-radius: 50%;
  background: #d96065;
  height: 100%;
  width: 97%;
}

.smiley-red .smile:after {
  background: #d96065;
  height: 80%;
  top: 60%;
  left: 0%;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="container">
    <div class="smiley-green">
      <div class="left-eye"></div>
      <div class="right-eye"></div>
      <div class="smile"></div>
    </div>

    <div class="smiley-yellow">
      <div class="left-eye"></div>
      <div class="right-eye"></div>
      <div class="smile"></div>
    </div>

    <div class="smiley-red">
      <div class="left-eye"></div>
      <div class="right-eye"></div>
      <div class="smile"></div>
    </div>
  </div>

  <textarea id="log">Events will show up here!
</textarea>

  <script src="script.js"></script>

</body>
</html>
relatedTarget can be null

The relatedTarget property can be null.

That’s normal and just means that the mouse came not from another element, but from out of the window. Or that it left the window.

We should keep that possibility in mind when using event.relatedTarget in our code. If we access event.relatedTarget.tagName, then there will be an error.

Events frequency

The mousemove event triggers when the mouse moves. But that doesn’t mean that every pixel leads to an event.

The browser checks the mouse position from time to time. And if he notices changes then triggers the events.

That means that if the visitor is moving the mouse very fast then DOM-elements may be skipped:

If the mouse moves very fast from #FROM to #TO elements as painted above, then intermediate <div> (or some of them) may be skipped. The mouseout event may trigger on #FROM and then immediately mouseover on #TO.

In practice that’s helpful, because if there may be many intermediate elements. We don’t really want to process in and out of each one.

From the other side, we should keep in mind that we can’t assume that the mouse slowly moves from one event to another. No, it can “jump”.

In particular it’s possible that the cursor jumps right inside the middle of the page from out of the window. And relatedTarget=null, because it came from “nowhere”:

In case of a fast move, intermediate elements may trigger no events. But if the mouse enters the element (`mouseover`), when we're guaranteed to have `mouseout` when it leaves it.

Check it out “live” on a teststand below.

The HTML is two nested <div> elements. If you move the mouse fast over them, then there may be no events at all, or maybe only the red div triggers events, or maybe the green one.

Also try to move the pointer over the red div, and then move it out quickly down through the green one. If the movement is fast enough then the parent element is ignored.

Result
script.js
style.css
index.html
green.onmouseover = green.onmouseout = green.onmousemove = handler;

function handler(event) {
  let type = event.type;
  while (type < 11) type += ' ';

  log(type + " target=" + event.target.id)
  return false;
}


function clearText() {
  text.value = "";
  lastMessage = "";
}

let lastMessageTime = 0;
let lastMessage = "";
let repeatCounter = 1;

function log(message) {
  if (lastMessageTime == 0) lastMessageTime = new Date();

  let time = new Date();

  if (time - lastMessageTime > 500) {
    message = '------------------------------\n' + message;
  }

  if (message === lastMessage) {
    repeatCounter++;
    if (repeatCounter == 2) {
      text.value = text.value.trim() + ' x 2\n';
    } else {
      text.value = text.value.slice(0, text.value.lastIndexOf('x') + 1) + repeatCounter + "\n";
    }

  } else {
    repeatCounter = 1;
    text.value += message + "\n";
  }

  text.scrollTop = text.scrollHeight;

  lastMessageTime = time;
  lastMessage = message;
}
#green {
  height: 50px;
  width: 160px;
  background: green;
}

#red {
  height: 20px;
  width: 110px;
  background: red;
  color: white;
  font-weight: bold;
  padding: 5px;
  text-align: center;
  margin: 20px;
}

#text {
  font-size: 12px;
  height: 200px;
  width: 360px;
  display: block;
}
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="green">
    <div id="red">Test</div>
  </div>

  <input onclick="clearText()" value="Clear" type="button">

  <textarea id="text"></textarea>

  <script src="script.js"></script>

</body>

</html>

“Extra” mouseout when leaving for a child

Imagine – a mouse pointer entered an element. The mouseover triggered. Then the cursor goes into a child element. The interesting fact is that mouseout triggers in that case. The cursor is still in the element, but we have a mouseout from it!

That seems strange, but can be easily explained.

According to the browser logic, the mouse cursor may be only over a single element at any time – the most nested one (and top by z-index).

So if it goes to another element (even a descendant), then it leaves the previous one. That simple.

There’s a funny consequence that we can see on the example below.

The red <div> is nested inside the blue one. The blue <div> has mouseover/out handlers that log all events in the textarea below.

Try entering the blue element and then moving the mouse on the red one – and watch the events:

Result
script.js
style.css
index.html
function mouselog(event) {
  text.value += event.type + ' [target: ' + event.target.className + ']\n'
  text.scrollTop = text.scrollHeight
}
.blue {
  background: blue;
  width: 160px;
  height: 160px;
  position: relative;
}

.red {
  background: red;
  width: 100px;
  height: 100px;
  position: absolute;
  left: 30px;
  top: 30px;
}

textarea {
  height: 100px;
  width: 400px;
  display: block;
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div class="blue" onmouseover="mouselog(event)" onmouseout="mouselog(event)">
    <div class="red"></div>
  </div>

  <textarea id="text"></textarea>
  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>

</html>
  1. On entering the blue one – we get mouseover [target: blue].
  2. Then after moving from the blue to the red one – we get mouseout [target: blue] (left the parent).
  3. …And immediately mouseover [target: red].

So, for a handler that does not take target into account, it looks like we left the parent in mouseout in (2) and returned back to it by mouseover in (3).

If we perform some actions on entering/leaving the element, then we’ll get a lot of extra “false” runs. For simple stuff may be unnoticeable. For complex things that may bring unwanted side-effects.

We can fix it by using mouseenter/mouseleave events instead.

Events mouseenter and mouseleave

Events mouseenter/mouseleave are like mouseover/mouseout. They also trigger when the mouse pointer enters/leaves the element.

But there are two differences:

  1. Transitions inside the element are not counted.
  2. Events mouseenter/mouseleave do not bubble.

These events are intuitively very clear.

When the pointer enters an element – the mouseenter triggers, and then doesn’t matter where it goes while inside the element. The mouseleave event only triggers when the cursor leaves it.

If we make the same example, but put mouseenter/mouseleave on the blue <div>, and do the same – we can see that events trigger only on entering and leaving the blue <div>. No extra events when going to the red one and back. Children are ignored.

Result
script.js
style.css
index.html
function log(event) {
  text.value += event.type + ' [target: ' + event.target.id + ']\n';
  text.scrollTop = text.scrollHeight;
}
#blue {
  background: blue;
  width: 160px;
  height: 160px;
  position: relative;
}

#red {
  background: red;
  width: 70px;
  height: 70px;
  position: absolute;
  left: 45px;
  top: 45px;
}

#text {
  display: block;
  height: 100px;
  width: 400px;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <div id="blue" onmouseenter="log(event)" onmouseleave="log(event)">
    <div id="red"></div>
  </div>

  <textarea id="text"></textarea>
  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>

</html>

Event delegation

Events mouseenter/leave are very simple and easy to use. But they do not bubble. So we can’t use event delegation with them.

Imagine we want to handle mouse enter/leave for table cells. And there are hundreds of cells.

The natural solution would be – to set the handler on <table> and process events there. But mouseenter/leave don’t bubble. So if such event happens on <td>, then only a handler on that <td> can catch it.

Handlers for mouseenter/leave on <table> only trigger on entering/leaving the whole table. It’s impossible to get any information about transitions inside it.

Not a problem – let’s use mouseover/mouseout.

A simple handler may look like this:

// let's highlight cells under mouse
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';
};

table.onmouseout = function(event) {
  let target = event.target;
  target.style.background = '';
};
Result
script.js
style.css
index.html
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';
  text.value += "mouseover " + target.tagName + "\n";
  text.scrollTop = text.scrollHeight;
};

table.onmouseout = function(event) {
  let target = event.target;
  target.style.background = '';
  text.value += "mouseout " + target.tagName + "\n";
  text.scrollTop = text.scrollHeight;
};
#text {
  display: block;
  height: 100px;
  width: 456px;
}

#table th {
  text-align: center;
  font-weight: bold;
}

#table td {
  width: 150px;
  white-space: nowrap;
  text-align: center;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 12px;
}

#table .nw {
  background: #999;
}

#table .n {
  background: #03f;
  color: #fff;
}

#table .ne {
  background: #ff6;
}

#table .w {
  background: #ff0;
}

#table .c {
  background: #60c;
  color: #fff;
}

#table .e {
  background: #09f;
  color: #fff;
}

#table .sw {
  background: #963;
  color: #fff;
}

#table .s {
  background: #f60;
  color: #fff;
}

#table .se {
  background: #0c3;
  color: #fff;
}

#table .highlight {
  background: red;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>


  <table id="table">
    <tr>
      <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
    </tr>
    <tr>
      <td class="nw"><strong>Northwest</strong>
        <br>Metal
        <br>Silver
        <br>Elders
      </td>
      <td class="n"><strong>North</strong>
        <br>Water
        <br>Blue
        <br>Change
      </td>
      <td class="ne"><strong>Northeast</strong>
        <br>Earth
        <br>Yellow
        <br>Direction
      </td>
    </tr>
    <tr>
      <td class="w"><strong>West</strong>
        <br>Metal
        <br>Gold
        <br>Youth
      </td>
      <td class="c"><strong>Center</strong>
        <br>All
        <br>Purple
        <br>Harmony
      </td>
      <td class="e"><strong>East</strong>
        <br>Wood
        <br>Blue
        <br>Future
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>Southwest</strong>
        <br>Earth
        <br>Brown
        <br>Tranquility
      </td>
      <td class="s"><strong>South</strong>
        <br>Fire
        <br>Orange
        <br>Fame
      </td>
      <td class="se"><strong>Southeast</strong>
        <br>Wood
        <br>Green
        <br>Romance
      </td>
    </tr>

  </table>

  <textarea id="text"></textarea>

  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>
</html>

These handlers work when going from any element to any inside the table.

But we’d like to handle only transitions in and out of <td> as a whole. And highlight the cells as a whole. We don’t want to handle transitions that happen between the children of <td>.

One of solutions:

  • Remember the currently highlighted <td> in a variable.
  • On mouseover – ignore the event if we’re still inside the current <td>.
  • On mouseout – ignore if we didn’t leave the current <td>.

That filters out “extra” events when we are moving between the children of <td>.

Here’s the full example with all details:

Result
script.js
style.css
index.html
// <td> under the mouse right now (if any)
let currentElem = null;

table.onmouseover = function(event) {
  if (currentElem) {
    // before entering a new element, the mouse always leaves the previous one
    // if we didn't leave <td> yet, then we're still inside it, so can ignore the event
    return;
  }

  let target = event.target.closest('td');
  if (!target || !table.contains(target)) return;

  // yeah we're inside <td> now
  currentElem = target;
  target.style.background = 'pink';
};


table.onmouseout = function(event) {
  // if we're outside of any <td> now, then ignore the event
  if (!currentElem) return;

  // we're leaving the element -- where to? Maybe to a child element?
  let relatedTarget = event.relatedTarget;
  if (relatedTarget) { // possible: relatedTarget = null
    while (relatedTarget) {
      // go up the parent chain and check -- if we're still inside currentElem
      // then that's an internal transition -- ignore it
      if (relatedTarget == currentElem) return;
      relatedTarget = relatedTarget.parentNode;
    }
  }

  // we left the element. really.
  currentElem.style.background = '';
  currentElem = null;
};
#text {
  display: block;
  height: 100px;
  width: 456px;
}

#table th {
  text-align: center;
  font-weight: bold;
}

#table td {
  width: 150px;
  white-space: nowrap;
  text-align: center;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 12px;
}

#table .nw {
  background: #999;
}

#table .n {
  background: #03f;
  color: #fff;
}

#table .ne {
  background: #ff6;
}

#table .w {
  background: #ff0;
}

#table .c {
  background: #60c;
  color: #fff;
}

#table .e {
  background: #09f;
  color: #fff;
}

#table .sw {
  background: #963;
  color: #fff;
}

#table .s {
  background: #f60;
  color: #fff;
}

#table .se {
  background: #0c3;
  color: #fff;
}

#table .highlight {
  background: red;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>


  <table id="table">
    <tr>
      <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
    </tr>
    <tr>
      <td class="nw"><strong>Northwest</strong>
        <br>Metal
        <br>Silver
        <br>Elders
      </td>
      <td class="n"><strong>North</strong>
        <br>Water
        <br>Blue
        <br>Change
      </td>
      <td class="ne"><strong>Northeast</strong>
        <br>Earth
        <br>Yellow
        <br>Direction
      </td>
    </tr>
    <tr>
      <td class="w"><strong>West</strong>
        <br>Metal
        <br>Gold
        <br>Youth
      </td>
      <td class="c"><strong>Center</strong>
        <br>All
        <br>Purple
        <br>Harmony
      </td>
      <td class="e"><strong>East</strong>
        <br>Wood
        <br>Blue
        <br>Future
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>Southwest</strong>
        <br>Earth
        <br>Brown
        <br>Tranquility
      </td>
      <td class="s"><strong>South</strong>
        <br>Fire
        <br>Orange
        <br>Fame
      </td>
      <td class="se"><strong>Southeast</strong>
        <br>Wood
        <br>Green
        <br>Romance
      </td>
    </tr>

  </table>

  <script src="script.js"></script>

</body>
</html>

Try to move the cursor in and out of table cells and inside them. Fast or slow – doesn’t better. Only <td> as a whole is highlighted unlike the example before.

Summary

We covered events mouseover, mouseout, mousemove, mouseenter and mouseleave.

Things that are good to note:

  • A fast mouse move can make mouseover, mousemove, mouseout to skip intermediate elements.
  • Events mouseover/out and mouseenter/leave have an additional target: relatedTarget. That’s the element that we are coming from/to, complementary to target.
  • Events mouseover/out trigger even when we go from the parent element to a child element. They assume that the mouse can be only over one element at one time – the deepest one.
  • Events mouseenter/leave do not bubble and do not trigger when the mouse goes to a child element. They only track whether the mouse comes inside and outside the element as a whole.

Tasks

importance: 5

Write JavaScript that shows a tooltip over an element with the attribute data-tooltip.

That’s like the task Tooltip behavior, but here the annotated elements can be nested. The most deeply nested tooltip is shown.

For instance:

<div data-tooltip="Here – is the house interior" id="house">
  <div data-tooltip="Here – is the roof" id="roof"></div>
  ...
  <a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Read on…">Hover over me</a>
</div>

The result in iframe:

P.S. Hint: only one tooltip may show up at the same time.

Open a sandbox for the task.

importance: 5

Write a function that shows a tooltip over an element only if the visitor moves the mouse over it, but not through it.

In other words, if the visitor moves the mouse on the element and stopped – show the tooltip. And if he just moved the mouse through fast, then no need, who wants extra blinking?

Technically, we can measure the mouse speed over the element, and if it’s slow then we assume that it comes “over the element” and show the tooltip, if it’s fast – then we ignore it.

Make a universal object new HoverIntent(options) for it. With options:

  • elem – element to track.
  • over – a function to call if the mouse is slowly moving the element.
  • out – a function to call when the mouse leaves the element (if over was called).

An example of using such object for the tooltip:

// a sample tooltip
let tooltip = document.createElement('div');
tooltip.className = "tooltip";
tooltip.innerHTML = "Tooltip";

// the object will track mouse and call over/out
new HoverIntent({
  elem,
  over() {
    tooltip.style.left = elem.getBoundingClientRect().left + 'px';
    tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px';
    document.body.append(tooltip);
  },
  out() {
    tooltip.remove();
  }
});

The demo:

If you move the mouse over the “clock” fast then nothing happens, and if you do it slow or stop on them, then there will be a tooltip.

Please note: the tooltip doesn’t “blink” when the cursor between the clock subelements.

Open a sandbox with tests.

The algorithm looks simple:

  1. Put onmouseover/out handlers on the element. Also can use onmouseenter/leave here, but they are less universal, won’t work if we introduce delegation.
  2. When a mouse cursor entered the element, start measuring the speed on mousemove.
  3. If the speed is slow, then run over.
  4. Later if we’re out of the element, and over was executed, run out.

The question is: “How to measure the speed?”

The first idea would be: to run our function every 100ms and measure the distance between previous and new coordinates. If it’s small, then the speed is small.

Unfortunately, there’s no way to get “current mouse coordinates” in JavaScript. There’s no function like getCurrentMouseCoordinates().

The only way to get coordinates is to listen to mouse events, like mousemove.

So we can set a handler on mousemove to track coordinates and remember them. Then we can compare them, once per 100ms.

P.S. Please note: the solution tests use dispatchEvent to see if the tooltip works right.

Open the solution with tests in a sandbox.

Tutorial map

Comments

read this before commenting…
  • You're welcome to post additions, questions to the articles and answers to them.
  • To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…)
  • If you can't understand something in the article – please elaborate.