loading...
Cover image for How to Create Simple Skeuomorphic Buttons in CSS

How to Create Simple Skeuomorphic Buttons in CSS

jonkantner profile image Jon Kantner ・6 min read

If you’re a fan of skeuomorphism, then this tutorial on creating buttons in that style is for you. Skeuomorphic buttons you find in the wild can take all sorts of forms, but we’ll create a basic one similar to what I’ve used in this Pen.

Normal State

To start off with the initial button state, here’s the HTML you’ll need:

<button>
    <span tabindex="-1">Button</span>
</button>

The reason for the <span> with that tabindex is for pulling off the keyboard-only focus later. We could use :focus-visible in CSS here, but just for deeper browser support we’ll adopt Roman Komarov’s approach as already used.

For better organization and productivity, we’ll do all styling in Sass (SCSS syntax). Then we start with a flat-colored button without any states as well as two variables for the button background and text color ($btnColor, $btnTextColor).

body {
    background: #c7cad1;
}
button {
    $btnColor: #0053d0;
    $btnTextColor: #fff;
    background: $btnColor;
    border: 0 {
        radius: 0.375em;
    };
    color: $btnTextColor;
    cursor: pointer;
    font: 24px/1.5 sans-serif;
    margin: 1.5em auto;
    padding: 0.5em 1em;
    -webkit-tap-highlight-color: transparent;
    &, span {
        display: block;
    }
}

There are a couple key things here:

  • Having a slightly dark enough page background for the body (or whatever the parent element should be) will allow all edges of the button hole to be seen.
  • We clear the -webkit-tap-highlight-color to avoid the brief dark highlight on Apple devices.
  • Both the button and text span should be display: block to allow transitions near the end.

So far, here’s what we have:

Solid blue button with the text “Button”

Now we apply the look. We’ll need more variables after $btnTextColor for the button edge, edge of the button hole, drop shadow color, dark edges, lighter edges, and a dark edge adopting more of the button color.

button {
    . . .
    $btnEdge: 0.2em;
    $btnHoleEdge: 0.1em;
    $dropShadowColor: rgba(0,0,0,0.47);
    $darkEdgeColor: rgba(0,0,0,0.27);
    $lightEdgeColor: rgba(255,255,255,0.27);
    $coloredDarkEdgeColor: darken($btnColor,10%);
    . . .
}

Next, anywhere before &, span { }, the order of box shadows for drawing a bulk of the button will be as follows:

button {
    . . .
    box-shadow:
        // drop shadow
        $btnEdge $btnEdge 0.5em $dropShadowColor,
        // top, left, and right edges of hole
        0 (-$btnHoleEdge) 0 $btnHoleEdge $darkEdgeColor,
        // bottom edge of hole
        0 $btnHoleEdge 0 $btnHoleEdge $lightEdgeColor,
        // right edge of button
        (-$btnEdge) 0 $btnEdge $coloredDarkEdgeColor inset,
        // top edge of button
        0 $btnEdge $btnEdge $lightEdgeColor inset,
        // left edge of button
        $btnEdge 0 $btnEdge $lightEdgeColor inset,
        // bottom edge of button
        0 (-$btnEdge) $btnEdge $coloredDarkEdgeColor inset;
    . . .
}

Where we draw or color the shadows depends on where the direction of light is. In this case, it’ll come from the top left. Therefore, the shadow shall be cast 0.2em to the right and from the top. To explain how everything should be shaded:

  • We use $darkEdgeColor to add slightly dark transparency to hole edges that should receive hardly any light, which would be the top, left, and right.
  • $lightEdgeColor is for adding transparent white to the hole and button edges receiving more light (note: because the drop shadow initially covers it, we’ll see the full effect only when the button is pressed).
  • For a little more color on the button edges receiving minimal light, we apply $coloredDarkEdgeColor rather than $darkEdgeColor.

The texture is kicking in now, but there’s more to go.

Button now having a drop shadow, blurred lighter blue top and left edges and darker blue right and bottom edges, inside a hole with all dark gray edges except for a light gray bottom edge

Let’s spice it up by adding slight shading from top to bottom as well as a small reflection at the top to the current background.

button {
    . . .
    background:
        // top-to-bottom shading
        linear-gradient(rgba(0,0,0,0),rgba(0,0,0,0.13)),
        // reflection at top
        radial-gradient(90% 7% at 50% 8%,rgba(255,255,255,0.47) 25%,rgba(255,255,255,0) 50%),
        $btnColor;
}

When you look at an object from a certain angle, sometimes you can barely see a visible transition of shades. Take this picture of a plastic for example:

Cursor moving up and down on a red cube to show transition from lighter to dark

