Ugly Sweaters with CSS

Jake McCambley
15 min readMar 29, 2021

When I sit down to code in the evening, my usual focus space is in a window nook in my family room surrounded on three sides by windows. In the chill of spring in New Hampshire, I’ll typically find myself in a cozy sweater, with a cup of tea, looking through my reflection out over a yard of slowly melting slush.

I began learning CSS Grid on a particularly chilly evening, taking frequent breaks to stare into space at the reflection of my sweater in the window, and I had the idea to create my own iteration of the segmented and semi-chaotic pattern on my chest. I’m not so great with a needle and thread, but I figured I could put my coding skills to the test.

I got started creating and styling grids and eventually, here’s what I came up with:

Want to see the code in action? Check out the codepen here.

At first glance, this might not look much like a grid layout, but it is! In a few words, what you’re looking at is three grids, each containing uniquely clipped cells, stacked within a parent element, which is also shaped using a clip-path. I’ve also added a few animations and hover classes to add movement and breathe some life into the project.

In this post, I want to elaborate on those few words, and explain the steps behind building an ugly sweater using only CSS.

Prerequisites: Basic understanding of HTML and CSS.

Before jumping into the step-by-step process, let’s take a moment to discuss the two main CSS features utilized throughout this process.

CSS Grid

CSS Grid is positioning tool that allows us to take a high degree of control over the two-dimensional flow of items on a page. Other similar positioning tools you may have come across are CSS Float and CSS Flexbox. Float is the oldest and consequently most widely compatible positioning tool available. However, due to its many limitations in comparison to its successors, Float has fallen out of use in most modern web pages. Flexbox has many of the same advantages as Grid, but is primarily used for one-dimensional positioning of items on a page. This is an oversimplification of the similarities and differences between positioning tools, but for the purposes of this article, this is all we need to know. A quick Google search will unearth thousands of hours of videos that take an in depth look at the differences between the three if you’re curious.

To create a Grid Layout, all we need to do is assign the grid value to the display property of parent element.

.parent-element {
display: grid;
}

Upon assignment, all the children of the Grid element will now behave as grid cells.

Clip-Path

The clip-path property creates a region that determines what portion of an element should be shown and what should remain hidden. Rather than a simple rectangle, we can use the clip-path property create unique shapes through which to see content inside an element. It’s important to know that we don’t “crop” the element, we only indicate what portion of the element should be displayed. This means that the clip-path does not affect the flow of content inside or outside of the element to which it’s assigned. Here are a few examples:

.parent-element {
clip-path: circle(100px at 10px 50px);
/* creates a circle with a radius of 100px positioned 10px from the left and 50px from the top */
clip-path: ellipse(15px 10px);
/* creates an ellipse with a width of 15px and a height of 10px positioned at the center */
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
/* creates a diamond shape where each set of values corresponds to a vertex positioned along the X and Y axis according to the percentages, starting from the top-left of the element */
}

There are many more values you could assign to the clip-path property. For the purposes of this article, however, we’ll be focused solely on the polygon value.

Now that we understand the basic tools used in this project we can go ahead and discuss the steps taken to build an ugly sweater using CSS.

Step 1: The Setup

In this step we’ll lay out the basic structure of what will eventually become the sweater.

In our HTML file, create the basic structure by nesting a div container within the body tag. Within that div container, next create an img element and another div container. It should look like this

<html>
<head></head>
<body>
<div>
<img src="" alt="">
<div>
</div>
</div>
</body>

</html>

Assign classes to the body, img, and both div containers. We’ll use BEM naming conventions, but the goal is to use names that will help map out the layout of the project and the role of each element.

<body class="page">
<div class="sweater">
<img src="" alt="" class="sweater__outline>
<div class="sweater__section-container>
</div>
</div>
</body>

Find a nice image to serve as the backdrop for your sweater. This is an important step, as we’re going to be staring at this image for the remainder of the project. Assign the following styles to the page class.

Winter wonderland
Photo by Adam Chang on Unsplash
.page {  
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
background-image: url();
background-size: cover;
overflow: hidden;
}

Congratulations! With this addition you have gained one the most coveted skills in the world of CSS. It’s not immediately apparent quite yet, but with display: flex; justify-content: center; and align-items: center; we now have all of our content centered horizontally and vertically on the page. The additional properties allow the page to take up the entire viewport while preventing vertical and horizontal scrolling should our sweater grow larger than our frame.

The sweater container will be sized according to viewport width allowing the sweater to grow and shrink as we expand and collapse the browser window. We will assign its position property relative positioning in order to use absolute positioning on the sweater__section-container later. Finally, we need to center its children vertically horizontally as well. All told, the sweater class will look like this:

