My digital business card is inspired by this amazing 3d pokemon cards demo demo and also this twitter postwhich uses a similar effect.

I want to break down some of the techniques involved in a multi-part blog series. In this post we’ll focus on just the soft spotlight effect.

The effect is extremely subtle when you interact with these 3D cards. it mimics the reflection of a light source shining down on the card. This effect adds that extra level of realism to your 3D card.

Here’s the effect that we’re going to produce:

Let’s break this down, line by line.


<div class='card'>
  <div class="card__softlight"></div>

The .card is our container element which defines the element dimension and houses the inner element.

The spotlight effect is all achieved with the .card__softlight class.

The CSS🔗

.card {
  position: relative;
  height: 500px;
  width: 900px;
  background: grey;

.card__softlight {
  position: absolute;
  mix-blend-mode: soft-light;
  background: radial-gradient(farthest-corner circle at var(--x, 10%) var(--y, 10%),
    rgba(255, 255, 255, 0.8) 10%,
    rgba(255, 255, 255, 0.65) 20%,
    rgba(255, 255, 255, 0) 90%);

Some basic stuff in the card class. As the children element will be absolute positioned we need to remember to add position: relative so all children elements are contained relative to this element.

Moving into .card__softlight. we use inset: 0 to make this inner div “fill up” to the parent container. it’s the equivalent to :

  top: 0;
  right: 0;
  bottom: 0;
  left: 0;

There are a few things happening in the background property. We’re using the radial-gradient function to create a circular pattern with a subtle white glow effect.

if you want to see the radial gradient effect a bit clearer you can try swapping it for

background: radial-gradient( circle farthest-side at var(--x, 0%) var(--y, 10%), red 10%, green 20%, blue 80%);

Let’s take a deep dive into how the radial-gradient function works.

MDN documents the radial-gradient function like so:

radial-gradient( [ <ending-shape> || <size> ]? [ at <position> ]? , <color-stop-list> ) 

<ending-shape> - can either be circle or elipse. elipse is basically just a stretched circle to match the aspect ratio of the element it’s in. For our spotlight effect, we want to ensure the radial gradient is always a circle.

Try swapping circle for elipse to see how the radial gradient skews.

<size> - has four options documented here

This means we can omit farthest-corner and it would still function the same. I’ve kept it for explicitness.

try swapping farthest-corner for one of the other options. Maybe you think a different one looks better. it’s completely subjective!

<position> - defaults to center but supports x and y positions. Note, for this argument we’re using var(--x, 10%) and var(--y, 10%). These are CSS variables, the second argument in a CSS variable is the fallback value if either --x or --y has not been set yet. In the next section we will set --x and --y dynamically using javascript!

The final bit of magic is mix-blend-mode: soft-light we will make heavy use of mix-blend-mode throughout this tutorial series. This property will literally blend the radial background into the other elements. it is key to making the spotlight effect feel “softer”. Learn more about mix-blend-mode on MDN

try removing this property and experimenting with other blend values e.g multiply, hard-light or difference.

##\ The Javascript

With our CSS variables in place, we can now programmatically update the x and y positions of the radial gradient.

Firstly, we need to tap into the mousemove event to get the position our our cursor when we hover over our card. We can do so like this:

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

card.addEventListener("mousemove", (e) => {
	console.log(e.clientX , e.clientY) //e.g 130 30  

We have a problem though, the values returned from e.clientX and e.clientY are pixel positions of where the cursor is on the screen. Here is an example of the values that will be returned by e.clientX and e.clientY .

We want relative units within the card element itself like so:

To achieve this we need the following values:

  1. The size of the card element
  2. The position of the element relative to the viewport.

Fortunately, we have everything we need within Element.getBoundingClientRect() . This method which exists on any HTML element returns an object that looks something like:

    "x": 8,
    "y": 8,
    "width": 900,
    "height": 500,
    "top": 8,
    "right": 908,
    "bottom": 508,
    "left": 8

We can now work out the relative unit of the cursor position within the .card element with some basic maths!:

Here’s the logic we’ll need to write:

Let’s convert that pseudo logic into javascript:

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

const {
  width: cardWidth,
  height: cardHeight,
  left: cardLeft,
  top: cardTop
} = card.getBoundingClientRect();

card.addEventListener("mousemove", (e) => {
  let X = (e.clientX - cardLeft) / cardWidth;
  let Y = (e.clientY - cardTop) / cardHeight;

  let cardXPercentage = `${X * 100}%`;
  let cardYPercentage = `${Y * 100}%`;

  console.log(cardXPercentage, cardYPercentage); // e.g "50%" "50%""--x", cardXPercentage);"--y", cardYPercentage);

Note the last two lines is where the fun happens:"--x", cardXPercentage);"--y", cardYPercentage);

We set our percentage values into our CSS variables with CSSStyleDeclaration.setProperty(). This is how as we move our cursor the radial gradient follows along 🙌.

In the next part we’ll explore how the tilt effect works,