Running along the side of it with macOS’s Digital Color Meter (or you can click around a similar image with the eyedropper in your favorite image editing tool), you can see the gradual shift in hex from red to a darker red (roughly #eb0011 to #d5000e). That’s the purpose of the first gradient. Depending on the material, you may see some reflection at top edges, which the second gradient illustrates.

The last thing to add to the button’s initial state is this text shadow before &, span { }:

button {
    . . .
    text-shadow: 0 0 $btnEdge fade-out($btnTextColor,0.53);
    . . .
}

The text won’t look so sharp if we provide a slight blur having the same value as $btnEdge and the same color but about half the opacity.

Looking at what we have now, we’re ready to move forward with the button states!

Thin lighter blue radial gradient at top of button, transparent to transparent black linear gradient from top to bottom, slight text shadow added to button text

:active State

To begin with our down state, we need an additional variable $coloredDarkerEdgeColor with a slightly darker blue.

button {
    . . .
    $coloredDarkerEdgeColor: darken($btnColor,20%);
    . . .
}

The darker blue edges of the button ought to become darker while pressed to show the lack of light between button and hole edges.

Then we apply the same stack of shadows with some parameter changes for :active as well as some scaling down for the button text span:

button {
    . . .
    &:active {
        box-shadow:
            // pull shadow inward
            0 0 0 $dropShadowColor,
            // edges of hole stay the same
            0 (-$btnHoleEdge) 0 $btnHoleEdge $darkEdgeColor,
            0 $btnHoleEdge 0 $btnHoleEdge $lightEdgeColor,
            // darker right shadow with a bit of blue to result from button being deep into hole
            (-$btnEdge) 0 $btnEdge $coloredDarkerEdgeColor inset,
            // similar shadow at top and left
            0 $btnEdge $btnEdge $darkEdgeColor inset,
            $btnEdge 0 $btnEdge $darkEdgeColor inset,
            // same as 4th shadow but coming from bottom
            0 (-$btnEdge) $btnEdge $coloredDarkerEdgeColor inset;
        span {
            transform: scale(0.95);
        }
    }
}

To sum up what happens at this point:

  • We change the x- and y-positions to 0 to pull that drop shadow inward.
  • Because nothing should happen to them, we leave the next two box shadows for the edges of the hole alone.
  • The fourth and last shadows is where we apply the darker blue using $coloredDarkerEdgeColor.
  • The lighter shadows before the last shall use the direct opposite color, which would be $darkEdgeColor.
  • At the same time, the button text transform will give a top-down 3D perspective.

What we’ve done here in a nutshell is an optical illusion since it’s merely a change of a couple box shadows and scaling of text.

Now what the button needs is some animation. Let’s add 0.1-second box-shadow and transform transitions to the button and span as well as will-change: transform to the span, which will stop the button from jerking up and down.

button {
    . . .
    transition-property: box-shadow;
    &, span {
        display: block;
        transition: {
            duration: 0.1s;
            timing-function: linear; 
        }
    }
    span {
        transition-property: transform;
        will-change: transform;
    }
    . . .
}

After all that, we have the effect we’re looking for; however, it looks weird to see that native focus ring encircle the button or the text, whether you tab into it or click it. Tackling that will be the final step.

Fully functional button with native focus rings around the button and text

:focus State

Since neither the button nor its text should have that native outline for :focus, let’s take that away with outline: none.

button {
    . . .
    &, span {
        . . .
        &:focus {
            outline: none;
        }
    }
    . . .
}

For a new focus style, we must consider what would best fit the button. If the button were a LED, a change of background color would certainly fit perhaps as well as an extra box shadow with the button color. For a plastic button like this one, a change of text color as well as its shadow works best. So that calls for one more variable ($btnTextFocusColor), and we’ll use a lighter shade of the button color. Right after our :active selector is where we’ll apply the :focus.

button {
    . . .
    $btnTextFocusColor: lighten($btnColor,40%);
    . . .
    &:focus {
        color: $btnTextFocusColor;
        text-shadow: 0 0 $btnEdge fade-out($btnTextFocusColor,0.53);
    }
}

And now we have a complete button! If you click it now, those annoying focus rings won’t be there. Plus, the new focus shouldn’t persist. That’s because the span containing the button text covers it. That way, it’ll look more natural clicking or tapping that button. Therefore, it should be via keyboard only the text changes color just to indicate where the user is tabbing.

Conclusion

All in all, realistic-looking buttons like these or even other form controls don’t require a complicated HTML structure. Although we didn’t use them here, you can even take advantage of using :before and :after if you feel more elements are needed just as I did in this LED switch, which were for the rotating part. By learning to effectively stack gradients and box shadows, you can pull off a lot of detail in the fewest DOM elements possible.

Posted on by:

Discussion

pic
Editor guide