skip to Main Content

I’m trying to make a flashlight effect that will reveal a content inside a black box. So a flashlight should be a circle centered in place of a cursor and bigger than my custom img cursor which is also centered (exactly like in the current solution). The issue is that I cannot achieve the reveal of a content functionality and ended up with that radial-gradient css property which does nothing in case of revealing. Made it to visualize how it should look like. Could anyone help?

This is a reproduction of my code. The img in place of a cursor is random but normally it would be a flashlight img.

Link to a sandbox – https://codesandbox.io/s/javascript-forked-6v9rcm?file=/src/index.js

const box = document.querySelector(".box");

const customCursor = document.createElement("img");
customCursor.className = "custom-cursor";
customCursor.src = "https://picsum.photos/50";

const flashLight = document.createElement("div");
flashLight.className = "flashlight";

box.appendChild(customCursor);
box.appendChild(flashLight);

box.addEventListener("mousemove", (e) => {
  const rect = box.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;

  customCursor.style.display = "block";
  flashLight.style.display = "block";

  customCursor.style.left = `${x - customCursor.width / 2}px`;
  customCursor.style.top = `${y - customCursor.height / 2}px`;

  flashLight.style.left = `${x - flashLight.offsetWidth / 2}px`;
  flashLight.style.top = `${y - flashLight.offsetHeight / 2}px`;
});

