Combinig Css mask, Radial-gradients and a smitten of Javascript
Earlier this week i created a little codepen (check here if you cant wait) that combines css masks with linear-gradients to create a spotlight effect, and i added a little Javascript to make the light follow the mouse cursor.
People seem to like the demo, so in this article i will explain the concepts behind it for anyone that wants to know how it works.
Let's get started!
The background image
First we have to set a background image for the body element. You could also do this on a div, or whatever element has your fancy, if you want to use it in a section of your site.
body {
margin: 0;
height: 100vh;
background-image: url(https://unsplash.it/1920/1080);
background-position: center center;
background-size: cover;
}
In this snippet we first remove the margins of the body so we wont have any white space surrounding our mask later on. We also make the body the same height as the viewport by setting its height to 100vh (Viewport Height)
And lastly we grab a random 1920 x 1080 image from the unsplash library, set its position to the center and give it a size of cover so that it covers the entire element/viewport.
The mask element
For our actual mask we are gonna create a div that also covers the entire viewport.
<div class="mask"></div>
To make it the same size as the viewport we set its width to 100vw (Viewport Width) and just like the body we set its height to 100vh.
.mask {
width: 100vw;
height: 100vh;
background-color: hsla(0,0%,0%, 0.9);
}
To create the darkened effect we set the background color to black and give it a opacity of 0.9 using the hsla function
if your not familiar with the hsl and hsla functions i have a video that you can check out here: Css Hsl colors introduction
Css mask and radial-gradient
The first piece of the magic is the combination of css mask and radial gradient.
As the name implies the css mask property alows us to create masks for our elements in a similar way we create masks in photoshop. But imstead of black and white in this case we use black and transparent.
.mask {
mask: radial-gradient(
circle at 50% 50%,
transparent 40px,
black 150px
);
}
So here we set the mask property on the .mask div and set its value to a radial-gradient function.
The first line "circle at 50% 50%," sets the center of the gradient to the center of the screen.
Next we set a transparent color up to 40px from the center, and then we add black that starts at 150px and everything in between will be a transition from transparent to black. Resulting in a transparent circle that fades out to black.
Custom properties
Because we want our mask to move we have to make the position of our gradient "hole" variable. So lets replace those values with some css custom properties.
First we have to define these properties to give them a starting value. In this case i define them on the :root but you can also do this on the .mask element itself.
:root {
--mouse-x: 50%;
--mouse-y: 50%;
}
And now we can use them inside out radial-gradient function.
.mask {
mask: radial-gradient(
circle at var(--mouse-x) var(--mouse-y),
transparent 40px,
black 150px
);
}
Make all browsers behave well
Browser support for mask goes pretty much across the board
But we do have to add a -webkit-mask to make sure all browsers do what we want.
.mask {
...
-webkit-mask: radial-gradient(
circle at var(--mouse-x) var(--mouse-y),
transparent 40px,
black 150px
);
}
So our completed css should look like this
:root {
--mouse-x: 50%;
--mouse-y: 50%;
}
body {
margin: 0;
height: 100vh;
background-image: url(https://unsplash.it/1920/1080);
background-position: center center;
background-size: cover;
}
.mask {
width: 100vw;
height: 100vh;
background-color: hsla(0,0%,0%, 0.9);
mask: radial-gradient(
circle at var(--mouse-x) var(--mouse-y),
transparent 40px,
black 150px
);
-webkit-mask: radial-gradient(
circle at var(--mouse-x) var(--mouse-y),
transparent 40px,
black 150px
);
}
Move things with javascript
To move our gradient to match our mouse cursor position we need just a little bit of javascript.
So lets start by adding an eventlistener to get the position of the mouse.
document.addEventListener('pointermove', (pos) => {
}
Inside our event listener we recieve a pos variable that holds the pos.clientX and pos.clientY properties that represent the x and y position of the cursor respectively!
The radial-gradient sets its position based on % values, so we have to convert our mouse position to a 0 to 100% value.
document.addEventListener('pointermove', (pos) => {
let x = parseInt(pos.clientX / window.innerWidth * 100);
let y = parseInt(pos.clientY / window.innerHeight * 100);
});
I'm not much of a math person so please don't ask me to tell you how this works exactly. The only thing i know is that it works and that it gives us 0-100% values for the x and y positions! :p
The parseInt is there to make sure we get an actual number. It doesn't matter much in this case though.
Now that we have the correct values we can update our custom properties.
const mask = document.querySelector('.mask');
document.addEventListener('pointermove', (pos) => {
let x = parseInt(pos.clientX / window.innerWidth * 100);
let y = parseInt(pos.clientY / window.innerHeight * 100);
mask.style.setProperty('--mouse-x', x + '%');
mask.style.setProperty('--mouse-y', y + '%');
});
The first line here gets us a reference to our mask element so we can manipulate its properties.
We then call mask.style.setProperty twice to update the values of said properties. The first argument passed is the name of the property we want to update, and the second is the value we want to set. In our case we tag on a percent symbol to make it the correct unit.
The completed js code should be like this now.
const mask = document.querySelector('.mask');
document.addEventListener('pointermove', (pos) => {
let x = parseInt(pos.clientX / window.innerWidth * 100);
let y = parseInt(pos.clientY / window.innerHeight * 10);
mask.style.setProperty('--mouse-x', x + '%');
mask.style.setProperty('--mouse-y', y + '%');
});
We should now have a functional spotlight effect as shown in the code pen below. Wiggle your mouse over the pen!
Going a bit more advanced with Gsap, you know... just because we can.
Although its complete overkill for this use case i'm a massive not affiliated Gsap fan boy, so if you'll let met i'm gonna take this opportunity to show you a very tiny bit of it.
Gsap is "Professional-grade JavaScript animation for the modern web". In normal terms its just an amazing library for animating things in javascript. Or react if that's your thing!
I guess its almost everybody's thing! :p
But don't worry... in this case we are just going to use two Gsap functions.
I am working on some other content that goes a bit deeper into the css/javascript combination and it will also cover some more of the Gsap library. So make sure to give me a follow here on Dev or on twitter if you're curious about that!
Now lets get Gsap into the project.
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
Nice and simple! Now lets grab a helper function called mapRange from gsap.utils.
const {mapRange} = gsap.utils;
Again im not very good at math, but mapRange is something i can explain. or at least im gonna give it a go.
Like the name implies mapRange maps one range to another range. So lets say we have a 0 to 1000 range, and we have another 0 to 100 range. If in the first range our value is 100, then it would map to 10 in the second range.
0-1000: 100
0-100: 10
I hope that makes some sense!? Anyway, our mouse's x position is somewhere between 0 and the width of the viewport (window.innerWidth) and we have to map that to a value between 0 and 100%. We can do that by using the mapRange function like shown below.
document.addEventListener('pointermove', (pos) => {
let x = mapRange(
0, window.innerWidth,
0, 100,
pos.clientX
);
});
Like i said we are mapping a value between 0 and window.innerWidth to a value between 0 and 100, and we pass it the actual value pos.clientX being the cursor x position.
We can then repeat this to get the y value by swapping innerWidth with innerHeight.
document.addEventListener('pointermove', (pos) => {
let x = mapRange(
0, window.innerWidth,
0, 100,
pos.clientX
);
let y = mapRange(
0, window.innerHeight,
0, 100,
pos.clientY
);
});
Now that we got our values again we can use the gsap.set function to update our custom properties.
document.addEventListener('pointermove', (pos) => {
...
gsap.set(mask,{
'--mouse-x': x + '%'
})
gsap.set(mask,{
'--mouse-y': y + '%'
})
});
Gsap.set takes two parameters. The first is the element we want to set the values for. In our case we pass the mask reference but you can also pass a string being the css selector for the element. So we could have used ".mask" and it wuold do the same thing.
The second paramater should be an object holding key value pairs for the properties we want to set and their respective values. In this case we use the custom property names for the key and the x and y values we created plus an % symbol.
Al this combined we should result in the code below.
const mask = document.querySelector('.mask');
const {mapRange} = gsap.utils;
document.addEventListener('pointermove', (pos) => {
let x = mapRange(
0, window.innerWidth,
0, 100,
pos.clientX
);
let y = mapRange(
0, window.innerHeight,
0, 100,
pos.clientY
);
gsap.set(mask,{
'--mouse-x': x + '%'
})
gsap.set(mask,{
'--mouse-y': y + '%'
})
});
We really had no need to use Gsap here, but this is just a tiny bit of what it can do, and i hope it got you curious to explore some more of it.
Here's a codepen with the updated code.
Follow?
Follow me on Youtube, Twitter or here on Dev.to @Vanaf1979 for more things to come.
Oh... The image i used in the codepens was kindly provided for free by Todd Quackenbush on Unsplash.
Thanks for reading, stay safe and stay the right kind of positive!
Top comments (9)
Wow really cool one Stephan!
Reminds me of my zoom function, and never thought about using it like this.
Must give this a try, looks like a fun project.
Thanks Chris,
What is your zoom function? Can ji view that somewhere?
This one: daily-dev-tips.com/posts/vanilla-j...
Oh that's very cool! Now i need to make my own version of this one! :p
Damn, Stephan! This is nifty. I wouldn't have thought of any of this. Great work.
Thanks Tyler,
This thing has been bugging me for quite a while, and the found the last piece of the puzzle last week! haha
It's a pretty sweet "stack" of techniques that has a lot of potential!
This is going to be useful in a horror game! Nice!
Very Cool
Thanks Naimur! ๐