Let’s have a look at how we can create a row of links that sorta run into each other with a chevron-like shape and notch on each block like you might see in a hierarchical breadcrumb navigation.

You’ve probably seen this pattern a lot. It comes up often in things like multi-step forms and site breadcrumbs. For our purposes we’re going to call these “ribbons” so we know what we’re referring to as we go.

Like a lot of things on the web, we can make ribbons like these in many ways! I’ve created a demo page that brings a variety of them together, like using CSS triangles, SVG backgrounds, and the CSS clip-path property.

Starting with the HTML structure

For each demo, the HTML structure will largely be the same where we have a <nav> that acts as the parent element and then links inside it as the children.

Note that these elements should be accessible, according to A11y Style Guide website. It’s a good rule to build components with accessibility in mind and introducing accessibility at the very start is the best way to prevent the classic “I forgot to make it accessible” situation.

Let’s create some baseline styles

When it comes to things like this, we want to make sure the sizing of the elements is done right. For this purpose, we are going to define the font size of the .ribbon (that’s what we’re going to call these things) wrapper element and then use em units on the child element which are the links themselves.

This particular technique would be beneficial for defining the size of the triangle shape for each ribbon because we would use the same sizes to calculate triangle. And since we are using em units to calculate the ribbon element size, we could resize all elements by redefining the font-size on the wrapper element.

Let’s use CSS Grid for the layout because, well, we can. We could do this in a way that offers deeper browser support, but we’ll leave that up to you based on your support requirements.

We are going to define four columns:

Three for ribbon elements

One to fix spacing issues. As it is, the right arrow shape would be placed outside of the ribbon component and that could mess up the original layout.

If you prefer to avoid stretching the ribbon elements, the grid could be defined differently. For example, we could use max-content to adjust columns by content size. (Note, however, that max-contentis not very well supported yet in some key browsers.)

I am sure there are many different ways we could have gone about the layout. I like this one because it defines the exact gap between ribbon elements without complicated calculations.

Accessibility is not only adding aria attributes. It also includes color contrast and readability, as well as adding hover and focus states. If you don’t like outline style, you could use other CSS properties, like box-shadow, for example.

Option 1: The border approach

First, we should set the element’s width and height to zero so it doesn’t get in the way of the pseudo-elements we’re using to draw the triangle with borders. Then we should draw the triangle using borders, specifically by defining a solid left border that matches the color of the background to make it blend in with the rest of the ribbon. From there, let’s define top and bottom borders and make them transparent. The trick here is to calculate the size of the border.

Our ribbon element has a content size of the line-height value plus the top and bottom paddings:

1

1.333em + 0.667em + 0.667em = 2.667em

That means our top and bottom borders should be half that size. The only thing left to do is to position elements absolutely to the correct side of the component.

123456789101112131415161718192021222324252627282930313233343536373839

/* The left arrow */.ribbon--alpha.ribbon__element:before {/* Make the content size zero */content: '';height: 0;width: 0;/* Use borders to make the pseudo element fit the ribbon size */border-bottom: 1.333em solid transparent;border-left: 0.667em solid #fff;border-top: 1.333em solid transparent;/* Position the element absolutely on the left side of the ribbon element */position: absolute;top: 0;bottom: 0;left: 0;}/* The right arrow */.ribbon--alpha.ribbon__element:after {/* Make the content size zero */content: '';height: 0;width: 0;/* Use borders to make the pseudo-element fit the ribbon size */border-bottom: 1.333em solid transparent;border-left: 0.667em solid;border-top: 1.333em solid transparent;/* Position the element absolutely on the right side of the ribbon element and push it outside */position: absolute;top: 0;right: 0;bottom: 0;-webkit-transform: translateX(0.667em);transform: translateX(0.667em);}

Since the right triangle should match the background color of the ribbon, let’s remember to add the correct border color for each ribbon’s pseudo-element.

1234567891011121314

/* The right arrow of the first element */.ribbon--alpha.ribbon__element:nth-child(1):after {border-left-color: #11d295;}/* The right arrow of the second element */.ribbon--alpha.ribbon__element:nth-child(2):after {border-left-color: #ef3675;}/* The right arrow of the third element */.ribbon--alpha.ribbon__element:nth-child(3):after {border-left-color: #4cd4e9;}

And there we go!

Option 2: The background image approach

We can also create a triangle using a background image. This requires creating an image that matches the design, which is a little cumbersome, but still totally possible. We are going to use SVG here since it’s smooth at any resolution.

Unlike the border triangle approach, we want to match the height of our pseudo-element with the height of the ribbon element, or 100%. The width of the component should match the left border width of the border triangle, which is 0.666666em in our case. Then we should use a white triangle for the background image on the triangle of the left side, and then use triangle images with color for the triangles on the right side. Again, we are using absolute positioning to place our triangles to the correct side of the ribbon element.

/* The left arrow */.ribbon--beta.ribbon__element:before {/* Define the arrow size */content: '';height: 100%;width: 0.666666em;/* Define the background image that matches the background color */background-image: url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgMTAgNDAiIHdpZHRoPSIxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsPSIjZmZmIj48cGF0aCBkPSJtNSAxNSAyMCAxMGgtNDB6IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0ibWF0cml4KDAgLTEgLTEgMCAyNSAyNSkiLz48L3N2Zz4=);background-position: center left;background-repeat: no-repeat;background-size: 100%;/* Position the element absolutely on the left side of the ribbon element */position: absolute;bottom: 0;top: 0;left: 0;}/* The right arrow */.ribbon--beta.ribbon__element:after {/* Define the arrow size */content: '';height: 100%;width: 0.667em;/* Define the background image attributes */background-position: center left;background-repeat: no-repeat;background-size: 100%;/* Position the element absolutely on the right side of the ribbon element and push it outside */position: absolute;top: 0;right: 0;bottom: 0;-webkit-transform: translateX(0.667em);transform: translateX(0.667em);}/* Define the background image that matches the background color of the first element */.ribbon--beta.ribbon__element:nth-child(1):after {background-image: url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgMTAgNDAiIHdpZHRoPSIxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNSAxNSAyMCAxMGgtNDB6IiBmaWxsPSIjMTFkMjk1IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0ibWF0cml4KDAgLTEgLTEgMCAyNSAyNSkiLz48L3N2Zz4=);}/* Define the background image that matches the background color of the second element */.ribbon--beta.ribbon__element:nth-child(2):after {background-image: url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgMTAgNDAiIHdpZHRoPSIxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNSAxNSAyMCAxMGgtNDB6IiBmaWxsPSIjZWYzNjc1IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0ibWF0cml4KDAgLTEgLTEgMCAyNSAyNSkiLz48L3N2Zz4=);}/* Define the background image that matches the background color of the third element */.ribbon--beta.ribbon__element:nth-child(3):after {background-image: url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgMTAgNDAiIHdpZHRoPSIxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNSAxNSAyMCAxMGgtNDB6IiBmaWxsPSIjNGNkNGU5IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0ibWF0cml4KDAgLTEgLTEgMCAyNSAyNSkiLz48L3N2Zz4=);}

