How to Create CSS Ribbon Shapes with a Single Element
In this article, I’ll show you how to use modern CSS tricks to create fancy CSS ribbon shapes with minimal code. As an extra bonus, our ribbons will have hover animations!
CSS ribbons are everywhere, and you can find a ton of articles about them, but the ones we’ll create here are a bit special. We’re going to rely on a single element to create each of the shapes, and CSS variables to easily control them. We aren’t going to rely on fixed dimensions or magic numbers. The shapes will fit their content so you don’t have to worry about the text inside.
I’ve made a collection of CSS ribbon shapes with a lot of cool examples, and in this article, we’re going to study two types of them, pictured below.
I’ll be calling the left one the “folded ribbon” and the right one the “rotated ribbon”.
Creating a CSS Folded Ribbon Shape
The first step in creating our folded CSS ribbon is to define the variables of our shape.
.ribbon {
--r: 20px; /* control the cutout of the ribbon */
--s: 20px; /* size of the folded part */
--c: #d81a14;
}
Two variables will control the shape, and one variable will control the color.
Now let’s move to the code. We’re mainly going to rely on clip-path
. The image below illustrates the polygon shape we’re going to use.
We add some padding to avoid cutting the text, then we apply the clip-path
:
.ribbon {
--r: 20px; /* control the cutout of the ribbon */
--s: 20px; /* size of the folded part */
--c: #d81a14;
line-height: 1.6; /* control the height */
padding-inline: 1.2lh calc(var(--r) + .2lh);
background: var(--c);
clip-path: polygon(1lh 0,100% 0,calc(100% - var(--r)) 50%,100% 100%,100% 100%, 0 100%,0 100%);
}
Using the CSS lh unit
You may be wondering what’s going on with the lh
unit. It’s a new unit that corresponds to the line-height
value. Since we’re using one line of text, the line-height
setting is what controls the height, so 1lh
is equivalent to the height of the element, which is super useful. (You can read more about the lh
unit in An Overview of CSS Sizing Units.)
In clip-path
, I need to cut the shape of an isosceles triangle, and to do this I need to know the height of the element. 1lh
is equal to that height.
Now, to create the folded part, we’re still going to use clip-path
and update the previous polygon. The cool thing about clip-path
is that it can cut “outside” the boundaries of the element. It may sound surprising or maybe useless, given that we have nothing outside, but it means we can include things like box-shadow, outline, pseudo-elements, and so on.
In our case, we’ll rely on box-shadow
. The image below illustrates the trick.
Note how I’m updating the clip-path
to include four new points, three of which are outside the element. Since the part we’re cutting is outside, it’s not visible, but if we add a big box-shadow
we make if visible. I’ve used a blue color to illustrate the idea above, but in the code we’ll use the same color as the background:
.ribbon {
--r: 20px; /* control the cutout of the ribbon */
--s: 20px; /* size of the folded part */
--c: #d81a14;
line-height: 1.6; /* control the height */
padding-inline: 1.2lh calc(var(--r) + .2lh);
background: var(--c);
clip-path: polygon(1lh 0,100% 0,calc(100% - var(--r)) 50%,100% 100%,1lh 100%,1lh calc(100% + var(--s)),.5lh calc(100% + var(--s) + var(--r)),0 calc(100% + var(--s)),0 100%);
box-shadow: 0 0 0 999px var(--c); /* a big spread radius */
}
Finally, we add a touch of shadow effect by introducing a gradient and another box-shadow and we’re done. Our CSS ribbon shape is perfect!
See the Pen
CSS-only Ribbon with a nice hover effect by Temani Afif (@t_afif)
on CodePen.
You’re probably wondering how to create the second ribbon (the green one). We do the same thing but with a different polygon. We take the first polygon and we invert it.
A polygon can be written like so:
clip-path: polygon(X1 Y1, X2 Y2, ..., Xn Yn)
To get the opposite shape, you change all Xi
by 100% - Xi
. As simple as that! Before checking my code, try to do it alone using the polygon of the first ribbon.
In the demo above, hover the shapes to notice a nice animation. To achieve it, we need to update the polygon on hover by offsetting some points. I won’t re-write the whole polygon on hover, but I will define a CSS variable that will control the offset.
If you focus on the animation, you’ll notice that we have three points moving to the left and three points moving down and to the left as well.
We update the Xi
of the points moving to left with Xi + d
and we update the Yi
of the points moving doing with Yi + d
. Then we simply update the variable d
to control the movement:
.ribbon {
--d: 0px; /* This will control the offset */
clip-path: polygon(calc(1lh + var(--d)) 0,100% 0,calc(100% - var(--r)) 50%,100% 100%,calc(1lh + var(--d)) 100%,calc(1lh + var(--d)) calc(100% + var(--s) + var(--d)),calc(.5lh + var(--d)) calc(100% + var(--s) + var(--r) + var(--d)),var(--d) calc(100% + var(--s) + var(--d)),var(--d) 100%);
}
.ribbon:hover {
--d: .2lh;
}
If you see such a polygon for the first time, you may get confused, as it looks a bit scary. But in reality, it’s not that complex. We started with a simple polygon and we slowly added more points and more calculations until we reached this complex one.
Creating a Rotated CSS Ribbon Shape
Let’s tackle the second shape. For this one, we’ll use the new trigonometric functions along with CSS variables and calc()
like the previous one. To understand the logic behind this shape, let’s rotate it and keep the text in a straight line.
I’m adding a bit of transparency to see the parts behind the main element. I’ll be using pseudo-elements to create them. I’ve also added the blue outline to illustrate the area of the element. This shape will be controlled with two variables:
.ribbon {
--r: 30px; /* control the cutout of the ribbon */
--a: 15deg; /* control the rotation */
}
The r
is doing the same job as with the previous shape. The a
will control the rotation of the main element and a lot of other things.
Let’s start with the main element. We can see from the figure that we need to cut it from each side, so you may logically think about using clip-path
, but not this time. We’ll rely on a gradient coloration, where the part we need to cut will have a transparent color:
.ribbon {
--r: 30px; /* control the cutout of the ribbon */
--a: 15deg; /* control the rotation */
background:
linear-gradient(calc(90deg + var(--a)),
#0000 calc(1lh*sin(var(--a))),
var(--c) 0 calc(100% - 1lh*sin(var(--a))),
#0000 0
);
}
Here comes the geometry.
The a
is the angle variable we defined. Considering this, the gradient needs to have an angle equal to 90deg + a
, and the transparent color should start at 0
and stop at d
. Doing some math, d
is equal to 1lh*sin(a)
. If we apply the same logic on the other side, we get the following code:
background:
linear-gradient(calc(90deg + var(--a)),
#0000 0% calc(1lh*sin(var(--a))),
var(--c) calc(1lh*sin(var(--a))) calc(100% - 1lh*sin(var(--a))),
#0000 calc(100% - 1lh*sin(var(--a))) 100%
);
We do some optimization by removing the 0%
and 100%
(they’re implicit), and when we have two consecutive color stops that are equal, we can replace the second one with 0
:
background:
linear-gradient(calc(90deg + var(--a)),
#0000 calc(1lh*sin(var(--a))),
var(--c) 0 calc(100% - 1lh*sin(var(--a))),
#0000 0
);
We’re done with the main element, so let’s move to the pseudo-elements. Here as well, we need some geometry tricks to identify the size.
We can easily find the height H from the previous figure we used to identify the gradient configuration. It’s equal to 1lh/cos(a)
. For the width W, it’s equal to (100% - x)*cos(a)
, where 100%
is the width of the main element and x
is that small part where we have the transparency. It’s equal to 1lh*tan(a)
.
Both pseudo-elements have the same size, so our code is as follows:
.ribbon:before,
.ribbon:after {
content: "";
position: absolute;
height: calc(1lh/cos(var(--a)));
width: calc(100%*cos(var(--a)) - 1lh*sin(var(--a)));
}
If you’re not comfortable with the math and you’re a bit lost with the formula, it’s fine. You don’t need to accurately understand them. The goal is to be able to adjust the shape using CSS variables. The formulas are here to make things easier and avoid us dealing with hard-coded values and magic numbers.
After the dimension, we should correctly place each pseudo-element, rotate it, and use clip-path
for the cutout part:
.ribbon:before,
.ribbon:after {
content: "";
position: absolute;
transform: translate3d(0,0,-1px);
rotate: var(--a);
height: calc(1lh/cos(var(--a)));
width: calc(100%*cos(var(--a)) - 1lh*sin(var(--a)));
background: color-mix(in srgb,var(--c),#000 40%);
}
h1:before {
right: 0;
top: 0;
transform-origin: top right;
clip-path: polygon(0 0,100% 0,100% 100%,0 100%,var(--r) 50%);
}
h1:after {
left: 0;
bottom: 0;
transform-origin: bottom left;
clip-path: polygon(0 0,100% 0,calc(100% - var(--r)) 50%,100% 100%,0 100%);
}
The code should be self-explanatory and the clip-path
value should be easy to understand. We used more complex polygons with the first shape.
Note the use of color-mix()
, which allows me to create a dark version of the main color. I’m also using a 3D translate with a negative value on the z-axis to bring the pseudo-elements behind the main element. You might think that this is the job of z-index
, but it won’t work due to some stacking context issues that I’ve detailed in this Stack Overflow thread.
Now, if we rotate the element in the opposite direction, we get our CSS ribbon shape.
See the Pen
Rotated Ribbon with a cool hover effect by Temani Afif (@t_afif)
on CodePen.
As with the previous example, I’ll let you dissect the code of the green ribbon and see what changes I made to get the opposite shape.
Conclusion
It was a fun exercise, don’t you think? We explored some modern CSS features like CSS variables, calc()
, and trigonometric functions, and we combined them to create fancy ribbon shapes.
If you want more, go check out my full collection of ribbon shapes. Try to build some of them alone before checking the code. It will be a good exercise to practice what you’ve learned here.