How to create CSS Layout using Flexbox

So, you have your first mockup or design in your hands, but how do you translate it to HTML and CSS? Should you rely on CSS frameworks like Bootstrap, TailwindCSS, or others? No matter your choice, it would benefit you to understand the mechanics behind the layout.

If you are in a hurry, here is a demo with the end result

Let's say we have a standard mockup with a Header, Footer and a few sections. Before diving into code, I usually look at the mockup and figure out how many different sections I have. Let’s do that.

Example of simple home page mockup with Navigation, Hero section, two and four column section and a footer

Here are our Sections:

  1. Main navigation
  2. Hero section
  3. Two column Section
  4. Four column Section
  5. Two-column Section, the same as (3) but reversed
  6. The Footer, which looks like it is attached to the bottom

We do not have Mobile view, but we can assume that Two Col section would stack image below and Four column would nicely collapse into one column.

I made visual adjustments to see each section.

Website Homepage mockup divided by sections

Let’s add some colours we can use for our design. I use this colour palette: coolors.co

1E1E24 Raisin Black
92140C Penn red
FFF8F0 Floral white
FFCF99 Sunset
111D4A Space cadet

Setting up HTML structure

Setting up the basic HTML structure and using it semantically correctly is essential to get going. Semantic HTML has many benefits. It improves SEO and makes it more accessible for users with disabilities. Also, it makes code maintenance better, as you can read and understand the purpose of the elements.

Let's start with the basic HTML structure.

1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
    <meta charset="UTF-8">
5
    <title>CSS Layout</title>
6
</head>
7
<body>
8

9
<nav>navigation</nav>
10
<section>Hero</section>
11
<section>Cols 8 - 4</section>
12
<section>Cols 4</section>
13
<section>Cols 4 - 8</section>
14
<footer>Footer</footer>
15

16
<h1>h1 title</h1>
17
<h2>h2 title</h2>
18
<h2>h3 title</h2>
19
<p>paragraph</p>
20
<a href="#">link</a>
21

22
</body>
23
</html>

HTML example without CSS added We use the following tags: nav, section and footer to highlight their semantic purpose.
As you see, even though we didn’t add any CSS, the code above produces HTML with added style. The Browser has its own CSS stylesheet, the "User-Agent-Stylesheet.” This is why Title tags have different sizes and links have a default colour.

You can see this example in the CodePen.

CSS Reset

To remove inconsistencies and differences in rendering between browsers, we can use CSS Reset.

In the CodePen examples, I used built-in CSS Reset. However, there are plenty you can find. For example, The new CSS reset.

What does it do? Well, each browser has its own stylesheet for title sizes, list styling, body margins and paddings and many other things. Basically, reset.css zeroes all those values between browsers and makes all elements look the same. So, you can start your styling with a blank canvas.

Take a look at the pen below. All elements on the page look the same, although they represent titles, links, sections and paragraphs. To demonstrate this, I added The New CSS reset, but in the following examples, I will use CodePen's built-in Reset to reduce cluttering.

Now we have a clean blank and are ready to add some styles.

Fonts

Typography itself is a massive topic. There are font sizes, weight, line height, how text interacts with titles, and so on. At a minimum, we need to decide on a font family and define font sizes.

For font family, I will use probably the most known font family from Bootstrap

font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";

I will use the Utopia website's handy tool to calculate the font sizes.

Let’s add basic fonts for titles, links and paragraph tags which will be used for all website.

1
:root {
2
    /* FONTS */
3
    /* @link https://utopia.fyi/type/calculator?c=320,18,1.2,1240,20,1.25,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12 */
4
    --step--2: clamp(0.7813rem, 0.7747rem + 0.0326vi, 0.8rem);
5
    --step--1: clamp(0.9375rem, 0.9158rem + 0.1087vi, 1rem);
6
    --step-0: clamp(1.125rem, 1.0815rem + 0.2174vi, 1.25rem);
7
    --step-1: clamp(1.35rem, 1.2761rem + 0.3696vi, 1.5625rem);
8
    --step-2: clamp(1.62rem, 1.5041rem + 0.5793vi, 1.9531rem);
9
    --step-3: clamp(1.944rem, 1.771rem + 0.8651vi, 2.4414rem);
10
    --step-4: clamp(2.3328rem, 2.0827rem + 1.2504vi, 3.0518rem);
11
    --step-5: clamp(2.7994rem, 2.4462rem + 1.7658vi, 3.8147rem);
12
    /* COLORS */
13
    --colorRaisinBlack: rgb(30, 30, 36);
14
    --colorPennRed: rgb(146, 20, 12);
15
    --colorFloralWhite: rgb(255, 248, 240);
16
    --colorFloralWhite50: rgba(255, 248, 240, 0.5);
17
    --colorSunset: rgb(255, 207, 153);
18
    --colorSpaceCadet: rgb(17, 29, 74);
19
    --colorSpaceCadetLight: rgb(61, 76, 135);
20
}
21

