Creating Directionally Lit 3D Buttons with CSS

Jhey Tompkins
Jhey Tompkins
Published in
·Updated:

Share this article

Creating Directionally Lit 3D Buttons with CSS
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
  • Premium Results
  • Publish articles on SitePoint
  • Daily curated jobs
  • Learning Paths
  • Discounts to dev tools

7 Day Free Trial. Cancel Anytime.

I’m not too sure how I stumbled into this one. But something led me to this tweet:

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:

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.