CSS3 Trickery: A Lesson in Gradients and Masks

Posted by in Code, Design

I’m currently working on a project where one of the designs I’m to implement involves making one of these suckers:

However, that’s not all. Based on where the user is amidst these four steps, it will actually end up looking something like this:

Normally, I’d achieve this with CSS sprites and be rather sad about it all since whatever I’d be doing would be either a) not semantically correct, or b) terribly convoluted. Luckily, this project is specified to only having to run on Chrome, which I finally get to use some CSS3 and not care who knows it.

The First Attempt

Let’s back up a bit, though. Before the designer got his hands on this project, I was actually both designing and developing the entire shebang, so I actually had the entire thing already coded out, HTML, CSS, Javascript, PHP, and all. However, the company I work for finally got around to hiring a real designer, so out went my intuitive and standards-compliant touch interface and in came a guy with a graphic design degree who thought 18px was tall enough for buttons in a touch UI.

Anyways, here was my original markup for these steps:

1
2
3
4
5
6
<ul id="steps">
    <li class="done">1. Prescription</li>
    <li>2. Lenses</li>
    <li>3. Coatings</li>
    <li>4. Checkout</li>
</ul>

Simple enough, right? In fact, if those black lines weren’t diagonal, this whole problem could be solved with some border-right and :last-child action in addition to the box-shadow and border-radius styles I already had in place. Unfortunately, they’re about 30° from being easy, so I had to think of something else.

Starting out, I thought I would just apply both the gray and red gradients directly to the <li> elements and use -webkit-transform to rotate() a 1px wide pseudo element. Well, bad idea. That line of thinking ran me into a predicament that looked a bit like this:

You’ll notice that line is aliased to an unacceptable degree, not to mention the fact that having red and gray states next to each other created competing borders that I couldn’t think of a way to elegantly mask, so I had to change up tactics.

Progress!

My first step was to keep the gray gradient wholly contained to the actual <ul> element rather than each individual <li> but keep those pumping out the red gradients. This would help avoid the issue I had with my original solution where two borders at equal z-indexes were vying for dominance.

Then, I inserted a pseudo element that simply rendered a diagonal gradient that went from transparent to (almost) black to transparent with some slight fading for aesthetic appeal.

1
2
3
4
5
6
7
8
9
10
#steps li:before {
    content:'';
    display:block;
    position:absolute;
    width:40px;
    height:54px;
    top:0;
    left:-20px;
    background:-webkit-gradient(linear, left 14, right 40, color-stop(0%, transparent), color-stop(48%, transparent), color-stop(50%, #676767), color-stop(50%, #676767), color-stop(52%, transparent), color-stop(100%, transparent));
}

If you can’t tell, this is the diagonal line. It’s not as straightforward or as neat as using -webkit-transform, but it renders way better on the screen.

However, this still didn’t solve the issue of mixing diagonal lines with vertical gradients. No, that had to be rectified with another pseudo element.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#steps li.done:before, #steps li.done + li:not(.done):before {
    background:
    -webkit-gradient(linear, left 14, right 40, color-stop(0%, transparent), color-stop(48%, transparent), color-stop(50%, #592327), color-stop(50%, #592327), color-stop(52%, transparent), color-stop(100%, transparent)),
    -webkit-gradient(linear, left top, left bottom, color-stop(0%, #CA263D), color-stop(100%, #850304));
}
#steps li.done + li:not(.done):not(.current):after {
    content:'';
    display:block;
    position:absolute;
    width:40px;
    height:54px;
    top:0;
    left:-20px;
    background:
    -webkit-gradient(linear, left 14, right 40, color-stop(0%, transparent), color-stop(48%, transparent), color-stop(50%, #592327), color-stop(50%, #592327), color-stop(52%, transparent), color-stop(100%, transparent)),
    -webkit-gradient(linear, left top, left bottom, color-stop(0%, #E2E2E2), color-stop(100%, #949494));
    -webkit-mask-box-image:-webkit-gradient(linear, left 14, right 40, color-stop(0%, transparent), color-stop(51%, transparent), color-stop(51%, #000000), color-stop(100%, #000000));
}

While admittedly this isn’t the most beautiful piece of code I’ve ever come up with, it does work and works well. The :before changes for .done states by using two gradients for its multiple backgrounds: one diagonal for the line and one vertically for the red background. This leaves a red unwanted triangle on neighboring red/gray steps, so the :after element recreates the gray gradient and applies a diagonal mask to the top left portion of it.

Booya.

View the demo (requires you to be running a WebKit browser).