22
body {
23
    display: flex;
24
    flex-direction: column;
25
    background: var(--colorFloralWhite);
26
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
27
}
28

29
h1 {
30
    font-size: var(--step-5);
31
}
32

33
h2 {
34
    font-size: var(--step-4);
35
}
36

37
h3 {
38
    font-size: var(--step-3);
39
}
40

41
p {
42
    margin-top: 1rem;
43
    font-size: var(--step-0);;
44
    line-height: 1.6rem;
45
}
46

47
a {
48
    color: var(--colorSpaceCadetLight);
49
    transition: color 0.3s;
50
}

What is that :root part? The Root represents the document, or in HTML, it actually represents HTML. Somehow, it is a tradition to define CSS variables to the :root element, and that’s exactly what I did as well. I defined all font sizes; I won’t use all of them in the current example. However, colours will be used fully. On the body tag, I defined the background colour and font family, and the rest is self-explanatory.

Layout Techniques

There are different techniques for approaching layout. A long time ago, when the grass was greener, I used the Floating technique to build websites. That was quite challenging and involved clearing after elements to get them aligned. The differences between browsers were massive, and IE6 was a real pain.

Currently, I mostly use CSS Flexbox and CSS Grid for CSS layouts. For our Mockup, I will use Flexbox. Here is a great resource if you need to refresh on a Flexbox: CSS Tricks. I also use it quite often myself.

I usually start from the top to the bottom and style each section one by one. Based on the Mockup, each section should be full width; however, the content itself is restricted to a maximum width. Also, the background colour for some sections should cover the full width of the element.

That means we can’t wrap the whole page in one div tag and add max width. If we did, we wouldn’t be able to add a full-width background colour for a few sections.

Navigation

Let’s figure out what is needed for Navigation. We have a max-width for navigation; we want it in the middle of the screen, and all but the last item should be on the left side.

Nav example with max width and the last item on the right

1
<nav>
2
    <div>
3
        <a href="./">Logo</a>
4
        <a href="#/product">Product</a>
5
        <a href="#/market">Market</a>
6
        <a href="#/learn">Learn</a>
7
        <a href="#/getStarted">Get Started</a>
8
    </div>
9
</nav>

So, we have the <nav/> tag as a wrap and the first <div/> tag as the container, which will hold max-width. If we look at the mockup, we notice that the same pattern is for each section.

In that case, we can write something like that in CSS:
nav > div {
width: 100%;
max-width: 1200px;
margin: auto }

However, we need the same for the Hero, Footer, and every Section. For that, we can use is() selector to grab all sections and define the same structure for each of them.

1
:is(nav, section, footer) > div {
2
    width: 100%;
3
    max-width: 1200px;
4
    margin: auto;
5
}

Let’s define Navigation’s styles.

1
/** NAVIGATION */
2
nav {
3
    padding: 1rem;
4
}
5

6
nav > div {
7
    display: flex;
8
    gap: 1rem;
9
    align-items: center;
10
    font-size: var(--step-1)
11
}
12

13
nav a:last-child {
14
    margin-left: auto;
15
}

Adding 1rem to the Nav I set some breathing space between items and Nav.

nav > div defines flex with the 1rem gap and chooses the font size for the items inside the nav.
The align-items center define the vertical alignment for the items.

Since we want the last item to be pushed to the far right, we add margin-left: auto, which will do exactly what we want. This feels a bit like a magic trick and probably is. When we apply margin auto to the flex child, it will fill that space and push the item in the opposite direction.

This should setup our Navigation in place. The only thing I want to add is the “logo”. For that, I will use Iconify website and choose the lighthouse from here: Icons. I choose it as SVG and replace the first item

That’s it. Our navigation is sorted using a few lines of CSS.

To simplify CSS, I use the built-in Pen CSS reset. So, if you copy HTML and CSS from the pen, remember to add your favourite reset CSS to replicate the example.

Hero Section

The Hero section is really simple. I want the background gradient to be filled full size and the text to stay max-width 600px. Why 600px? Because it is the comfortable width for reading.

