This content originally appeared on DEV Community and was authored by Jash Gopani
Hello there, if you've arrived here after reading my previous post, I'd like to congratulate you as you already understand half of the code used in this effect?. For those who are unaware, I previously published an article describing how to create the Windows 10 button hover effect. I highly suggest that you read this because I explain some essential CSS properties used in these effects.
Note: The aim of writing this article is that readers of all skill levels understand maximum content. I have explained all the necessary basic concepts used in the effect in brief in this article; So please do not ignore the article by its length. Instead, if you are not a beginner, I request you to go through the content and provide your valuable feedback :)
You can have a look at the final grid hover effect below.
Let's begin!
Breaking down the effect logically:
- The cursor moves near some grid item.
- As soon as it reaches a minimum distance from the item, the borders of those nearby items are highlighted.
- The intensity of highlight on the border of items is based on the position of the cursor.
So, it is obvious that we will be working with mouse events, especially the mousemove
event.
I started the basic setup by forking my own implementation of Windows button hover effect codepen and then adding the mouse events to the win-grid
element. Here is the initial code.
HTML
<html>
<head>
<title>Windows 10 grid hover effect</title>
</head>
<body>
<h1>Windows 10 Button & Grid Hover Effect</h1>
<div class="win-grid">
<div class="win-btn" id="1">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="2">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="3">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="4">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="5">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="6">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="7">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="8">This is a windows hoverable item inside windows grid</div>
<div class="win-btn" id="9">This is a windows hoverable item inside windows grid</div>
</div>
</body>
</html>
CSS
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100&display=swap");
* {
box-sizing: border-box;
color: white;
font-family: "Noto Sans JP", sans-serif;
}
body {
background-color: black;
display: flex;
flex-flow: column wrap;
justofy-content: center;
align-items: center;
}
.win-grid {
border: 1px solid white;
letter-spacing: 2px;
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: stretch;
text-align: center;
grid-gap: 1rem;
padding: 5rem;
}
.win-btn {
padding: 1rem 2rem;
text-align: center;
border: none;
border-radius: 0px;
border: 1px solid transparent;
}
button:focus {
outline: none;
}
JS
document.querySelectorAll(".win-btn").forEach((b) => {
b.onmouseleave = (e) => {
e.target.style.background = "black";
e.target.style.borderImage = null;
};
b.addEventListener("mousemove", (e) => {
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left; //x position within the element.
const y = e.clientY - rect.top; //y position within the element.
e.target.style.background = `radial-gradient(circle at ${x}px ${y}px , rgba(255,255,255,0.2),rgba(255,255,255,0) )`;
e.target.style.borderImage = `radial-gradient(20% 75% at ${x}px ${y}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.1) ) 1 / 1px / 0px stretch `;
});
});
const body = document.querySelector(".win-grid");
body.addEventListener("mousemove", (e) => {
//effect logic here
});
This is how our output looks at this point
A quick explanation for the above code:
HTML code is pretty simple, a container div which will be the grid, and inside it are the items.
In CSS, I have used a CSS grid to layout the items, so that the design remains responsive. The grid layout has 3 items, the grid has the class win-grid and the grid items are of class win-btn.
JS is the button hover effect code. For a detailed explanation read this.
Now starts the interesting part!
Note: This is my logic and there can be a different approach for implementing this effect but after looking at existing implementations available online I can assure you that my approach is the clean, least complicated, and scalable unlike other hardcoded ones ?.
When the cursor comes inside the grid area, we need elements surrounding the cursor up to a particular distance. I refer to this radius or distance value as offset
in my code. The bad news is that there is no method in JS to find elements in a certain region, but the good news is that there exists a method to find elements given a coordinate!
The method is document.elementFromPoint(x,y)
;
It returns the topmost element falling under the coordinate passed as arguments. So if the coordinates are valid, then the method will return the body
or some other element inside the body
.
Your immediate question would be how exactly do we use this method to find surrounding nearby elements and what coordinates do we pass?
To understand this, have a look below.
From the figure, you might have guessed that we will check for points on the circumference of the circular region. That's absolutely correct!
We have 2 approaches from here:
- Either we check for all points on the circumference
- We skip some points
Obviously, option 2 looks less complicated; but which points to check for and which to skip?
Since the max number of elements inside the grid, near the cursor, will be 4, we can check in all 8 directions around the cursor just like we do in real life!
How to calculate those 8 points though?
Since these points lie on the circumference of the circle, we will use simple vector mathematics to find them.
So if p(x,y) is a point on the circumference of a circle on origin, with radius r, at a particular angle from the X-axis, the coordinates are calculated as follows
px = r*cos(angle)
py = r*sin(angle)
Note : angle is in radians i.e (degrees * PI / 180)
You can directly calculate these points, by simple logic (x-offset,y) for left, (x+offset,y) for right, and so on…But that would be too much hardcoding. Initially, I had gone for this approach and realized that if I want to increase or decrease the number of points around the cursor position, I had to declare or comment out lines of code, and that way we would not be writing very efficient code ?
Since the cursor is not going to be on the origin, we need to add the x and y distance from the origin to our coordinates px and py (Refer to the diagram above). Hence our new coordinates of the point on circumference become cx,cy (I call it changed x and y)
So the formula changes to
cx = x + r*cos(angle)
cy = y + r*sin(angle)
//where x,y refers to the current position of the cursor on the screen
?For those who don't know
the origin of the screen is the top left corner and the left edge is the positive Y-axis and the top edge is the positive X-axis.
Now, since we know how to find those 8 points, we will find elements on those points. We check if the element is not null, then check if its class is win-btn
or not, and also, we need to check if the element already exists in the nearBy
array or not. We only move ahead with the element if it does not exist in the nearBy
array; then we finally apply border-image
to the element.
Why don't we save the elements first then loop over the array again...that would be donkey work tbh.
The exists in
nearBy
array check is required because the mouseover event triggers every time the cursor is moved and our logic will be fired every time the event fires. So we need to ensure that we are not saving the same elements again and again.
Now calculating the border image is already explained in the previous article, so I won't explain it here again.
If the above explanation is not making sense to you, have a look at the code below.
Some readers at this point are like
Here you go ?
//generate the angle values in radians
const angles = [];
for (let i = 0; i <= 360; i += 45) {
angles.push((i * Math.PI) / 180);
}
//for each angle, find and save elements at that point
let nearBy = [];
nearBy = angles.reduce((acc, rad, i, arr) => {
//find the coordinate for current angle
const cx = Math.floor(x + Math.cos(rad) * offset);
const cy = Math.floor(y + Math.sin(rad) * offset);
const element = document.elementFromPoint(cx, cy);
if (element !== null) {
;
if (
element.className === "win-btn" &&
acc.findIndex((ae) => ae.id === element.id) < 0
) {
const brect = element.getBoundingClientRect();
const bx = x - brect.left; //x position within the element.
const by = y - brect.top; //y position within the element.
if (!element.style.borderImage)
element.style.borderImage = `radial-gradient(${offset * 2}px ${
offset * 2
}px at ${bx}px ${by}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.1),transparent ) 9 / 1px / 0px stretch `;
return [...acc, element];
}
}
return acc;
}, []);
- *What code is this? *?
- *Why is he using
reduce()
and why notmap()
orforEach()
? *? -
what is this
reduce()
method ??
Just think what all steps we want to follow...
For each angle in the angles
array,
1. We want to find an element from the coordinates.
2. Apply style to the element if valid
3. Save the element on which style was applied into the nearBy
array
So after processing each angle of the angle
array, we want a single result i.e an array containing all nearBy elements which then, we store in the nearBy
array.
In such scenarios where we want a single output after performing some operation on each item of an array, we use the reduce()
method.
The reduce method
It takes 2 arguments
- function that is executed for each item in the array and returns the updated result by performing some operation over the previous result.
- variable (generally referred to as accumulator) that is equal to the latest result returned by the function mentioned above
The first argument i.e the function
This has several arguments
- The accumulator (this will be the result up to the current item)
- The current item of the array
- index of the item (optional argument)
- array itself on which we are looping over (optional argument)
So, what happens inside reduce is that
- It starts with the first item of the angle array. The accumulator has the initial value that is set in our code (In our case, it is an empty array).
The current index is 0 and inside our function,
We find an element based on the current angle and apply CSS to it (if applicable), and finally what we do is we return a new array with existing items of the accumulator (which do not exist at this point because the accumulator is empty) and our new element lets say e1 i.e
[...acc, element]
.
So our updated accumulator is [e1]
- Now, for the second item in the array, this process repeats,
So the accumulator becomes
[e1,e2]
- and this goes on till we reach the end of the array.
4.Let's say if we get an element e3 which is
win-grid
itself, we don't want to add it toaccumulator
, so we simply return theaccumulator
as it is. So our accumulator remains [e1,e2] only.
Why don't we use map()
or forEach()
There are 2 reasons for this
- If we don't return anything in the
map
function, it will save anundefined
value in the result array and to remove those we would have to use thefilter()
method ? and we don't want to reiterate the array just for that. - The forEach method does not return any value, it will run a function for each item and we will have to push items manually into the
nearby
array which is not incorrect but thereduce()
method exists for such use cases so it is more appropriate to usereduce()
here.
That was a lot !!!
Let's have a look at the code and output at this point.
const offset = 69;
const angles = []; //in deg
for (let i = 0; i <= 360; i += 45) {
angles.push((i * Math.PI) / 180);
}
let nearBy = [];
/*Effect #1 - https://codepen.io/struct_dhancha/pen/QWdqMLZ*/
document.querySelectorAll(".win-btn").forEach((b) => {
b.onmouseleave = (e) => {
e.target.style.background = "black";
e.target.style.borderImage = null;
e.target.border = "1px solid transparent";
};
b.addEventListener("mousemove", (e) => {
e.stopPropagation();
e.target.border = "1px solid transparent";
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left; //x position within the element.
const y = e.clientY - rect.top; //y position within the element.
e.target.style.background = `radial-gradient(circle at ${x}px ${y}px , rgba(255,255,255,0.25),rgba(255,255,255,0) )`;
e.target.style.borderImage = `radial-gradient(20% 65% at ${x}px ${y}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.7),rgba(255,255,255,0.1) ) 9 / 2px / 0px stretch `;
});
});
const body = document.querySelector(".win-grid");
body.addEventListener("mousemove", (e) => {
const x = e.x; //x position within the element.
const y = e.y; //y position within the element.
nearBy = angles.reduce((acc, rad, i, arr) => {
const cx = Math.floor(x + Math.cos(rad) * offset);
const cy = Math.floor(y + Math.sin(rad) * offset);
const element = document.elementFromPoint(cx, cy);
if (element !== null) {
if (
element.className === "win-btn" &&
acc.findIndex((ae) => ae.id === element.id) < 0
) {
const brect = element.getBoundingClientRect();
const bx = x - brect.left; //x position within the element.
const by = y - brect.top; //y position within the element.
if (!element.style.borderImage)
element.style.borderImage = `radial-gradient(${offset * 2}px ${
offset * 2
}px at ${bx}px ${by}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.1),transparent ) 9 / 1px / 0px stretch `;
return [...acc, element];
}
}
return acc;
}, []);
});
Here is the output
So as you can see, we are successful in detecting and highlighting nearby elements ?.
But, we must not forget to clear the previously applied effects when the mouse moves. This way, every time the mouse moves, the elements which were highlighted at the previous position are changed back to their original transparent border state and then we calculate all the nearby elements again from fresh and apply effects to the valid ones! And yes, do not forget to clear the previously saved nearBy elements else your cursor is at a new location and the current nearBy and previous nearBy both elements will be highlighted ? which would be not-so-pleasing.
So 2 things to do, remove all nearBy elements and border-image on them. We do this, just before calculating the new nearBy elements.
//inside the event listener
nearBy.splice(0, nearBy.length).forEach((e) => (e.style.borderImage = null));
//reduce method below
This one line of code does the 2 things I said.
The splice()
method takes a starting index and the number of items to be removed from that starting index, including the starting index and it modifies the original array. Hence after splice() operation, our nearBy
array is empty. The splice()
method returns an array containing all the items which were removed. So we iterate over that array and remove the border-image
of all those elements!
And we are almost done...
Just some small edge cases to cover...
- Since the mouseover event is listened to by both win-btn and win-grid elements, we need to make sure that we don't trigger our grid event when we are inside the button
- Also, we want to clear any existing grid effects applied to a button, when we enter that button
- Clear all effects when the cursor leaves
win-grid
For case 1,
we prevent the event bubbling i.e the passing of the event from the child to the parent when both of them have an event listener registered for the same event. In this case, the win-btn
is the child and the win-grid
is the parent.
We do this by adding 2 things
-
event.stopPropogation()
insidemousemove
event listener ofwin-btn
which prevents the event from propagating / passing further. - by checking the
className
of thee.target
element in the event listener ofwin-grid
. If the class iswin-btn
, simply return.
For case 2,
clear nearBy
array in mouseenter
event of win-btn
!
For case 3,
clear nearBy
array in mouseleave
event of win-grid
!
Since clearing nearby is performed multiple times, I have shifted that code to a method clearNearBy()
and I call that wherever clearing is to be done.
And that is finally all the code
const offset = 69;
const angles = []; //in deg
for (let i = 0; i <= 360; i += 45) {
angles.push((i * Math.PI) / 180);
}
let nearBy = [];
function clearNearBy() {
nearBy.splice(0, nearBy.length).forEach((e) => (e.style.borderImage = null));
}
/*Effect #1 - https://codepen.io/struct_dhancha/pen/QWdqMLZ*/
document.querySelectorAll(".win-btn").forEach((b) => {
b.onmouseleave = (e) => {
e.target.style.background = "black";
e.target.style.borderImage = null;
e.target.border = "1px solid transparent";
};
b.onmouseenter = (e) => {
clearNearBy();
};
b.addEventListener("mousemove", (e) => {
e.stopPropagation();
e.target.border = "1px solid transparent";
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left; //x position within the element.
const y = e.clientY - rect.top; //y position within the element.
e.target.style.background = `radial-gradient(circle at ${x}px ${y}px , rgba(255,255,255,0.25),rgba(255,255,255,0) )`;
e.target.style.borderImage = `radial-gradient(20% 65% at ${x}px ${y}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.7),rgba(255,255,255,0.1) ) 9 / 2px / 0px stretch `;
});
});
const body = document.querySelector(".win-grid");
body.addEventListener("mousemove", (e) => {
if (document.elementFromPoint(e.x, e.y).className === "win-btn") return;
const x = e.x; //x position within the element.
const y = e.y; //y position within the element.
clearNearBy();
nearBy = angles.reduce((acc, rad, i, arr) => {
const cx = Math.floor(x + Math.cos(rad) * offset);
const cy = Math.floor(y + Math.sin(rad) * offset);
const element = document.elementFromPoint(cx, cy);
if (element !== null) {
if (
element.className === "win-btn" &&
acc.findIndex((ae) => ae.id === element.id) < 0
) {
const brect = element.getBoundingClientRect();
const bx = x - brect.left; //x position within the element.
const by = y - brect.top; //y position within the element.
if (!element.style.borderImage)
element.style.borderImage = `radial-gradient(${offset * 2}px ${
offset * 2
}px at ${bx}px ${by}px ,rgba(255,255,255,0.7),rgba(255,255,255,0.1),transparent ) 9 / 1px / 0px stretch `;
return [...acc, element];
}
}
return acc;
}, []);
});
body.onmouseleave = (e) => {
clearNearBy();
};
If you have reached here then a big Thankyou ? for completing this article.
I hope that I was able to convey all the information to you and you enjoyed reading it and have learned something new.
Please comment below your reviews and suggestions about the article and share it with your dev friends.
Feel free to comment about topics that you want an explanation/tutorial on!?
I am planning to post one tutorial on recreating the windows taskbar calendar effect also since existing implementations of it are not very great as I mention previously!
If you are excited about that, then please share your excitement in the comments ?
You can refer to the additional resources mentioned below for a better understanding of CSS and JS.
This content originally appeared on DEV Community and was authored by Jash Gopani
Jash Gopani | Sciencx (2021-04-24T11:47:13+00:00) Windows 10 grid hover effect using HTML, CSS, and vanilla JS. Retrieved from https://www.scien.cx/2021/04/24/windows-10-grid-hover-effect-using-html-css-and-vanilla-js/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.