Why do I have to learn geometry?

Recently, I was given the task of implementing a little widget showing the stages of a project. It looks like this:

Can’t be that hard, right?

Each “stage” of a process is shown as a chevron. In order to fit a large process into a smaller space, the text wraps and, more importantly, the chevrons shrink down so that only the most important — the first, the last, and the current stages — still show.

Luckily, someone on my project team pointed me to this implementation of a very similar widget.


Awesome! It implements the chevrons, which are made with crazy large borders — with color only on the left and transparent otherwise — on the :before and :after of each stage to create the pointy parts. So a triangle making the border of the chevron (the :before) is positioned just slightly to the right of the triangle part of the chevron (the :after).

However, I do see some differences — colors first of all, the bounding box around the whole thing, and also, the responsive behavior is different from my requirements. When there are too many steps to fit, it will wrap instead of collapsing as mine needs to. But it’s a great start — let’s go!

Recoloring the chevrons

My first step is to recolor the chevrons. Leaddyno’s implementation is using Less, and so am I, so we get to take advantage of Less variables and mixins. The original only took color as a parameter to the mixin that draws the triangle part of the chevron. It made some assumptions about the size and position. I realized that when I got to drawing the solid versus the bordered chevrons, I was going to need more flexibility. So, I added some parameters to make the mixin more flexible.

.kie-chevron(@color, @height, @width, @top) {
position: absolute;
display: block;
border: @height solid transparent;
border-left: @width solid @color;
border-right: 0;
top: @top;
z-index: 10;
content: ‘’;

I hate putting “magic numbers” in my code. So I defined a few variables in my Less to control the sizing when I call the mixin. Here comes the first math…just divide the height of the widget by two to get the width of the chevron; that’s not too hard, is it?

@kie-wizard-border-width: 2px;
@kie-wizard-row-height: 42px;
@kie-wizard-chevron-width: @kie-wizard-row-height / 2; // because we want a 45 degree angle

Oops, not just recoloring the chevrons…

Once I dug in, I realized that I had some fairly significant differences to take care of. Besides the aesthetic differences of color and chevron angle, Leaddyno’s implementation fills the width of the widget, and the stages wrap onto additional lines if there are too many to fit. I need to wrap text within each stage and then collapse the widget down to the essential stages if there isn’t room for all.

First though, I’ll tackle the colors. I love that I only need to mark one stage in the HTML — the current one. The default styling will be for the past stages, and then I can style the current stage by selecting the .active class. For future stages, .active ~ li — all the lis that are after the .active one — does the trick.

Leaddyno’s nav-wizard puts a border on the entire widget, but I can’t do that because the right end needs to be a chevron. So, a top and bottom border on the li’s it is. Unfortunately, this doesn’t play together with the chevrons too nicely — I’m getting gaps in various places that they don’t belong. You don’t see it in the solid chevrons, but in the bordered ones there are gaps. Remember — we are drawing the dividing lines with triangles, so that little gap is the very tip of the triangle.

Adjusting the parameters for the chevrons to look nice on the bordered stages meant that there was a funny edge in the solid ones.

At this point, I started sketching to get the necessary geometry straight!

I love Flair markers.

And sketching with more detail.

Don’t try to make your own graph paper out of notebook paper.

And sketching on actual graph paper.

If you look closely (which I do not recommend) you will see some square roots of 2. Since the chevron borders are diagonal lines, if we offset them to the right by only the border width, then they will appear to be thinner than the other borders. For a 45 degree angle, the adjustment needs to be a multiplier of sqrt(2). Why? The good old Pythagorean Theorem says a²+b²=c². In a 45–45–90 triangle, the legs will be equal, so

x² + x² = c²
2(x²) = c²
sqrt(2) x = c

So for the completed steps, we can adjust the right attribute of the chevron by (border-width * sqrt(2)) and now the diagonal line is the correct width!

Note: in the end, I made the chevrons narrower to save some space. In that case, since that changes the angle of the line, you’ll have to use some trigonometry to get the correct factor. As any good math teacher I’ve had was happy to say, I’ll leave that as an exercise.

Because the incomplete steps have a border, simply moving the :before chevron out by a factor of sqrt(2) doesn’t work — it creates a gap. So we have to add a right border to fill in the gap. It took me quite a few sketches on graph paper to prove to myself that the width of that border should be (sqrt(2) — 1) * width. Take a look at the third..and fourth…sketches above to check my work.

Have you been following along? Here’s a CodePen with everything in wacky colors so you can see what’s going on.

I like pictures that lay everything out at once, so if you do too, then look at this:

One last piece to take care of — because the :after triangles on the bordered chevrons are a bit shorter (subtracting the border width from the height), then the width needs to be reduced accordingly. Otherwise, you don’t get a 45 degree angle. We can find the correct width with a proportion:

(fancy picture not drawn by hand)

Set the colors properly — and we have it! Hurrah!

I’m about to collapse

Now for that collapsing behavior. Flexbox is going to do the work for us.

First, we don’t want to shrink to be smaller than the chevron, so set a min-width based on the width of the chevron and the border:

min-width: @kie-wizard-chevron-width + (@kie-wizard-border-width * sqrt(2)); // only let steps shrink down to the width of the chevron plus the border

And for the incomplete (bordered) states, it’s slightly different:

// adjust the min-width to include the new offset
min-width: @wizard-chevron-width + ((sqrt(2) — 1) * @wizard-border-width);

Are you still with me? We want the first, last, and active states to be able to shrink if necessary, so they need to allow flex-shrink, but we want the other stages to shrink down completely first. Since flex-shrink works proportionally, we’ll give the pieces we want to stay large a flex-shrink of 1, but give the other pieces a flex-shrink of a really big number (and a flex-basis of 0 to make them as small as possible) so that they’ll shrink before the others.

li {
flex: 1 10000 0;
// don’t let the first, last, and current steps shrink down until the others have
&:active {
flex: 1 1 8em;

I think that’s enough for now. If you want to look at how the text wraps and then ellipses, start with Chris Coyier’s article on Line Clampin’.

So when your kids someday say, “Why do I have to learn geometry? I’m never going to need it,” then you can teach them how to turn border attributes into perfectly-proportioned, stretchy, squeezy, pointy chevrons.

A HUGE thank you to Andres Galante and Jenn Giardino not only for reviewing this article, but also for convincing me that there might actually be someone else geeky enough to want to read my writing about math and CSS.

Show your support

Clapping shows how much you appreciated Sarah Rambacher’s story.