Windows 10 grid hover effect using HTML, CSS, and vanilla JS
Tags: html, css, javascript, webdev
Published: 2021-04-24 11:47:13

##Table of Contents
 1. [Introduction](#introduction)
 2. [Observations](#observations)
 3. [Getting Started](#getting-started)
 4. [The Crux](#the-crux)
     1. [Finding nearby elements to cursor](#finding-nearby-elements-to-cursor)
     2. [How to Calculate nearby points](#how-to-calculate-nearby-points)
     3. [Selecting and Styling the right elements](#selecting-and-styling-the-right-elements)
     4. [The Code](#the-code)
     5. [The Reduce Method](#the-reduce-method)
 5. [Handling Edge Cases](#handling-edge-cases)
 6. [Additional Resources](#additional-resources)

> **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 :)

##Introduction

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👏. I highly suggest that you read the first part (Button hover effect) because I explain some essential CSS properties used in all these effects.

You can have a look at the final grid hover effect below.

![Final Grid Hover Effect](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r166cv6nx4d6vx9i5660.gif)

 

{% codepen https://codepen.io/jashgopani/pen/MWJzPzJ %}

#Let's begin!

##Observations

1. The cursor moves near some grid item.
2. As soon as it reaches a minimum distance from the item, the borders of those nearby items are highlighted.
3. 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.

##Getting Started

I started the basic setup by forking my own implementation of [Windows button hover effect codepen](https://dev.to/jashgopani/windows-10-button-hover-effect-using-css-and-vanilla-js-1io4) and then adding the mouse events to the `win-grid` element. Here is the initial code.

### HTML
```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
```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
```javascript
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

![Initial code output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fz64h76ynaa06eptctha.gif)

#### 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](https://dev.to/jashgopani/windows-10-button-hover-effect-using-css-and-vanilla-js-1io4).

### Now starts the interesting part!

##The Crux

> **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.

###Finding nearby elements to cursor

![Cursor region diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zih2kijul5dpsby7fawq.png)

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: 
1. Either we check for all points on the circumference
2. 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 nearby points

![8 direction points](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/76ktalust52457w2yavv.png)

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 🙃

![Shift of origin of cursor region](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ut4560ed72q0b030wlw.png)

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
```

> **ℹ**: 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.

###Selecting and Styling the right elements

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](https://dev.to/jashgopani/windows-10-button-hover-effect-using-css-and-vanilla-js-1io4), 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

![meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c9thj9kss8r22y62bepc.png)
 
Here you go 😜

###The Code

```javascript
//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 not `map()` or `forEach()`? *🤔 
* *what is this `reduce()` method ?*😓

![zakir meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xudy308g92n5wxdubqzg.png)


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
1. **function that is executed for each item** in the array and **returns the updated result** by performing some operation over the previous result.
2. 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
1. The accumulator (this will be the result up to the current item)
2. The current item of the array
3. index of the item *(optional argument)*
4. array itself on which we are looping over *(optional argument)*

#### So, what happens inside reduce is that
1. 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]`**
2. Now, for the second item in the array, this process repeats,
So the accumulator becomes `[e1,e2]`
3. 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 to `accumulator`, so we simply return the `accumulator` as it is. So our **accumulator remains [e1,e2] only**.

#### Why don't we use `map()` or `forEach()`
There are 2 reasons for this
1. If we don't return anything in the `map` function, it will save an `undefined` value in the result array and to remove those we would have to use the `filter()` method 🥴 and we don't want to reiterate the array just for that.
2. 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 the `reduce()` method exists for such use cases so it is more appropriate to use `reduce()` here.
 
### That was a lot !!!
Let's have a look at the code and output at this point.

```javascript
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

![PartiallyWorking Grid gif (1)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gro40c9ux4xjn8mteptm.gif)
 

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.

```javascript
//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...**

##Handling Edge Cases
Just some small edge cases to cover...
1. Also, we want to clear any existing grid effects applied to a button, when we enter that button
2. Clear all effects when the cursor leaves `win-grid`


#### For case 1,

clear `nearBy` array in `mouseenter` event of `win-btn` !

#### For case 2,

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

```javascript
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.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.

  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.

Feel free to comment if you have any questions or issues and I'll try to help you!😁

Be ready for my next article as it going to be about creating the Windows 10 Calendar effect using the concepts I explained in these 2 articles.
Do not forget to share this article with your dev friends 😉.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u6l0hz6silc7fvxi6om0.png)

##Additional Resources
You can refer to the additional resources mentioned below for a better understanding of CSS and JS.

1. [MDN Docs - CSS](https://developer.mozilla.org/en-US/docs/Web/CSS)
2. [MDN Docs - JavaScript](https://developer.mozilla.org/en-US/docs/Web/JS)
3. [CSS Tricks](https://css-tricks.com/snippets/css/complete-guide-grid/)