There we go!

Option 3: The inline SVG approach

Instead of loading a different SVG triangle for each background image, we could use inline SVG directly in the HTML.

This particular approach allows us to control the fill color of each SVG arrow with CSS. The arrow size is calculated by the ribbon size. Once again, we are using the em units to define the size and arrows are absolutely positioned, like the other approaches we’ve seen so far.

123456789101112131415161718192021222324252627282930313233

/* Position arrows absolutely and set the correct size */.ribbon--gamma.ribbon__elementsvg {height: 2.667em;position: absolute;top: 0;width: 0.667em;}/* The left arrow */.ribbon--gamma.ribbon__elementsvg:first-child {fill: #fff; /* Define the background image that matches the background color */left: 0; /* Stick left arrows to the left side of the ribbon element */}/* The right arrow */.ribbon--gamma.ribbon__elementsvg:last-child {left: 100%; /* Push right arrows outside of the ribbon element */}/* Define the fill color that matches the background color of the first element */.ribbon--gamma.ribbon__element:nth-child(1)svg:last-child {fill: #11d295;}/* Define the fill color that matches the background color of the second element */.ribbon--gamma.ribbon__element:nth-child(2)svg:last-child {fill: #ef3675;}/* Define the fill color that matches the background color of the third element */.ribbon--gamma.ribbon__element:nth-child(3)svg:last-child {fill: #4cd4e9;}

Option 4: The clip-path approach

We can create the ribbon triangles with a polygon that masks the background. Firefox’s Shape Editor is a fantastic tool to draw shapes directly in the browser with a GUI, as is Clippy.

Since polygons must be created using percentages, we should use our best judgment to match the size of border triangles. Also, note that percentage-based polygons might look a little funny on some viewports, especially when element sizes are adapting to its surroundings, like wrapper elements. Consider redefining polygons for different viewports.

Since we defined our wrapper element using CSS Grid, we should expand the ribbon elements but leave the last one at the size of the polygon triangle, which is 5% in our case. The last ribbon element should be wider by the size of the border triangle width to match the first two examples.

123456789

/* Make all ribbon elements (except the last one) wider by the size of the polygon triangle */.ribbon--delta.ribbon__element:not(:last-child) {width: 105%;}/* Make the last ribbon element wider by the size of the border triangle */.ribbon--delta.ribbon__element:last-child {width: calc(100% + .667em);}

Variations on these options

Now that we’ve learned how to create the breadcrumb ribbon a few different ways, we could play around with it, like adding shadows or gradients and different sizes.

Adding a shadow

We could add the shadow on our ribbon elements. Make sure to avoid the shadow on the left or right side of the ribbon element.

Using gradients for color

We could add gradients to our ribbon element. Be sure to match the color of the right triangle when doing so. Also, make sure to comply with contrast accessibility.

For example, if we are going to use the border approach or background image approach, we should use mostly horizontal (i.e. left-to-right) gradients (with the exceptions of some carefully calculated angled gradients). If we are using the clip-path approach, we could use any gradient version we wish.

Combining all the things!

We can also combine different modifier classes to achieve an even more styling. For example, let’s use gradient and shadow modifiers together:

Any other angles to consider?

Making custom elements using different CSS techniques is a great way how each one of us could improve or refresh our knowledge. Before starting, it’s worth investing some thought into the maintainability and modularity of the component being built. A consistent naming convention, like BEM, is certainly helpful that. Accessibility is also a big deal, so starting with it in mind and documenting accessibility features along the way will serve you well.

We looked at four different approaches for drawing ribbon triangles. Have you used a different approach or know of one we haven’t considered here? Let me know in the comments!