1
/** HERO */
2
.hero {
3
    display: flex;
4
    padding: 2rem;
5
    color: var(--colorFloralWhite);
6
    background: linear-gradient(130deg, var(--colorPennRed) 0%, var(--colorRaisinBlack) 50%, var(--colorSpaceCadet) 100%);
7
}
8

9
.hero p {
10
    max-width: 600px;
11
    font-size: var(--step-1);
12
    line-height: 1.9rem;
13
    font-weight: 200;
14
}

For HTML, there is not much. I added a section with the class name hero, followed by an inner wrapper div to fix the content and then the content itself. Instead of a background gradient, there could be a background image. You also have to be careful with font colour as you have to think about accessibility.

1
<section class="hero">
2
    <div>
3
        <h1>Hero title</h1>
4
        <p>Hero description. Lipsum... </p>
5
    </div>
6
</section>

Footer

In the mockup the footer seems to stick to the bottom of the page. Let’s add the footer now before we have too much content to test it.

Again, with the same principle as above, we will use footer tag with inner wrapper for content.

1
<footer>
2
    <div>
3
        <a href="#">Item 1</a>
4
        <a href="#">Item 2</a>
5
        <a href="#">Item 3</a>
6
        <span>&copy;2024</span>
7
    </div>
8
</footer>

We want the last item to be on the right side, away from the rest, so I will reuse
margin-left: auto technique.
Since the footer background is dark, I will use reverse colours for the links.

1
/** FOOTER */
2
footer {
3
    margin-top: auto;
4
    padding: 0 2rem;
5
    background: var(--colorSpaceCadet);
6
    color: var(--colorFloralWhite);
7
}
8

9
footer > div {
10
    display: flex;
11
    gap: 1rem;
12
    padding: 2rem 0;
13
}
14

15
footer a {
16
    color: var(--colorFloralWhite);
17
    font-size: var(--step-0);
18
}
19

20
footer span {
21
    margin-left: auto;
22
}

Remember, I defined body tag like this.

1
body {
2
    display: flex;
3
    flex-direction: column;
4
    /*...*/
5
}

Before we use the magic power of margin auto, we need to set the page height to 100%. This is important!

1
html, body {
2
    height: 100%;
3
}

Now, when I define margin-top: auto on the footer, it will use that Flexbox magic and create a space between other elements, and that’s how we get the footer to stick to the bottom.

Two column sections

So, by looking at the mockup, we can see that there are two columns, and the size is roughly 2:1. I take a 12-col grid approach, so it is like 8:4. We will use the same approach as with the rest of the site. We will use a Section and an inside div wrapper.

1
<section class="cols-8-4">
2
    <div>
3
        <article>
4
            <h2>This is cols-8-4</h2>
5
            <p>.... Loads of text here</p>
6
        </article>
7
        <aside>
8
            <img src="https://picsum.photos/id/16/400/400" alt="Seaside">
9
        </aside>
10
    </div>
11
</section>

I’m using an article tag for the main part and an aside tag for the sidebar where I will keep the image.

The rest is straightforward. Firstly, I will give 2rems for the section.

For the inner <div> I will use the flex and direction column for mobile, and from the screen size 600px, I want to switch to flex-direction: row.

I will use gap: 1rem. Why gap? That way, I can switch in reverse and don’t need to adjust margins between elements. The gap is awesome!

The article and the aside will both use flex, as I want to use flex size. So, the end result will look something like this.

1
/** COLS-8-4 */
2
.cols-8-4 {
3
    padding: 2rem;
4
}
5

6
.cols-8-4 > div {
7
    display: flex;
8
    flex-direction: column;
9
    gap: 1rem;
10
}
11

12
@media (min-width: 600px) {
13
    .cols-8-4 > div {
14
        flex-direction: row;
15
    }
16
}
17

18
.cols-8-4 article {
19
    display: flex;
20
    flex-direction: column;
21
    flex: 2
22
}
23

24
.cols-8-4 aside {
25
    display: flex;
26
    flex-direction: column;
27
    flex: 1
28
}
29

30
.cols-8-4 aside img {
31
    border-radius: 0.5rem;
32
    min-width: 230px;
33
}

It's worth noting that I use @media query to accommodate different screen sizes. First, I define content direction as a column.

1
.cols-8-4 > div {
2
    display: flex;
3
    flex-direction: column;
4
    gap: 1rem;
5
}

Once the screen size is above 600px, I will use the flex-direction row:

1
@media (min-width: 600px) {
2
    .cols-8-4 > div {
3
        flex-direction: row;
4
    }
5
}

