CSS layout

Learn how to create CSS layout primitives and compose them together to create complex designs.

  • css
  • layout
  • fundamentals

Modern CSS has powerful tools for controlling where elements go on the page. We’re going to learn how to create style “primitives” (single-purpose bits of CSS) to solve different layout requirements, then see how we can combine those primitives together to create more complex layouts.

Layout fundamentals

Let’s quickly review some of the ways we can control layout with CSS.

Flow layout

Flow layout is the default way elements behave. Block elements like div, header and p take up the full-width of the page. Inline elements like span, strong and a only take up as much horizontal space as they need, and can sit next to each other.

The viewport scrolls vertically by default, when there’s too much content to fit on the screen. If the content is too wide to fit the browser will wrap elements onto the next line.

You can go a surprisingly long way without writing much layout CSS, since the defaults are pretty good.

Flexible box layout

Flexible box layout (usually called “flexbox”) is an alternate layout context you can set using the display: flex rule.

This allows a parent element to control how its children are laid out. By default it puts elements all on a single line (as if they were inline elements). Unlike inline elements they won’t wrap when there’s not enough room. You have to enable wrapping with the flex-wrap: wrap rule.

Flexbox is usually used for single-direction layouts. I.e. a row or a column, but not both. It’s also better for flexible layouts where you don’t need exact control over where every element goes.

Grid layout

Grid layout is another layout context that lets a parent element specify rows and columns for its children to slot into. You set this using display: grid.

Grid can be used to create very specific layouts using grid-template-columns and grid-template-rows to specify an exact layout grid. You can then place child elements into specific locations on the grid with grid-column and grid-row.

Grid is usually used for two-direction layouts. I.e. rows and columns. It works best when you have a specific grid in mind, but can be less flexible.


The Center: constraining content width

It’s important for content not to get too wide. Otherwise text gets pretty hard to read as your eyes have to travel so far left-to-right.

So it’s a common requirement to put content in a narrow horizontally centered column. For example the content on this very website is in a center column.

The best way to constrain width is with the max-width property. This is better than just width, as it allows content to shrink if the viewport is too small. E.g. if you set width: 60rem but the viewport was only 40rem wide the element would overflow by 20rem.

.center {
max-width: 30rem;
}
<div class="center">
<div class="box">Box 1</div>
</div>
Box 1
Constaining max-width

We can then use margin to control where the constrained column goes. Setting margin to auto tells the browser to use as much of the leftover available space as possible. E.g. if we set margin-left: auto it would push the element all the way to the right (since the left margin would take up all of the available space):

.center {
max-width: 30rem;
margin-left: auto;
}
Box 1
Left auto-margin example

To center an element we can balance this out with an equal margin-right: auto. Now both margins will get half the available space, pushing the element to the middle.

.center {
max-width: 30rem;
margin-left: auto;
margin-right: auto;
}
Box 1
Left and right auto-margin example

Customising width

We’re going to need control over how wide the Center allows content to get, otherwise it’s not very re-usable. We can control this in a couple of ways.

First we could use a CSS variable for the max-width:

.center {
max-width: var(--max-width, 30rem);
margin-left: auto;
margin-right: auto;
}

This will default to 30rem if no variable is set, but we can override it if needed:

<div class="center" style="--max-width: 10rem">
<div class="box">Box 1</div>
</div>
Box 1
Narrower Center example

This is very easy to use, but has a couple of disadvantages. First it allows any value to be used. This is flexible but will lead to inconsistency in our design. It’s better to pick pre-determined “allowed widths” so your layout doesn’t look random.

Second, CSS variables are inherited, which means nested Centers will use the --max-width from their parent.

<div class="center" style="--max-width: 60rem">
<div class="center">
<div class="box">Box 1</div>
</div>
</div>

You might expect the second .center here to be 30rem wide, since that’s the default. However it will inherit the --max-width: 60rem from its parent, which is unexpected.

Instead of CSS variables we can define “modifier” classes that we apply to override the max-width rule:

.center {
max-width: 30rem;
margin-left: auto;
margin-right: auto;
}
.width-sm {
max-width: 20rem;
}
.width-lg {
max-width: 40rem;
}
.width-xl {
max-width: 60rem;
}

Now we can add extra classes when we want different widths.

<div class="center width-xl">
<div class="center">
<div class="box">Box 1</div>
</div>
</div>

