Let’s dive into more details about events that happen when the mouse moves between elements.
Events 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 property relatedTarget
. This property complements target
. When a mouse leaves one element for another, one of them becomes target
, and the other one – relatedTarget
.
For mouseover
:
event.target
– is the element where the mouse came over.event.relatedTarget
– is the element from which the mouse came (relatedTarget
→target
).
For mouseout
the reverse:
event.target
– is the element that the mouse left.event.relatedTarget
– is the new under-the-pointer element, that mouse left for (target
→relatedTarget
).
In the example below each face and its features are separate elements. When you move the mouse, you can see mouse events in the text area.
Each event has the information about both target
and relatedTarget
:
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.
Skipping elements
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 it notices changes then triggers the events.
That means that if the visitor is moving the mouse very fast then some DOM-elements may be skipped:
If the mouse moves very fast from #FROM
to #TO
elements as painted above, then intermediate <div>
elements (or some of them) may be skipped. The mouseout
event may trigger on #FROM
and then immediately mouseover
on #TO
.
That’s good for performance, because there may be many intermediate elements. We don’t really want to process in and out of each one.
On the other hand, we should keep in mind that the mouse pointer doesn’t “visit” all elements along the way. It can “jump”.
In particular, it’s possible that the pointer jumps right inside the middle of the page from out of the window. In that case relatedTarget
is null
, because it came from “nowhere”:
You can check it out “live” on a teststand below.
Its HTML has two nested elements: the <div id="child">
is inside the <div id="parent">
. If you move the mouse fast over them, then maybe only the child div triggers events, or maybe the parent one, or maybe there will be no events at all.
Also move the pointer into the child div
, and then move it out quickly down through the parent one. If the movement is fast enough, then the parent element is ignored. The mouse will cross the parent element without noticing it.
let parent = document.getElementById('parent');
parent.onmouseover = parent.onmouseout = parent.onmousemove = handler;
function handler(event) {
let type = event.type;
while (type.length < 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;
}
#parent {
background: #99C0C3;
width: 160px;
height: 120px;
position: relative;
}
#child {
background: #FFDE99;
width: 50%;
height: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
textarea {
height: 140px;
width: 300px;
display: block;
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="parent">parent
<div id="child">child</div>
</div>
<textarea id="text"></textarea>
<input onclick="clearText()" value="Clear" type="button">
<script src="script.js"></script>
</body>
</html>
mouseover
triggered, there must be mouseout
In case of fast mouse movements, intermediate elements may be ignored, but one thing we know for sure: if the pointer “officially” entered an element (mouseover
event generated), then upon leaving it we always get mouseout
.
Mouseout when leaving for a child
An important feature of mouseout
– it triggers, when the pointer moves from an element to its descendant, e.g. from #parent
to #child
in this HTML:
<div id="parent">
<div id="child">...</div>
</div>
If we’re on #parent
and then move the pointer deeper into #child
, we get mouseout
on #parent
!
That may seem 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.
Please note another important detail of event processing.
The mouseover
event on a descendant bubbles up. So, if #parent
has mouseover
handler, it triggers:
You can see that very well in the example below: <div id="child">
is inside the <div id="parent">
. There are mouseover/out
handlers on #parent
element that output event details.
If you move the mouse from #parent
to #child
, you see two events on #parent
:
mouseout [target: parent]
(left the parent), thenmouseover [target: child]
(came to the child, bubbled).
function mouselog(event) {
let d = new Date();
text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
text.scrollTop = text.scrollHeight;
}
#parent {
background: #99C0C3;
width: 160px;
height: 120px;
position: relative;
}
#child {
background: #FFDE99;
width: 50%;
height: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
textarea {
height: 140px;
width: 300px;
display: block;
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="parent" onmouseover="mouselog(event)" onmouseout="mouselog(event)">parent
<div id="child">child</div>
</div>
<textarea id="text"></textarea>
<input type="button" onclick="text.value=''" value="Clear">
<script src="script.js"></script>
</body>
</html>
As shown, when the pointer moves from #parent
element to #child
, two handlers trigger on the parent element: mouseout
and mouseover
:
parent.onmouseout = function(event) {
/* event.target: parent element */
};
parent.onmouseover = function(event) {
/* event.target: child element (bubbled) */
};
If we don’t examine event.target
inside the handlers, then it may seem that the mouse pointer left #parent
element, and then immediately came back over it.
But that’s not the case! The pointer is still over the parent, it just moved deeper into the child element.
If there are some actions upon leaving the parent element, e.g. an animation runs in parent.onmouseout
, we usually don’t want it when the pointer just goes deeper into #parent
.
To avoid it, we can check relatedTarget
in the handler and, if the mouse is still inside the element, then ignore such event.
Alternatively we can use other events: mouseenter
and mouseleave
, that we’ll be covering now, as they don’t have such problems.
Events mouseenter and mouseleave
Events mouseenter/mouseleave
are like mouseover/mouseout
. They trigger when the mouse pointer enters/leaves the element.
But there are two important differences:
- Transitions inside the element, to/from descendants, are not counted.
- Events
mouseenter/mouseleave
do not bubble.
These events are extremely simple.
When the pointer enters an element – mouseenter
triggers. The exact location of the pointer inside the element or its descendants doesn’t matter.
When the pointer leaves an element – mouseleave
triggers.
This example is similar to the one above, but now the top element has mouseenter/mouseleave
instead of mouseover/mouseout
.
As you can see, the only generated events are the ones related to moving the pointer in and out of the top element. Nothing happens when the pointer goes to the child and back. Transitions between descendants are ignored
function mouselog(event) {
let d = new Date();
text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
text.scrollTop = text.scrollHeight;
}
#parent {
background: #99C0C3;
width: 160px;
height: 120px;
position: relative;
}
#child {
background: #FFDE99;
width: 50%;
height: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
textarea {
height: 140px;
width: 300px;
display: block;
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="parent" onmouseenter="mouselog(event)" onmouseleave="mouselog(event)">parent
<div id="child">child</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>
is able to catch it.
Handlers for mouseenter/leave
on <table>
only trigger when the pointer enters/leaves the table as a whole. It’s impossible to get any information about transitions inside it.
So, let’s use mouseover/mouseout
.
Let’s start with simple handlers that highlight the element under mouse:
// let's highlight an element under the pointer
table.onmouseover = function(event) {
let target = event.target;
target.style.background = 'pink';
};
table.onmouseout = function(event) {
let target = event.target;
target.style.background = '';
};
Here they are in action. As the mouse travels across the elements of this table, the current one is highlighted:
table.onmouseover = function(event) {
let target = event.target;
target.style.background = 'pink';
text.value += `over -> ${target.tagName}\n`;
text.scrollTop = text.scrollHeight;
};
table.onmouseout = function(event) {
let target = event.target;
target.style.background = '';
text.value += `out <- ${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;
cursor: pointer;
}
#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>
In our case we’d like to handle transitions between table cells <td>
: entering a cell and leaving it. Other transitions, such as inside the cell or outside of any cells, don’t interest us. Let’s filter them out.
Here’s what we can do:
- Remember the currently highlighted
<td>
in a variable, let’s call itcurrentElem
. - On
mouseover
– ignore the event if we’re still inside the current<td>
. - On
mouseout
– ignore if we didn’t leave the current<td>
.
Here’s an example of code that accounts for all possible situations:
// <td> under the mouse right now (if any)
let currentElem = null;
table.onmouseover = function(event) {
// before entering a new element, the mouse always leaves the previous one
// if currentElem is set, we didn't leave the previous <td>,
// that's a mouseover inside it, ignore the event
if (currentElem) return;
let target = event.target.closest('td');
// we moved not into a <td> - ignore
if (!target) return;
// moved into <td>, but outside of our table (possible in case of nested tables)
// ignore
if (!table.contains(target)) return;
// hooray! we entered a new <td>
currentElem = target;
onEnter(currentElem);
};
table.onmouseout = function(event) {
// if we're outside of any <td> now, then ignore the event
// that's probably a move inside the table, but out of <td>,
// e.g. from <tr> to another <tr>
if (!currentElem) return;
// we're leaving the element – where to? Maybe to a descendant?
let relatedTarget = event.relatedTarget;
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 <td>. really.
onLeave(currentElem);
currentElem = null;
};
// any functions to handle entering/leaving an element
function onEnter(elem) {
elem.style.background = 'pink';
// show that in textarea
text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
text.scrollTop = 1e6;
}
function onLeave(elem) {
elem.style.background = '';
// show that in textarea
text.value += `out <- ${elem.tagName}.${elem.className}\n`;
text.scrollTop = 1e6;
}
Once again, the important features are:
- It uses event delegation to handle entering/leaving of any
<td>
inside the table. So it relies onmouseover/out
instead ofmouseenter/leave
that don’t bubble and hence allow no delegation. - Extra events, such as moving between descendants of
<td>
are filtered out, so thatonEnter/Leave
runs only if the pointer leaves or enters<td>
as a whole.
Here’s the full example with all details:
// <td> under the mouse right now (if any)
let currentElem = null;
table.onmouseover = function(event) {
// before entering a new element, the mouse always leaves the previous one
// if currentElem is set, we didn't leave the previous <td>,
// that's a mouseover inside it, ignore the event
if (currentElem) return;
let target = event.target.closest('td');
// we moved not into a <td> - ignore
if (!target) return;
// moved into <td>, but outside of our table (possible in case of nested tables)
// ignore
if (!table.contains(target)) return;
// hooray! we entered a new <td>
currentElem = target;
onEnter(currentElem);
};
table.onmouseout = function(event) {
// if we're outside of any <td> now, then ignore the event
// that's probably a move inside the table, but out of <td>,
// e.g. from <tr> to another <tr>
if (!currentElem) return;
// we're leaving the element – where to? Maybe to a descendant?
let relatedTarget = event.relatedTarget;
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 <td>. really.
onLeave(currentElem);
currentElem = null;
};
// any functions to handle entering/leaving an element
function onEnter(elem) {
elem.style.background = 'pink';
// show that in textarea
text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
text.scrollTop = 1e6;
}
function onLeave(elem) {
elem.style.background = '';
// show that in textarea
text.value += `out <- ${elem.tagName}.${elem.className}\n`;
text.scrollTop = 1e6;
}
#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;
cursor: pointer;
}
#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>
Try to move the cursor in and out of table cells and inside them. Fast or slow – doesn’t matter. Only <td>
as a whole is highlighted, unlike the example before.
Summary
We covered events mouseover
, mouseout
, mousemove
, mouseenter
and mouseleave
.
These things are good to note:
- A fast mouse move may skip intermediate elements.
- Events
mouseover/out
andmouseenter/leave
have an additional property:relatedTarget
. That’s the element that we are coming from/to, complementary totarget
.
Events mouseover/out
trigger even when we go from the parent element to a child element. The browser assumes that the mouse can be only over one element at one time – the deepest one.
Events mouseenter/leave
are different in that aspect: they only trigger when the mouse comes in and out the element as a whole. Also they do not bubble.