Ok, great. Now, we have two columns implemented.

However, the second column looks very similar to the first one. The only difference is that the text and image are switched around. With Flex, it is super easy to reverse those two columns.

flex-direction: row-reverse and that’s it.
We want to use reverse only for screens above 600px.

1
@media (min-width: 600px) {
2
    .cols-8-4 > div {
3
        flex-direction: row;
4
    }
5

6
    .cols-8-4.reverse > div {
7
        flex-direction: row-reverse;
8
    }
9
}

Of course, for a reverse column, I use the additional class name reverse.

Four column section

To make things more fun, I want different content in those columns. Let's say I would add text in some and an image in others, which would fill the container fully. Also, I want those columns to start at mobile size and magically grow and expand with the screen size.

Let’s start with HTML and use the same pattern as before.

1
<section class="cols-4">
2
    <div>
3
        <div>
4
            <h3>Cool title here</h3>
5
            <p>... loads of text</p>
6
        </div>
7
        <div>
8
            <img src="https://picsum.photos/id/19/400/400" alt="Tree covered in the moss">
9
        </div>
10
        <div>
11
            <h3>Another title here</h3>
12
            <p>... loads of text</p>
13
        </div>
14
        <div>
15
            <img src="https://picsum.photos/id/18/400/400" alt="Close shot of the grass with the blurred meadow in a background">
16
        </div>
17
    </div>
18
</section>

Now, the fun part!

For those columns, I want to use a CSS grid. SmolCSS fully inspired me, and you should be, too! smolcss.dev

First, we wrap the usual bit in a section and give background colour and paddings.

The next part is the fun bit. I use the arbitrary value of 28ch, which takes the character zero-O width of the font. The gap between the columns is 1 rem. The fancy bit is the last line.

I define one unit as taking 1 fraction of space and define the min/max values for that space.

Although I define it as a four-column layout, in reality, I could add more or remove some columns. I simply calculated that 28ch, in this case, would take one row with four items. Nothing will break if I remove or add more items; however, at some screen sizes, it could leave some empty space.

1
.cols-4 {
2
    padding: 2rem;
3
    background: var(--colorSpaceCadet);
4
    color: var(--colorFloralWhite);
5
}
6

7
.cols-4 > div {
8
    --min: 28ch;
9
    --gap: 1rem;
10
    display: grid;
11
    grid-gap: var(--gap);
12
    grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--min)), 1fr));
13
}  

Ok, let’s define text size. Since I use dark blue, I want the font a bit smaller but with added letter spacing.

1
.cols-4 p {
2
    font-size: var(--step--1);
3
    line-height: 1.4rem;
4
    font-weight: 100;
5
    letter-spacing: 1px;
6
}

To style each column container, I can use a direct child selector, which is very handy as I don’t need to add any classes. I use overflow hidden as I don’t want the image to leave the container. You will see that in a bit.

1
.cols-4 > div > div {
2
    box-shadow: var(--colorFloralWhite50) 0 0 5px 0;
3
    border-radius: 0.5rem;
4
    overflow: hidden;
5
}

This is the result. It's not good. The text is very close to the edge of the container. If we add padding for the text, we will push the image away, too. We want the images to be full height and width. Also, there is this silly 4px gap for the last image. Let's fix all this nonsense!

screenshot of four columns layout showing empty spaces, missing paddings

Let’s add 1rem padding for the container, but only if there is no image.

1
.cols-4 > div > div:not(:has(img)) {
2
    padding: 1rem;
3
}

Let’s remove the 4px gap and make the image fill the container's space completely. Adding more text in the next column will push the image container, too, so to avoid the empty gap, we use object-fit: cover.

1
.cols-4 img {
2
    display: block;
3
    object-fit: cover;
4
    height: 100%;
5
    width: 100%
6
}

The last thing I want to add is underline for all but Navigation links.

1
nav a {
2
    text-decoration: none;
3
}
4

5
a:hover, a:focus {
6
    color: var(--colorPennRed);
7
    text-decoration: underline;
8
}

Final result

Back to top

CodePen list of examples

Here is the final result. It took less than 200 lines of CSS, including colours and font styling.

Do we really need a CSS framework? For big projects with multiple members, it makes sense to use the CSS framework as it sets the basic rules. For an in-house project with 2-3 developers, if they agree on an approach, it might be better to build it from scratch.

Conclusion

Do you think I missed something or got something wrong? Do you have a better solution? If you have any questions or comments, please feel free to contact me on Mastodon or LinkedIn

Happy Coding!

PS. This article is written by a human.