Challenge 1: using the Center

You’re going to fix the layout of this page. Currently all the content is full-width and it’s hard to read. Download the starter files using the command at the start of the workshop, then open challenge-1/index.html in your editor.

Challenge 1 preview

The header content should be constrained to 60rem wide, the first section to 40rem wide, and the contact section to 20rem wide.

Add the Center CSS you need to the style tag at the top. Then add classes to the HTML, but don’t change it in any other way. Here is the result you’re aiming for:

Challenge 1 solution

The Stack: controlling vertical space

The most important layout primitive is one to control the space between elements. For re-usability and simplicity it’s a good idea not to apply spacing rules to individual elements. E.g. if you put margin-left on a button you can only re-use it in places where left spacing makes sense.

It’s better to use a parent element to apply spacing to its children. This is often called a “stack”. There are lots of ways to implement this (e.g. using flexbox or grid), but for simplicity we’re going to do it with margin.

Let’s say we want 1rem of space between each of these boxes:

<div>
<div class="box">Box 1</div>
<div class="box">Box 2</div>
<div class="box">Box 3</div>
</div>
Box 1
Box 2
Box 3
Boxes with no space between them

We could add styles to our .box class, but then we couldn’t re-use those boxes in other places where margin-top didn’t work. Instead we can use the parent to add margin to its children:

.stack > * {
margin-top: 1rem;
}
<div class="stack">
<div class="box">Box 1</div>
<div class="box">Box 2</div>
<div class="box">Box 3</div>
</div>
Box 1
Box 2
Box 3
Boxes with space above all of them

This isn’t quite right: we’ve got space above every child—we only want the space between the children. This means no space above the first child.

There are a few ways to achieve this. We could add a rule disabling the margin for the first child:

.stack > * {
margin-top: 1rem;
}
.stack > *:first-child {
margin-top: 0;
}

Or we could only apply the rule to elements that are not the first child:

.stack > *:not(:first-child) {
margin-top: 1rem;
}

Or we could use the adjacent sibling combinator to only apply the rule to elements that have a sibling before them:

.stack > * + * {
margin-top: 1rem;
}
Box 1
Box 2
Box 3
Boxes with space between them

Customising spacing

Our stack primitive is useful, but we’re going to need different amounts of spacing to make a whole page. We can control this using multiple classes, just like with the Center.

This is nice because it allows us to choose a pre-set number of sizes, which keeps our layout consistent.

.stack-sm > * + * {
margin-top: 0.5rem;
}
.stack-md > * + * {
margin-top: 1rem;
}
.stack-lg > * + * {
margin-top: 2rem;
}
.stack-xl > * + * {
margin-top: 4rem;
}

Now we can control the space more easily:

<main class="stack-xl">
<section class="stack-md">
<div class="box">Box 1</div>
<div class="box">Box 2</div>
<div class="box">Box 3</div>
</section>
<section class="stack-md">
<div class="box">Box 4</div>
<div class="box">Box 5</div>
<div class="box">Box 6</div>
</section>
</main>
Box 1
Box 2
Box 3
Box 4
Box 5
Box 6
Nested stacks with differing space between them

Challenge 2: using the Stack

You’re going to use the Stack to fix the layout of a web page. Open challenge-2/index.html in your editor.

Challenge 2 preview

Currently there’s no space between anything. There should be 2rem of space between each section. There should be 1rem of space between the elements within each section. There should be 0.5rem between each form field and its label.

Fix the layout by defining Stack CSS inside the style tag, then only adding Stack classes to the HTML. Don’t add or remove any elements or write any other CSS! You can create this whole layout using only Stacks.

Challenge 2 solution preview

The Row: placing elements next to each other

Another very useful layout primitive is a “row”. Web interfaces often need elements placed next to each other. For example a horizontal list of links in a navigation bar, or the “Confirm” and “Cancel” buttons in a dialogue popup.

Are you sure you'd like to delete everything?

Buttons sitting next to each other in a row

Flexbox is designed for one-dimensional layouts, so it is perfect here. Setting display: flex on an element lets it control how its children are laid out. By default they will all be put in a single row.

.row {
display: flex;
}
These boxes are as big as their content
These boxes are as big as their content
Flex container example

Making it responsive

If you resize the container using the handle on the bottom right you’ll see that this layout doesn’t adapt. By default the flex children will shrink as much as their content allows, but they can’t get smaller than the longest word inside them. Once the container gets narrower than this they stop shrinking and get cut off.

