How to create amazing effects with CSS Shapes

A Clipping Demo title

Frontend developers tend to think in rectangles; rectangles inside rectangles inside rectangles inside rectangles. We might employ tricks with borders to make circles or triangles, but really they’re still just rectangular boxes in disguise. Well this is about to change with CSS Shapes, a W3C Candidate Recommendation that will change the way you think.

CSS Shapes allow you to create geometric shapes using shape functions: circle(), ellipsis(), inset() and polygon(), and apply them to elements or effects such as clipping and filters. What’s more, the shapes can affect the flow of the content, allowing you to wrap text neatly around features like circular avatars.

Perhaps the most powerful shape function is polygon() as it lets you create complex arbitrary shapes using unlimited points defined using co-ordinate pairs. If you’ve used SVG, this will be familiar to you.

In this tutorial, we’ll be using polygons with the clip-path property to cut a geometric shape out of our text so that only the area enclosed within the polygon is visible. We’ll also add some simple transitions to bring the effect to life. You can grab the code for each step in this repo from Github, here.

01. Begin with HTML

First, let’s get our HTML ready. We just need a <div>, our .clip class and our text, but as we’ll be using pseudo-elements for this effect, we’ll also add an attribute with the same value as the text so we can read it into the CSS rather than hard-coding it.

<div class=”clip” data-content=”A clipping demo”>
A clipping demo
</div>

Next, we want to make it fill the screen and align the text dead centre – we can use some Flexbox magic for this. Let’s also style and size the text.

.clip {
// fill screen
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
// alignment
display: flex; // use flex for alignment
justify-content: center; // horizontal alignment
align-items: center; // vertical alignment
// text styling
font-family: ‘Work Sans’, Arial, Helvetica, sans-serif; // Work Sans is available from Google Fonts
font-weight: 800;
font-size: 8rem;
text-align: center;
text-transform: uppercase;
}

02. Add some style

Now we can style the ::before and ::after pseudo-elements, with each becoming a layer on top of the text. By default, ::after will have the highest z-index. The handy attr() selector will read the value of our data-content attribute.  

The pseudo-elements will share the position and Flexbox properties, so we can refactor our CSS slightly and leverage the power of SCSS to keep our stylesheet tidy. The font styling will be inherited. We’re using ... to denote the properties we’ve already covered.

.clip {
...
// position self (&) and pseudo-elements (before / after)
&,
&::before,
&::after {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex; // use flex for alignment
  justify-content: center; // horizontal alignment
  align-items: center; // vertical alignment
}
&::before,
&::after {
  content: attr(data-content); // use the attribute value as the content
}
}

The result in the browser should look identical, because the pseudo-elements are sitting directly on top of the text. You can modify their styles in DevTools to see how they are layered.

03. Set backgrounds and colours

Time to give each layer a different colour and background – let’s go for some bold, on-trend, neon colours that we’ve predefined as variables. We can also force the text to wrap onto multiple lines using a CSS padding trick rather than adding these in the HTML. 

This is helpful as otherwise we’d have to use two different types of line breaks: <br> inside the <div> and the more obscure \A in the attribute.

.clip {
...
&,
&::before,
&::after {
  padding: 0 50%;
  box-sizing: border-box;
}
&::before {
  color: $white;
  background: $pink;
}
&::after {
  color: $pink;
  background: $blue;
}
}

The padding trick works by giving the text zero horizontal width, forcing the browser to wrap each word to a new line.

04. Clip the text

Time for the interesting bit – we’re ready to start clipping our text. We’re going to create a diagonal clip, slicing the screen into two triangles from the bottom left to top right. 

The ::before pseudo-element will only be visible in the top-left triangle, and ::after pseudo-element will only be visible in the bottom-right triangle. Here’s the code to achieve that:

.clip {
...
&::before {
  clip-path: polygon(0 0, 100% 0, 0 100%, 0 100%);
}
&::after {
  clip-path: polygon(100% 0, 100% 100%, 0 100%, 100% 0);
}
}

This creates polygons with four points. Each point is described by a co-ordinate pair; simply an X (left to right) and Y (top to bottom) value. The value can be absolute (for example: px) or relative (for example: %). The points are referenced to the top left, so the points at 100% 100% are in the bottom right.

Imagine lines connecting each of the points in the order that they are listed to form the shapes. In the ::before polygon it starts in the top left (0 0), moves across the screen to the top right (100% 0), and then down to the bottom left (0 100%).

Hopefully you will be seeing your clipped text now. Resize the browser and you will see the clipping react accordingly.

If you are using Chrome, you will probably see some repaint issues while resizing, caused by Chrome’s Composite Layers. Unfortunately, because the demo is full-screen, the recommended will-change: transform property and transform: translateZ(0) hack don’t rectify this. However, if you switch .clip to position: fixed; it works.

clip {
&,
&::before,
&::after {
  position: fixed;
}
}

Bear in mind that Chrome is trying to be helpful, and this workaround will have an impact on performance. Be sure to profile performance when doing things like this in production.

05. Add transitions

Now that we have got our clipped text, let’s bring it to life with some transitions. The good news is that you can simply transition the clip-path property so that the browser does all of the hard work. Let’s enable transitions on the pseudo-elements, and then define four different states to transition between.

State 1: This is the initial state, so let’s create three others

Add each block at the bottom of your CSS as you go, so you can see what it looks like.

State 2: Move the triangles apart slightly to reveal the background

This will look a bit like the flag of the Republic of Congo. Removing 20 per cent from the tip of each polygon triangle will do the trick.

State 3: Morph the triangles into rectangles

This is similar to the French Tricolour, in reverse.

What about that fourth co-ordinate pair? Well, this is where it comes in useful. It turns out that transitioning clip-path only works if the shape function used is the same (so polygon > polygon) and the number of points used is the same – the browser will transition each individual point. That’s why we have that fourth hidden point – it allows us to seamlessly transition from a triangle to a rectangle, we just reveal that fourth point when we need it.

.clip {
&::before {
  clip-path: polygon(0 0, 40% 0, 40% 100%, 0 100%);
}
&::after {
  clip-path: polygon(100% 0, 100% 100%, 60% 100%, 60% 0);
}
}

State 4: Twist those polygons

Sticking to our flag theme, this would resemble ‘I require a tug’ in the world of maritime flag signalling.

Here we’re twisting the polygons so they cross in the centre and form two triangles each. This is when transitions can help you to understand the co-ordinate pairs. By really slowing down the transition, you can see how each point moves across the screen to its new position, and you start to understand how the ordering of the pairs affects the transition. It actually gives you quite a lot of control over the transition.

.clip {
&::before {
  clip-path: polygon(100% 0, 0 0, 100% 100%, 0 100%);
}
&::after {
  clip-path: polygon(100% 0, 100% 100%, 0 0, 0 100%);
}
}

Wrap up

We’re all set but we’re not yet able to change the states, meaning you can’t see the transitions in action. There are many ways to achieve this, so it’s up to you. In the repo and CodePen, I’ve used a 100 per cent JavaScript-free solution with hidden radio buttons and the ~ general sibling selector – do take a look. You can also check out this tutorial’s CodePen here.

This article originally appeared in issue 298 of net, the magazine for professional web designers and developers – offering the latest new web trends, technologies and techniques. Buy issue 298 here or subscribe to net here.

Special Christmas offer: Save up to 47% on a subscription to net for you or a friend for Christmas. It's a limited offer, so move quickly...

Related articles: