Creating Directionally Lit 3D Buttons with CSS

I’m not too sure how I stumbled into this one. But something led me to this tweet:
Has anyone done this directional lighting cursor interaction with CSS? pic.twitter.com/zLL7Sk6kW5
— Jed Bridges (@JedBridges) July 1, 2020
And, to me, that’s a challenge.
The button design is neat. But I didn’t want to do a direct copy. Instead, we decided to make a “Twitter” button. The idea is that we create an almost transparent button with a social icon on it. And then that social icon casts a shadow below. Moving our mouse across the button shines a light over it. Pressing the button pushes it onto the surface. Here’s the final result:
Directional Lighting 3D CSS Twitter Button 🐦
👉 https://t.co/qpfzEwUMey via @CodePen pic.twitter.com/zWfwtPaixo
— Jhey 🐻🛠 (@jh3yy) January 30, 2021
In this article, we’re going to look at how you can make it too. The cool thing is, you can swap the icon out to whatever you want.
Key Takeaways
- Utilize CSS and HTML to create a 3D button with dynamic lighting effects that respond to cursor movements, enhancing user interaction.
- Implement Pug to efficiently generate SVG icons for buttons, allowing easy swaps and adjustments to different icons using mixins.
- Achieve a 3D visual effect by manipulating CSS properties such as `transform-style: preserve-3d;` and using variables to control depth and perspective.
- Enhance accessibility by using `aria-hidden` attributes and ensuring that interactive elements are perceivable via screen readers.
- Apply CSS transitions and transformations to simulate physical interactions like pressing the button, adding realism to the user experience.
- Use JavaScript for dynamic effects, such as moving a light shine across the button based on cursor position, with GSAP for smooth animations.
The Markup
My first-take approach for creating something like this is to scaffold the markup. Upon first inspection, we’ll need to duplicate the social icon used. And a neat way to do this is to use Pug and leverage mixins:
mixin icon()
svg.button__icon(role='img' xmlns='http://www.w3.org/2000/svg' viewbox='0 0 24 24')
title Twitter icon
path(d='M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z')
Here, we’ve created a mixin for rendering an SVG of the Twitter icon. This would render the Twitter icon if we invoke it like so:
+icon()
Doing that will give us a big Twitter icon.
See the Pen
1. Render An Icon by SitePoint (@SitePoint)
on CodePen.
Because social icon sets tend to use the same “0 0 24 24” viewBox
, we could make the title and path arguments:
mixin icon(title, path)
svg.button__icon(role='img' xmlns='http://www.w3.org/2000/svg' viewbox='0 0 24 24')
title= title
path(d=path)
Then our Twitter icon becomes
+icon('Twitter Icon', 'M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z')
But, we could pass it a key — and then have the paths stored in an object if we have many icons we wanted to use or repeat:
mixin icon(key)
-
const PATH_MAP = {
Twitter: "M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"
}
svg.button__icon(role='img' xmlns='http://www.w3.org/2000/svg' viewbox='0 0 24 24')
title= `${key} Icon`
path(d=PATH_MAP[key])
+icon('Twitter')
This can be a neat way to create an icon mixin to reuse. It’s a little overkill for our example, but worth noting.
Now, we need some markup for our button.
.scene
button.button
span.button__shadow
+icon('Twitter')
span.button__content
+icon('Twitter')
span.button__shine
It’s always good to be mindful of accessibility. We can check what our button gives off by checking the Accessibility panel in your browser’s developer tools.