This means our layout isn’t flexible enough to cope with different screen sizes. Generally when you put things in a row you want to make sure they can wrap when there’s no more space.

.row {
display: flex;
flex-wrap: wrap;
}
These boxes are as big as their content
These boxes are as big as their content
Flex wrap example

Resizing this example shows the right-most child wrapping onto a new line when there isn’t enough space for it.

What about media queries?

Note that we don’t need to add media queries here. Those are great when you need really specific control over exactly how and when the layout should change. But this layout is intrinsically responsive. It flows to fit whatever container it is inside based on its content. This tends to be simpler and more robust than trying to figure out exactly what breakpoints to add in media queries.

Spacing children out

Layouts usually require some space between each element. CSS has a handy property for controlling this for flexbox and grid containers: gap. This is a shorthand for row-gap and column-gap, which allow you to control the vertical/horizontal spacing separately.

.row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
These boxes are as big as their content
These boxes are as big as their content
Flex gap example

Note that the gap is maintained even when the children wrap. Using gap for flexbox is quite new, but it is supported by all modern browsers. If you need to support older browsers you can approximate the same effect using margins, but it’s more complex to make sure it handles wrapping.

Alignment

Flexbox allows control over how children are aligned both horizontally and vertically. Most of the time you want things vertically centered, so that different height children line up. You can control vertical alignment with align-items:

.row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
}
Small box
Tall box
Flex vertical alignment example

You can allow this value to be customised the same way we did for the Stack’s space above. Either a CSS variable or modifier classes:

.row {
/* ... */
align-items: var(--align, center);
}
.row {
/* ... */
align-items: center;
}
.align-start {
align-items: start;
}
.align-end {
align-items: end;
}
/* etc */

You may also need to allow control of horizontal alignment using the justify-content property. This lets the container push its children apart, or to either end of the container.

.row {
/* ... */
}
.justify-end {
justify-content: flex-end;
}
<div class="row justify-end">
<div class="box">Box 1</div>
<div class="box">Box 2</div>
</div>
Box 1
Box 2
Flex horizontal alignment example

Challenge 3: using the Row

Open challenge-3/index.html in your editor. You should see a page with a header containing a logo and a nav.

Challenge 3 preview

You need to make the header layout work correctly. The logo should be on the far left, with the nav on the far right, and all the links in a row, like this:

Challenge 3 solution

Again, only add Row CSS to the style tag and classes to the HTML. Don’t add any new HTML elements.

The Grid: equal-sized children

Sometimes you need to create a grid of elements, like an image gallery. Every element should be the same size, and the grid should automatically put as many elements as it can in a row.

CSS grid is perfect for this. It lets us create a two-dimensional layout (with columns and rows), and keeps all the elements consistently sized (unlike flexbox).

We can make a grid and set a specific number of columns. We can also use gap to space the columns out:

.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
}
<div class="grid">
<div class="box">Box 1</div>
<div class="box">Box 2</div>
<div class="box">Box 3</div>
<div class="box">Box 4</div>
<div class="box">Box 5</div>
<div class="box">Box 6</div>
</div>

Here we’re defining three columns that take up one fraction (1fr) of the available space. So they’ll all be the same size.

Box 1
Box 2
Box 3
Box 4
Box 5
Box 6
Three-column grid example

The children automatically get slotted into new rows, but there are always three columns. This isn’t very responsive: if you resize the example you’ll see the boxes get squished.

The solution is a fancy CSS trick that tells the grid to automatically create as many columns as it can fit:

.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
gap: 1rem;
}

This rule will create as many equal-sized columns as it can, as long as they don’t get smaller than 10rem. As the viewport gets bigger it’ll add columns; as the viewport gets smaller it’ll remove them.

Box 1
Box 2
Box 3
Box 4
Box 5
Box 6
Three-column grid example

Resize the example and you should see the grid automatically reflow to fit the available space.

Challenge 4: bring it all together

For the final challenge you’ll be recreating the Instagram Web profile layout—without writing any CSS at all.

Here’s how it currently looks:

Challenge 4 preview

And here’s what you’re aiming for:

Challenge 4 solution

Open challenge-4/index.html in your editor. You need to get as close to the final layout as you can by only adding classes to the HTML. No touching the CSS!