October 7, 2023

523

views

Spotlight Effect

A radial gradient that follows the mouse.


Crafts

Build, concepts, techniques and solutions

Lorem ipsum

$79lorem ipsum

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas id vehicula sapien.


Surface

First, we should develop the surface where the Spotlight Effect will occur:

Spotlight.tsx
export function Spotlight() {
  return <div className="spotlight" />;
}
styles.css
.spotlight {
  background: #181e33;
  border-radius: 0.75rem;
  border: solid 1px rgb(255 255 255 / 0.075);
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
  max-width: 24rem;
  min-height: 15.5rem;
  padding: 2.5rem 2rem;
  width: 100%;
}

Radial Gradient

Next, we should add the element responsible for the movement, in this case, we are using a pseudo-element for that, but you can use any element you prefer:

styles.css
.spotlight-second {
  /* ... */
  position: relative;
  z-index: 1;
}
 
.spotlight::before {
  background: radial-gradient(
    250px at center,
    rgba(130, 147, 248, 1),
    transparent 80%
  );
  border-radius: 0.75rem;
  content: "";
  inset: -1px;
  position: absolute;
  transition-duration: 0.3s;
  transition-property: background-color, opacity;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  z-index: -1;
}

Movement

Now, to perform the movement, we will use CSS custom properties (variables) to store the current mouse position. This approach is more performance-oriented because it doesn't depend on any state from any framework you might be using; everything will be handled by the browser, and JavaScript will be responsible only for updating the values of the CSS variables.

Since we're using React, we'll use useRef to reference the DOM element we'll manipulate. In the mousemove event handler, we'll calculate the radial-gradient position using the mouse coordinates obtained from MouseEvent.clientX and MouseEvent.clientY, relative to the viewport using the x and y values from getBoundingClientRect.

Finally, we'll store the values we found in CSS variables to inform the CSS about the position the radial-gradient() should take.

Spotlight.tsx
export function Spotlight() {
  const ref = useRef<ElementRef<"div">>(null);
 
  function handleMouseMove(event: React.MouseEvent<ElementRef<"div">>) {
    if (!ref.current) return;
    const { x, y } = event.currentTarget.getBoundingClientRect();
 
    ref.current.style.setProperty("--x", String(event.clientX - x));
    ref.current.style.setProperty("--y", String(event.clientY - y));
  }
 
  return <div ref={ref} className="spotlight" onMouseMove={handleMouseMove} />;
}

Now that the position values are being added, we can access them within the CSS and set the position of the radial-gradient() relative to the element's background. A trick to facilitate the conversion of values to pixels is to use the calc() function.

styles.css
.spotlight {
  /* ... */
}
 
.spotlight::before {
  background: radial-gradient(
    250px at center,
    250px at calc(var(--x, 0) * 1px) calc(var(--y, 0) * 1px),
    rgba(130, 147, 248, 1),
    transparent 80%
  );
  /* ... */
}

Hiding and Revealing

To finalize, we should hide the element until the mouse is over our card. To achieve this, we can use opacity.

styles.css
.spotlight {
  /* ... */
}
 
.spotlight::before {
  /* ... */
  background: radial-gradient(
    250px at calc(var(--x, 0) * 1px) calc(var(--y, 0) * 1px),
     rgba(130, 147, 248, 1),
    550px at calc(var(--x, 0) * 1px) calc(var(--y, 0) * 1px),
     rgba(130, 147, 248, 0.25),
    transparent 80%
  );
  opacity: 0;
}
 
.spotlight:hover::before {
  opacity: 1;
}

Complete

Lorem ipsum

$79lorem ipsum

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas id vehicula sapien.

Now that it's complete, change the transition-duration values to find the one that suits you best.

Spotlight.tsx
export function Spotlight() {
  const ref = useRef<ElementRef<"div">>(null);
 
  function handleMouseMove(event: MouseEvent<ElementRef<"div">>) {
    if (!ref.current) return;
    const { x, y } = event.currentTarget.getBoundingClientRect();
 
    ref.current.style.setProperty("--x", String(event.clientX - x));
    ref.current.style.setProperty("--y", String(event.clientY - y));
  }
 
  return (
    <div ref={ref} className="craft-spotlight" onMouseMove={handleMouseMove}>
      {...}
    </div>
  );
}
styles.css
.spotlight {
  background: #181e33;
  border-radius: 0.75rem;
  border: solid 1px rgb(255 255 255 / 0.075);
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
  max-width: 24rem;
  min-height: 15.5rem;
  padding: 2.5rem 2rem;
  position: relative;
  width: 100%;
  z-index: 1;
}
 
.spotlight::before {
  background: radial-gradient(
    550px at calc(var(--x, 0) * 1px) calc(var(--y, 0) * 1px),
    rgba(130, 147, 248, 0.25),
    transparent 80%
  );
  border-radius: 0.75rem;
  content: "";
  inset: -1px;
  opacity: 0;
  position: absolute;
  transition-duration: 0.3s;
  transition-property: background-color, opacity;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  z-index: -1;
}
 
.spotlight:hover::before {
  opacity: 1;
}