https://www.joshwcomeau.com/animation/3d-button/ JoshWComeau * Latest * Posts * Snippets * Goodies HomeTutorialsAnimation Building a Magical 3D Button Bet you can't click just once! Table of Contents IntroductionOur strategyThe detailsFocus outlinesA hover state Injecting personalityAdding a shadowColor and aestheticsStarted from the button now we here Introduction I had a neat realization recently: Buttons are the "killer feature" of the web. Every significant thing we do online, from ordering food to scheduling an appointment to playing a video, involves pressing a button. Buttons (and the forms they submit) make the web dynamic and interactive and powerful. But so many of those buttons are lackluster. They can trigger enormous changes in the real world, but they don't feel tangible at all. The feel like dull everyday pixels. In this tutorial, we'll build a whimsical 3D button: Push Me Intended audience This is an intermediate-level tutorial for front-end developers. It's focused on HTML/CSS, no JavaScript knowledge required. If you're relatively new to CSS transitions, I'd recommend reading "An Interactive Guide to CSS Transitions" first. Link to this heading Our strategy There's one main trick we'll use a couple times in this tutorial to create the illusion of a 3D button. Here's how it works: when the user interacts with our button, we'll slide a foreground layer up and down, in front of a stationary background: Push Me Reveal (Try sliding the "Reveal" slider, and then interacting with the button!) Why not use box-shadow or border? Those properties are super expensive to animate. If we want a buttery-smooth transition on the button, we'll have way more success with this strategy. Here's our MVP button in code: [ ] Enable "tab" key Our button element provides the burgundy background color that simulates the bottom edge of our button. We also strip away the default border/padding that comes with button elements. .front is our foreground layer. it gets a bright pink-crimson background color, as well as some text styles. We'll slide the foreground layer around with transform: translate. This is the best way to accomplish this effect, since transforms can be hardware-accelerated. While the mouse is held down on the button, the :active styles will apply. We'll shift the front layer down so that it sits 2px above the bottom. We could drop it to 0px, but I want to keep the 3D illusion going at all times. Link to this heading The details We've created a solid foundation, and now it's time to build some cool stuff on top of it! Link to this heading Focus outlines Most browsers will add an outline to a button when it's clicked, to indicate that the element has captured focus. Here's what this looks like by default, on Chrome / MacOS: In the MVP above, I took the liberty of adding an outline-offset declaration. This property gives our button a bit of a buffer: css This is a dramatic improvement, but it's still a bit of an eyesore. Plus, it doesn't work consistently: on Firefox, outline-offset doesn't work for the default "focus" outlines. We can't simply remove it, though--that outline is super important for folks who navigate using their keyboard. They rely on it to let them know which element is focused. Fortunately, we can use a swanky CSS pseudo-class to help us out: :focus-visible [ ] Enable "tab" key That's one heck of a selector, so let's break it down. The :focus pseudo-class will apply its declarations when an element is focused. This works regardless of whether the element is focused by tabbing to it on the keyboard, or by clicking it with a mouse. :focus-visible is similar, but it only applies when the element is focused and the user would benefit from seeing a visual focus indicator (because they're using a keyboard to navigate, for example). Finally, :not allows us to mix in some logic. The styles will apply when the element matches the :focus selector, but not the :focus-visible selector. In practical terms, this means that we'll hide the outline when the button is focused and the user is using a pointer device (eg. a mouse, a trackpad, a finger on a touchscreen). Browser support and accessibility I'll be honest about it: the rule above is pretty confusing! Is there a clearer way we could accomplish the same effect? What if we wrote it like this instead? css revert is a special keyword that will revert back to whatever the value ought to be, based on the browser's defaults*. In Chrome on MacOS, this equates to a solid blue line. It's simpler, right? We're saying "Hide the outline, except when visibly focused". Unfortunately, however, this alternative method has a problem: it doesn't work in older browsers. Show more Clicking and focus In most browsers, clicking a button will focus it. Depending on your browser and operating system, however, this might not be true for you! MDN has a great writeup that covers how different browsers behave when clicking buttons. Additionally, in Safari, buttons can be focused via "Option + Tab". In other browsers, the "Tab" key alone is sufficient. Link to this heading A hover state So, in real life, buttons don't rise up to meet your finger before you press on it. But wouldn't it be cool if they did? Let's shift the button up by a few pixels when they hover. Also, let's slap a transition on the front layer. This will animate the state changes, producing a more fluid interaction. [ ] Enable "tab" key I add the will-change: transform declaration so that this animation can be hardware-accelerated. This topic is covered in my Introduction to CSS Transitions. Link to this heading Injecting personality With a blanket transition: transform 250ms, we've given our button an animation, but it still doesn't have much in the way of personality. Let's consider the different actions that can be performed on this button: * It can be pressed * It can be released * It can be hovered * It can be departed from (when the user mouses away) Should each of these actions share the same characteristics? I don't think so. I want the button to snap down quickly when clicked, and I want it to bounce back when released. When the cursor wanders away, I want it to sink back to its natural position at a glacial pace. Here's what that looks like. Try interacting with the button to see the difference: [ ] Enable "tab" key We can set overrides for each state, to change how the animation behaves. In addition to picking different speeds, we can also change the timing functions! Our default transition, inside .front, is applied when the mouse leaves the button. It's our "return to equilibrium" transition. I've given it a leisurely duration of 600ms--an eternity when it comes to micro-interactions. I've also given it a custom easing curve, via cubic-bezier. I'll be writing more about cubic Bezier curves soon. In essence, they let us create our own timing curve. This is a lower-level tool that gives us a ton of control. In the case of our "equilibrium" curve, it's essentially a more-aggressive ease-out: Timeline[0 ] Run animation When we press down on the button, we switch to our :active transition. I've chosen a lightning-quick transition time of 34ms--roughly 2 frames at 60fps. I want this one to be speedy, since this is how people tend to press buttons in real life! Finally, our :hover transition. This state tackles two separate actions: * The rise-up when mousing over the button * The snap-back after releasing the button Ideally, I would pick different transitions for each of these actions, but it isn't possible in pure CSS. If I really wanted to go the extra mile, I'd need to write some JS to disambiguate between these states. I've crafted a "springy" Bezier curve that overshoots a little bit. This gives the button a ton more personality. Here's what this curve looks like: Timeline[0 ] Run animation Ultimately, Bezier curves will never look quite as lush as spring physics, but they can get pretty close with enough tinkering! Link to this heading Adding a shadow To really sell the whole "3D" thing, we can add a shadow: Push Me You may be tempted to reach for box-shadow to accomplish this, but we'll have much more success by repeating a trick we saw earlier. Our shadow will be a separate layer, and it'll move in the opposite direction of our front layer. Push Me Reveal In order for this to work, we'll need to restructure things a bit. Here's the markup for our new setup: html Before, we were using the