.sweater {
width: 35vw;
height: 35vw;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}

Note that I used 35vw for my width and height. This may change for you depending on what your outline looks like and what you decide you want your project to look like. Mess around with that number and see what works.

Step 2: The Sweater

The next step is to find the shape and outline of our sweater. The shape is entirely up to you. To determine mine, I googled “sweater outline” and opted for something with a minimal design and a few curves. We want to make sure there is no white background on our outline. If you can’t find an outline with no background, use this tool to remove it yourself.

If you want to change the color of your outline, there are handy photo editors out there online where you can mess with the hue and contrast, or you can use this tool to change colors fairly quickly.

Set the image path as the src attribute on your img element and give it a descriptive alt text.

We want this outline to sit above its sibling div container so we’ll assign it absolute positioning (reminder: make sure that the parent of img possesses relative positioning or else this sweater outline will be positioned according to the nearest positioned ancestor, in this case, the viewport) and a z-index of 1. The width and height of your outline will be another case of numbers that you’ll have to mess around with in order to fit your project. Here’s what the sweater class should look like:

.sweater {
position: absolute;
height: 177%;
width: 177%;
z-index: 1;
}

Lastly, we need to assign a clip-path to the sweater__section-container so that its contents don’t overflow the outline. If you assign background-color: red; to the sweater__section-container class you’ll see that the color expands beyond the outline we’ve created. Not good.

Sweater outline with an overflowing red background
Currently our outline isn’t doing a great job of containing anything…

In order to solve this issue we’ll need a clip-path value that closely follows the outline of our sweater. In the introduction we saw that we could create a circle , an ellipse , and a polygon clip-path. Unfortunately, there is no sweater clip-path. We’ll need to make one using the polygon value.

Fortunately, we live in a time of the internet where there are tools for just about any task you can imagine. Clippy is a wonderful tool that can accomplish just what we’re looking for, a custom clip path.

Navigate back to the page where you found your outline and copy the url of the outline used. Paste that url in the input field labelled “Custom url…” and toggle “Show outside clip-path” to on. Hover over the above pane and select “Custom Polygon” from the grid of shapes. Now you can click along your outline to create custom vertices aligned with your sweater. This may take a while, as it will require precision, and you’re bound to make mistakes. After some tinkering though, you’ll have your custom clip-path displayed at the bottom of the window and you’ll be set to clip sweater__section-container. My path looked like this:

clip-path: polygon(50% 20%, 57% 20%, 60% 20%, 64% 22%, 74% 27%, 76%     29%, 78% 32%, 79% 36%, 86% 67%, 86% 69%, 85% 70%, 86% 74%, 78% 75%, 77% 70%, 77% 70%, 75% 67%, 70% 44%, 69% 38%, 69% 40%, 69% 70%, 68% 73%, 67% 74%, 66% 75%, 65% 78%, 65% 80%, 35% 80%, 34% 75%, 33% 75%, 33% 74%, 33% 74%, 32% 73%, 31% 71%, 31% 39%, 31% 39%, 30% 40%, 30% 43%, 23% 70%, 22% 71%, 22% 73%, 22% 75%, 22% 76%, 19% 76%, 17% 76%, 15% 75%, 13% 75%, 13% 72%, 15% 71%, 14% 70%, 13% 68%, 14% 65%, 23% 31%, 24% 29%, 25% 28%, 26% 26%, 29% 25%, 31% 24%, 33% 23%, 35% 22%, 37% 20%, 40% 19%, 41% 20%, 43% 19%, 46% 19%, 49% 20%, 52% 19%);

Before bringing this clip-path back to your project, make sure the sweater__section-container contains the following properties:

.sweater__section-container {
position: relative;
min-width: 230%;
height: 230%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
opacity: .7;
}

With background-color: red; still applied, now assign the clip-path to your sweater__section-container class and watch the magic unfold. Don’t worry if your outline and your background don’t perfectly line up. You can tinker around with the width and height of the section container and its ancestors to fix the general placement of things. You could also adjust the positioning of your sweater__outline by assigning the top and left value to the class styles and adjusting those values to fit. If the general alignment is set but things are still overflowing the boundaries of the outline, open the inspect tool and adjust the percentage of the vertices in order to fine tune the shape. Once it looks good in the browser, copy and paste that path back to your project.

Congratulations! That was the hard part. Now it’s time to start messing with some grids.

Step 3: The Sections

Now that we’ve created a container for our sweater design, it’s time to create the grid sections that will compose the design of our sweater.