box.addEventListener("mouseleave", () => {
  customCursor.style.display = "none";
  flashLight.style.display = "none";
});
body {
  font-family: sans-serif;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.box {
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: none;
  width: 100%;
  padding: 5rem;
  background-color: #000;
  overflow: hidden;
  position: relative;
}

.custom-cursor {
  width: 50px;
  height: 50px;
  position: absolute;
  display: none;
  z-index: 999;
  pointer-events: none;
}
.flashlight {
  border-radius: 50%;
  width: 200px;
  height: 200px;
  background-image: radial-gradient(
    circle,
    rgba(255, 255, 255, 0.8) 0%,
    rgba(255, 255, 255, 0) 100%
  );
  position: absolute;
}
<!DOCTYPE html>
<html>
  <head>
    <title>Parcel Sandbox</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="app">
      <div class="box"><h1>Light</h1></div>
    </div>

    <script src="src/index.js"></script>
  </body>
</html>

5

Answers


  1. I have done some modifications to your code. Here is the final result:

    const showIt = document.getElementById("showIt");
    
    showIt.addEventListener('mouseenter', (e) => {
      e.target.style.color = 'yellow';
    });
    showIt.addEventListener('mouseleave', (e) => {
      e.target.style.color = 'transparent';
    });
    #showIt{
      position: relative;
      color: transparent;
      z-index: 1;
    }
    <div id="app">
      <div class="box"><h1 id="showIt">Light</h1></div>
    </div>

    Your main problem was that because the text was behind all other positioned elements, it couldn’t be detected. so we have to give it a position to bring it to the front to be discoverable.

    Login or Signup to reply.
  2. First, you need a container on which you want to apply the flashlight effect. After that, a specific HTML structure must be implemented within the container, which, with the appropriate CSS styling, creates the "flashlight" effect. The movement of this HTML structure based on mouse events can be achieved with JavaScript. I’ve prepared two examples for you.

    Solution # 1 (Local: Detecting mouse movement within the container)

    Each container here is equipped with its own event listener. If we move the cursor out of the container, the "flashlight" effect disappears, and it appears when the cursor is moved inside.

    function addFlashlightHTMLElementsTo(container) {
      const darknessDiv = document.createElement('div')
      darknessDiv.classList.add('darkness')
      const lightDiv = document.createElement('div')
      lightDiv.classList.add('light')
      const shadowDiv = document.createElement('div')
      shadowDiv.classList.add('shadow')
    
      lightDiv.appendChild(shadowDiv)
      darknessDiv.appendChild(lightDiv)
      container.appendChild(darknessDiv)
    }
    
    function setFlashlightTo(container) {
      addFlashlightHTMLElementsTo(container)
      const light = container.querySelector('.light')  
      
      const containerLeft = container.getBoundingClientRect().left
      const containerTop = container.getBoundingClientRect().top
      function update(e) {
        const scrollLeft = document.documentElement.scrollLeft
        const x = (e.clientX || 0) + scrollLeft - containerLeft
        const scrollTop = document.documentElement.scrollTop
        const y = (e.clientY || 0) + scrollTop - containerTop
    
        light.style.left = x + 'px'
        light.style.top = y + 'px'
      }
    
      container.addEventListener('mousemove', update)
      container.addEventListener('touchmove', update)
      
      light.style.background = '#000'
      container.addEventListener('mouseenter', function () {
        light.style.background = 'transparent'
      })
      container.addEventListener('mouseleave', function () {
        light.style.background = '#000'
      })
    }
    
    document.addEventListener('DOMContentLoaded', function () {
      const flashlightContainers = document.querySelectorAll('[flashlighted]')
      
      flashlightContainers.forEach((container) => {
        setFlashlightTo(container)
      })
    })
    .container {
      height: 300px;
      width: 400px;
      position: relative;
      overflow: hidden;
      margin: 10px 0;
    }
    
    .container img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
    
    .darkness {
      position: absolute;
      inset: 0;
      overflow: hidden;
      pointer-events: none;
      z-index: 1;
    }
    
    .light {
      position: absolute;
      width: 300px;
      height: 300px;
      border-radius: 50%;
      box-shadow: 0 0 0 400vw rgba(0, 0, 0, 0.99);
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      transition: all 0.1s;
    }
    
    .shadow {
      display: block;
      width: 100%;
      height: 100%;
      border-radius: 50%;
      box-shadow: inset 4px 0 37px 53px rgba(0, 0, 0, 0.9);
    }
    <div class="container" flashlighted>
      <img src="https://picsum.photos/400/300" />
    </div>
    
    <div class="container" flashlighted>
      <img src="https://picsum.photos/400/300" />
    </div>
    
    <div class="container" flashlighted>
      <h1>Lorem Ipsum</h1>
      <h3>Lorem ipsum dolor sit amet</h3>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
    </div>
    
    <div class="container" flashlighted>
      <h1>Lorem Ipsum</h1>
      <h3>Lorem ipsum dolor sit amet</h3>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
    </div>

    Solution # 2 (Global: Detecting mouse movement within the document.body)

    In this case, we are monitoring cursor movement across the entire page just once. We no longer track it for each container because it’s not necessary. There’s no need to monitor the body scrolling here because we are tracking cursor movement throughout the entire body, so its position determination already includes it.

    function addFlashlightHTMLElementsTo(container) {
      const darknessDiv = document.createElement('div')
      darknessDiv.classList.add('darkness')
      const lightDiv = document.createElement('div')
      lightDiv.classList.add('light')
      const shadowDiv = document.createElement('div')
      shadowDiv.classList.add('shadow')
    
      lightDiv.appendChild(shadowDiv)
      darknessDiv.appendChild(lightDiv)
      container.appendChild(darknessDiv)
    }
    
    document.addEventListener('DOMContentLoaded', function () {
      const flashlightContainers = document.querySelectorAll('[flashlighted]')
      
      flashlightContainers.forEach((container) => {
        addFlashlightHTMLElementsTo(container)
      })
      
      function update(e) {
        flashlightContainers.forEach((container) => {
          const light = container.querySelector('.light')
          
          const containerLeft = container.getBoundingClientRect().left
          const containerTop = container.getBoundingClientRect().top
    
          const x = (e.clientX || 0) - containerLeft
          const y = (e.clientY || 0) - containerTop
    
          light.style.left = x + 'px'
          light.style.top = y + 'px'
        })
      }
    
      document.addEventListener('mousemove', update)
      document.addEventListener('touchmove', update)
    })
    .container {
      height: 300px;
      width: 400px;
      position: relative;
      overflow: hidden;
      margin: 10px 0;
    }
    
    .container img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
    
    .darkness {
      position: absolute;
      inset: 0;
      overflow: hidden;
      pointer-events: none;
      z-index: 1;
    }
    
    .light {
      position: absolute;
      width: 300px;
      height: 300px;
      border-radius: 50%;
      box-shadow: 0 0 0 400vw rgba(0, 0, 0, 0.99);
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      transition: all 0.1s;
    }
    
    .shadow {
      display: block;
      width: 100%;
      height: 100%;
      border-radius: 50%;
      box-shadow: inset 4px 0 37px 53px rgba(0, 0, 0, 0.9);
    }
    <div class="container" flashlighted>
      <img src="https://picsum.photos/400/300" />
    </div>
    
    <div class="container" flashlighted>
      <img src="https://picsum.photos/400/300" />
    </div>
    
    <div class="container" flashlighted>
      <h1>Lorem Ipsum</h1>
      <h3>Lorem ipsum dolor sit amet</h3>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
    </div>
    
    <div class="container" flashlighted>
      <h1>Lorem Ipsum</h1>
      <h3>Lorem ipsum dolor sit amet</h3>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
    </div>
    Login or Signup to reply.
  3. Instead of adding a new div on top of the text, use a pseudo-class and add a circular radiant with a big hole in the middle. When moving the cursor over the box, update the CSS variables that control the background-position of the pseudo-element.

    [edit]

    I also made the image into a pseudo-element to remove some javascript code. getBoundingClientRect is compute-heavy so I moved it outside. This means that the javascript code must run after the first paint of the HTML code.

    Finally, it’s not proper to hard code pixels like I did, but you can probably use the rect variable now when it’s outside the mousemove event.

    const box = document.querySelector(".box");
    const rect = box.getBoundingClientRect();
    
    box.addEventListener("mousemove", (e) => {
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
    
      box.style.setProperty("--flashlight-x-pos", `${x - rect.width / 2}px`);
      box.style.setProperty("--flashlight-y-pos", `${y - rect.height / 2}px`);
    });
    
    box.addEventListener("mouseleave", () => {
      const inset = box.style.getPropertyValue("--flashlight-inset");
    
      box.style.setProperty("--flashlight-x-pos", inset);
      box.style.setProperty("--flashlight-y-pos", inset);
    });
    body {
      font-family: sans-serif;
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    
    .box {
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: none;
      width: 100%;
      padding: 5rem;
    
      overflow: hidden;
      position: relative;
      
      --flashlight-y-pos: -200px;
      --flashlight-x-pos: -200px;
      --flashlight-inset: -300px;
    }
    
    .box::before {
      content: '';
      position: absolute;
      inset: var(--flashlight-inset);
      background-image: radial-gradient(circle, transparent 0%, rgba(47,52,2,0.4) 60px, black 70px, black 100%);
      background-position: var(--flashlight-x-pos) var(--flashlight-y-pos);
      background-repeat: no-repeat;
    }
    
    .box::after {
      content: '🔦';
      font-size: 30px;
      
      position: absolute;
      transform: translate(var(--flashlight-x-pos), var(--flashlight-y-pos));
      
      display: flex;
      align-items: center;
      justify-content: center;
    }
    <body>
      <div id="app">
        <div class="box">
          <h1>Light</h1>
        </div>
      </div>
    
      <script src="src/index.js"></script>
    </body>
    Login or Signup to reply.
  4. Of course you can make it better from now on according to your needs.

    let flashlight = document.querySelector(".flashlight");
    let rect = flashlight.getBoundingClientRect();
    
    flashlight.addEventListener("mousemove", function (e) {
      e.target.style.background = `radial-gradient(circle at ${e.clientX - rect.x}px ${e.clientY - rect.y}px, transparent, #000 200px, #000 100%)`;
    });
    .flashLightContainer {
      width: 100%;
      height: 150px;
      display: flex;
      justify-content: center;
      align-items: center;
      position: relative;
      overflow: hidden;
    }
    .flashLightContainer p {
      background:#f00;
      padding: 5px;
      border-radius: 5px;
      color: #fff;
    }
    .flashlight {
      position: absolute;
      width: 100%;
      height: 100%;
      background: #000;
    }
    <div class="flashLightContainer">
      <div class="flashlight"></div>
      <p>Super secret secret</p>
    </div>
    Login or Signup to reply.
  5. It is hard to understand what you are trying to achieve… As an option, you can use box-shadow.
    Something like this:

    wrapper.addEventListener('mousemove', function() {
      const { left, top } = this.getBoundingClientRect();
      cursor.style.transform = `translate3d(${event.clientX - left}px, ${event.clientY - top}px, 0)`;
    })
    
    wrapper.querySelector('h1').addEventListener('click', () => alert('Clicked!'))
    body {
      height: 100vh;
      display: grid;
      place-items: center;
      margin: 0;
    }
    
    #wrapper{
      width: 240px;
      height: 240px;
      position: relative;
      display: grid;
      place-items: center;
      overflow: hidden;
    }
    
    #wrapper:after {
      content: '';
      position: absolute;
      inset: 0;
      background: black;
      pointer-events: none;
    }
    
    #wrapper:hover:after {
      display: none;
    }
    
    #wrapper:hover #cursor {
      display: grid;
    }
    
    #cursor {
      position: absolute;
      width: 100px;
      height: 100px;
      margin: -50px 0 0 -50px;
      box-shadow: 0 0 0 100vmax black;
      border-radius: 50%;
      will-change: transform;
      place-items: center;
      pointer-events: none;
      left: 0;
      top: 0;
      display: none;
    }
    <div id="wrapper">
      <h1>Click me</h1>
      <div id="cursor">
        <img src="https://picsum.photos/50" alt="">
      </div>
    </div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search