Spotlight Button Animation Jun 22
Spotlight effect on button background that follows the mouse around while hovering.
Created by Vercel
About this component
This effect has become quite popular recently. The version above comes from Vercel, on their latest event page. Another good example is the CTA button on Harvey.AI, who have given it a little more of a neon effect.
The spotlight adds extra interest to a button, and makes the whole design feel more luxurious. It's done by using a radial gradient as the background, and then animating the gradient's location based on the mouse position.
How to build it
The basic principle of this animation is to set a radial-gradient()
as the background of the button, and then animate the gradient's location based on the mouse position.
Note: In the Vercel example, they're also animating the border of the button. I've chosen to leave that out of this explanation to keep it simple. I'll go into more detail about the border effect in a future post.
Three steps
To build the animation, and have it follow the mouse position, we'll have to do three things:
- Add a
radial-gradient()
background to the button using the::after
pseudo-element - Add an event listener to track the mouse position on the button
- Connect the mouse position state to the gradient location
Note: I'm only going to mention the CSS necessary for the animation itself. The rest of the styles can be found in the CodeSandBox at the end.
Step 1: Creating the radial gradient
We're starting with just one simple element, a <button>
. To create the radial-gradient, we have to add some styles to the button itself, and to the ::after
pseudo-element.
Let's start with the radial gradient. By using CSS variables instead of hard-coded values, we can easily update the location of the gradient later on.
button {
--left-value: 0px;
--top-value: 0px;
}
button::after {
background: radial-gradient(
circle at var(--left-value) var(--top-value),
#b8b8b8,
#0000000f
);
}
To make sure the ::after
element (and in turn, the gradient) covers the entire button, we need to add the code below.
button {
--left-value: 0px;
--top-value: 0px;
position:relative;
}
button::after {
background: radial-gradient(
circle at var(--left-value) var(--top-value),
#b8b8b8,
#0000000f
);
position: absolute;
inset: 0px;
content: "";
}
By using inset:0
along with position absolute
, the ::after
pseudo-element will span from the left to the right, and from the top to the bottom of the button. Inset is a shorthand property for setting left
, right
, top
, and bottom
all at the same time.
Position relative
is necessary to make sure the ::after
element is positioned relative to the button itself, and not to the viewport.
Finally, we need to add content:""
, because without any value for content
, the ::after
element won't show up at all.
Tip: The reason we're using the
::after
pseudo-element is mostly a matter of convenience, as it means we don't have to add an unnecessary empty div to the button. You could also use a div, and then add the gradient as a background to that div. Read more about::after
on CSS Tricks.
Step 2: Listening for the mouse position
Since we want the spotlight to follow the mouse around, we need to know where the mouse is located exactly. Because the radial gradient is positioned relative to the button, we're looking for the relative mouse position on the button itself, measured from the top left corner.
To calculate the relative mouse position on the button, we need to have the position of the mouse within the viewport, and the position of the button within the viewport.
In order to get those positions, we'll listen for the onMouseMove
event on the button, which will enable us to get some data about the mouse position. This is updated every time you move the mouse on the button.
We'll pass this event to the getRelativeCoordinates
function that will then calculate the relative mouse position on the button.
const getRelativeCoordinates = (event) => {
const rect = event.target.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
To understand what this function does, let's break it down into its parts.
event.target.getBoundingClientRect()
returns data about the size and position of the button.- 'Left' and 'top' are the horizontal and vertical positions of the button within the viewport.
event.clientX
returns the horizontal position of the mouse within the viewport.event.clientY
returns the vertical position of the mouse within the viewport.
If you subtract the distance of the button from the edges of the viewport (rect.left
) from the distance of the mouse from the edges of the viewport (event.clientX
), you get the distance of the mouse from the edges of the button (x
).
To make this more clear, I've added a visual example below. Try hovering to see the different distances in action.
x
e.clientX
rect.left
y
e.clientY
rect.top
Note: I've rounded these values to integers for presentation purposes, but they can actually be many decimals long, so it's a little more fuzzy than it looks in this example.
Remember that where the mouse is on the button, is where we want the radial-gradient()
to be centered.
const [mousePosition, setMousePosition] = useState({
x: 0,
y: 0,
})
const handleMouseMove = (event) => {
setMousePosition(getRelativeCoordinates(event))
}
<button onMouseMove={handleMouseMove} />
Whenever the mouse is moved, the onMouseMove
event fires, and updates the mousePosition
state with the returns of the getRelativeCoordinates
function.
const [mousePosition, setMousePosition] = useState({});
const handleMouseMove = (event) => {
setMousePosition(getRelativeCoordinates(event));
};
And then updating CSS variables accordingly.
<button
style={{
"--left-value": `${mousePosition.x}px`,
"--top-value": `${mousePosition.y}px`
}}
onMouseMove={handleMouseMove}
>
</button>
As you can see, the radial gradient is now following the mouse around. Moving the mouse changes the mousePosition
state, which in turn updates the CSS variables, on which the radial-gradient()
depends.
Bonus: Adding a smooth transition
To make the spotlight fade in and out smoothly, we can add a subtle transition. First we have to know when hovering starts and ends, then we can set the opacity
of the spotlight to fade in or out accordingly.
First, create a new state to keep track of whether the mouse is hovering over the button or not. Also add two state updater functions to handle the onMouseEnter
and onMouseLeave
events.
const [showSpotlight, setShowSpotlight] = useState(false);
const handleHoverStart = () => {
setShowSpotlight(true);
};
const handleHoverEnd = () => {
setShowSpotlight(false);
};
Then add these event listeners to the button, and use the showSpotlight
state to set the opacity of the spotlight. Just like before, we're using a CSS Variable to connect the React and JavaScript changes with the CSS.
<button
style={{
"--after-opacity": showSpotlight ? 0.5 : 0.2
}}
onMouseEnter={handleHoverStart}
onMouseLeave={handleHoverEnd}
/>
Finally, using the --after-opacity
CSS variable, we can update the opacity of the spotlight in CSS. Add a transition to make it fade in and out smoothly, instead of just appearing and disappearing.
button::after {
opacity: var(--after-opacity);
transition: opacity 0.5s ease-out;
}
The final result with the smooth transition is a lot more subtle and elegant than the previous version.
CodeSandBox
Check out the CodeSandBox below to see the full code for this spotlight effect.