Within the sweater__section-container element, create three more div containers on the same level (not nested within one another). Give each of these a sensible class name. They’ll each correspond to the top, middle, and bottom designs of your sweater, so name them something like sweater__section-top, sweater__section-mid, and sweater__section-bot respectively.

To each of these classes, assign the following properties.

.sweater__section-... {
display: grid;
overflow: hidden;
}

With these properties, each section will now behave as a grid container, and its children will behave as grid cells. We’ve set overflow to hidden due to the fact that we’ll want to set the height of each section independent from the content within it. We’ll add enough cells to each section such that when the sweater expands to its full size, there are cells enough to populate all the visible cells, but we don’t want those extra cells to affect the height of each section. We want them hidden from view until they’re needed.

On your typical sweater, the three sections of each sweater aren’t distributed evenly. The top section might be 15% of the full length, while the middle and bottom sections might be 30% and 55% respectively. We’ll set our sweater sections accordingly. Again, these will need to be adjusted to fit your specific project, but mine looked like this:

.sweater__section-top {
max-height: 30%;
}
.sweater__section-mid {
max-height: 15%;
}
.sweater__section-bot {
max-height: 55%;
}

Before we continue setting up our grids, we need to feed them some volume. Within each section, create around 100 div containers. Really… 100. It’s an arbitrary number for now and you can always add or delete them as needed. This will at least give us something to populate the sweater sections. Assign them each a class of sweater__cell. We’ll come back to these cells later. Next however, we’ll configure our grid.

The next step will be to determine three measurements for each grid: The height grid cells, the width of the grid cells, and the distance between each cell.

To set the grid cell height, assign the grid-auto-rows property to each sweater__section and give a pixel value that suits your taste.

To set the cell widths, we’ll use the grid-template-columns property. Now this property is vastly flexible and I won’t go too in depth with all of its specific capabilities. If you’re interested, I encourage you to look into them on your own. For the sake of this project, we’ll be using the following value for the property: repeat(auto-fill, minmax(n, 1fr)). What this means, in short, is that the grid will automatically create columns and populate them with new cells according to the width available in which to place said columns. Each column will take up one fraction (1fr) of the available space. New columns will appears as long as there is a minimum of n pixels of width for them to populate. For example if n=200px and the grid is 650px wide, there will be three columns of equal width. If the grid is expanded to 150px to 800px, the grid will automatically create a new column with a width of 200px and there will now be four columns of 200px each.

To set the gap between the cells, assign the gap property to each sweater__section and give it yet another pixel value that suits your taste. The gap property sets both the gaps between the rows and the gaps between the columns. If you’d like to set different values for each gap, use the row-gap and column-gap properties instead.

The specifics here are up to you. For my project, I wanted a top section that had many small, short, and wide cells; a middle section that had many medium, tall, and thin cells; and a bottom section that had a few large, tall, and wide cells. Each of my sweater__sections looked like this:

.sweater__section-top {
display: grid;
overflow: hidden;
grid-auto-rows: 20px;
grid-template-columns: repeat(auto-fit, minmax(20px, 1fr));
grid-gap: 0px;
max-height: 30%;
}
.sweater__section-mid {
display: grid;
overflow: hidden;
grid-auto-rows: 40px;
grid-template-columns: repeat(auto-fit, minmax(30px, 1fr));
grid-gap: 3px;
max-height: 15%;
}
.sweater__section-bot {
display: grid;
overflow: hidden;
grid-auto-rows: 200px;
grid-template-columns: repeat(auto-fit, minmax(50px, 1fr));
grid-gap: 10px;
max-height: 55%;
}

Finally, add some color to the cells by assigning a background-color and border to the section that call for them. Here are my sections again:

.sweater__section-top {
display: grid;
overflow: hidden;
grid-auto-rows: 20px;
grid-template-columns: repeat(auto-fit, minmax(20px, 1fr));
grid-gap: 0px;
max-height: 30%;
background-color: #4D0000;
}
.sweater__section-mid {
display: grid;
overflow: hidden;
grid-auto-rows: 40px;
grid-template-columns: repeat(auto-fit, minmax(30px, 1fr));
grid-gap: 3px;
max-height: 15%;
background-color: #4D0000;
border: 3px solid #4D0000;
}
.sweater__section-bot {
display: grid;
overflow: hidden;
grid-auto-rows: 200px;
grid-template-columns: repeat(auto-fit, minmax(50px, 1fr));
grid-gap: 10px;
max-height: 55%;
background-color: #4D0000;
}
Where are all those cells we just configured?

Now we’ve got all of our cells but our sweater is still looking rather… plain. That’s because we haven’t added any styles to the cells we added a few steps ago. The cells are there, but they don’t have any coloring. All we see is the background color. In the next steps we’ll add color to those cells in order to make them pop.

Step 4: The Cells

Now in each section we have around 100 cells, but each of those cells have the same class assigned to them. It might seem that we’re either going to need to apply color to each cell individually with new classes, one by one, cell by cell, over and over or we’re going to have to settle for assigning a single background-color to that one class and calling a day.

To get this far in the project only to be able to use one color for our grid cells without jumping through hoops would be a big let down. Fortunately, we’ve got another useful tool in our arsenal: the :nth-child() pseudo class. Using this pseudo class, we can select specific elements within their position amongst siblings. For instance, .sweater__cell:nth-child(2) selects the second sweater__cell no matter how many cells share the same parent. We can also select more than just individual children. By using .sweater__cell:nth-child(2n) we can select every second element in a group of siblings. This means that in addition to the second sibling, we also select the fourth, sixth, eighth, tenth etc. Finally, we can offset the beginning of the patter by adding an integer to n. While .sweater__cell:nth-child(2n) selects the fourth, sixth, eighth, tenth etc, .sweater__cell:nth-child(2n+1) selects the first, third, fifth, seventh, ninth etc.

Using this functional notation you can quickly apply background colors to all of your cells in whatever pattern you’d like. I wanted to use five colors, so my pseudo classes looked like this:

.sweater__cell:nth-child(5n) {
background-color: #FFCDB2;
}
.sweater__cell:nth-child(5n+1) {
background-color: #FFB4A2;
}
.sweater__cell:nth-child(5n+2) {
background-color: #E5989B;
}
.sweater__cell:nth-child(5n+3) {
background-color: #B5838D;
}
.sweater__cell:nth-child(5n+4) {
background-color: #6D6875;
}

Now we’ve got color added to each of our cells, but they’re still missing the unique shapes that make an “ugly sweater” truly ugly. This next step will combine two tools that we’ve just learned about above: clip-path and :nth-child(). To add another level of specificity in styling, we’ll also introduce descendant combinators, selectors used to selector only the descendants of a specific parent. A descendant combinator is a pair of selectors separated by a space, and it looks like this: .ancestor .descendant {styles} where styles are only applied to the descendant elements if said elements are descendants of the ancestor element. For this projects purpose, we want to combine these three tools such that we can assign clip-paths to a specific pattern of descendants of only a specific ancestor.

What you do here is a great opportunity to add personal style to the project and is thus entirely up to you. I’ll provide an example to get started, but the possibilities are endless.

If I wanted to apply a star shape clip to every odd cell of the middle section of my sweater, a rhombus shape clip to every even cell in the middle section, and a cross shape clip to every cell in the bottom section, my stylesheet would look like this:

.sweater__section-mid .knit:nth-child(odd) {
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}
.sweater__section-mid .knit:nth-child(even) {
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
}
.sweater__section-bot .knit {
clip-path: polygon(10% 25%, 35% 25%, 35% 0%, 65% 0%, 65% 25%, 90% 25%, 90% 50%, 65% 50%, 65% 100%, 35% 100%, 35% 50%, 10% 50%);
}
For example…

Step 5: The Spice

After some tinkering

A clip-path here a clip-path there and all of the sudden we’ve got an ugly sweater! We’re done! You could stop here, but I suggest you take some time here to tinker around with custom clip-paths and varying color-schemes on each section until you have an ugly sweater that resembles something you own in real life, or something that simply speaks to you. Heck if you have the energy, you could even take a deep dive into animations and try something like expanding the sweater when the mouse hovers over it.

Make adjustments until making them becomes muscle memory, add style to spice things up according to your liking. This is a good chance to practice with play.

In Summary

In this project we took a brief look at a few important topics:

  • How and when to use grid layout as opposed to other CSS positioning methods.
  • How to use grid-template-columns to automatically determine the amount and size of columns in a grid.
  • How to use clip-paths to determine what content within an element should and should not be displayed.
  • How using the :nth-child() pseudo-selector allows us to select specific siblings amongst a group of many siblings.
  • How to use descendant combinators to select only descendants of a specific ancestor.

In the end we’ve not only made out very own ugly sweater to show off, but we’re also left with a deeper understanding of the capabilities and flexibility inherent in CSS. By practicing these tools in a low-risk learning environment, we’re more than ready to apply them to more practical applications like full scale page layouts.

Want to see the code in action? Check out the codepen here.

--

--

Jake McCambley

Learning to code by teaching others — Living at the cross section of tech, music, and the outdoors — Currently studying Web Development at Practicum by Yandex.