Skip to content

Basic CSS Layouts with Grid and Flexbox

Jeff Siarto edited this page Feb 2, 2025 · 18 revisions

Introduction

Page layouts on the web have come a long way since the days of using an HTML <table> to cobble together complex designs. Not only were these pages bulky, but they were inaccessible to screen readers and other assistive devices. Today, modern CSS gives us a suite of tools for wrangling typography and moving objects around a browser window without too much trouble. In this tutorial we'll take a look a common layout that uses CSS Grid and Flexbox together, and use CSS custom properties to DRY up our stylesheet.

Prerequisites

Markup

If you started with the HTML boilerplate template, you should have an index.html file that looks like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Homepage</title>
    <link rel="stylesheet" href="screen.css">
  </head>
  <body>
    <h1>Home</h1>
    <nav>
      <ul>
        <li><a href="index.html" class="active">Home</a></li>
        <li><a href="about.html">About</a></li>
      </ul>
    </nav>
    <script src="index.js"></script>
  </body>
</html>

We're not going to be using the about.html page in this tutorial (you can just leave it alone), but we are going to be using the existing <nav> structure for the navigation on our page.

Add Additional Structure

Although we're keeping the <nav>, we need to add some additional HTML structure around it and after it to complete the markup for the site.

  1. Add a <header> element to contain the main heading and navigation. You can also add some additional navigation links you'd like:
<header>
  <h1>Home</h1>
  <nav>
    <ul>
      <li><a href="index.html" class="active">Home</a></li>
      <li><a href="about.html">About</a></li>
      <li><a href="#">Work</a></li>
      <li><a href="#">Contact</a></li>
    </ul>
  </nav>
</header>
  1. Add a <main> block below the <header> which includes the main subheading (what we're calling the hero) and the images. Notice that we're using Lorem Picsum, an image filler service to provide stand-in images for this example:
<main>
  <section id="hero">
    <h2>Lorem Picsum</h2>
  </section>
  <section id="stories">
    <a class="story" href="#"><img src="https://picsum.photos/1000" alt="placeholder"></a>
    <a class="story" href="#"><img src="https://picsum.photos/1000" alt="placeholder"></a>
    <a class="story" href="#"><img src="https://picsum.photos/1000" alt="placeholder"></a>
  </section>
</main>

Your completed index.html file should look like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Homepage</title>
    <link rel="stylesheet" href="screen.css">
  </head>
  <body>
    <header>
      <h1>Gallery</h1>
      <nav>
        <ul>
          <li><a href="#">Home</a></li>
          <li><a href="#">Work</a></li>
          <li><a href="#">About</a></li>
          <li><a href="#">Contact</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <section id="hero">
        <h2>Lorem Picsum</h2>
      </section>
      <section id="stories">
        <a class="story" href="#"><img src="https://picsum.photos/1000" alt=""></a>
        <a class="story" href="#"><img src="https://picsum.photos/1000" alt=""></a>
        <a class="story" href="#"><img src="https://picsum.photos/1000" alt=""></a>
      </section>
    </main>
  <script src="index.js"></script>
  </body>
</html>

Style

Before we get started with specific section styling rules, let's set up some custom properties (CSS variables) for our reusable values. Add the following :root ruleset to screen.css (or whatever the CSS file is named that's linked in the <head> of your HTML file):

:root {
  --main-bg: #222;
  --main-bg-alt: #111;
  --main-type: #f3f3f3;
  --main-type-alt: #aaa;
  --image-border-radius: 20px;
  --header-border-radius: 40px;
}

Each of these custom properties can be accessed further down in the CSS file by utilizing the var() keyword: color: var(--main-bg);

Body and Navigation Styling

Below the :root ruleset in your CSS file, add the following block of CSS. Notice how we're using the var() keyword to insert our variable values from the :root ruleset. This will allow us to easily change those values at the top of our CSS file, without having to find and replace across our entire file.

body {
  margin: 0 20%;
  background: var(--main-bg);
  color: var(--main-type);
  font-family: Helvetica, sans-serif;
}

header {
  margin-top: 40px;
  padding: 0 30px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--main-bg-alt);
  border-radius: var(--header-border-radius);
}

header h1 {
  margin: 0;
  font-size: 1em;
  text-transform: uppercase;
}

nav ul {
  padding: 0;
  list-style-type: none;
}

nav ul li {
  display: inline-block;
  margin-left: 20px;
}

nav ul li a {
  color: var(--main-type-alt);
  text-decoration: none;
}

Most of the "style" for the header and navigation comes from the header {} rule. We use a display: flex to align the h1 and nav--essentially combining them into a single element. This is bolstered by the cohesive background and corner radii that will match the rest of the page.

Tip

Adjust the values of different CSS properties above and see what effect they have on the resulting design. Specifically, look at the align-items and justify-content properties in the header.

Hero Text and Image Grid

Below the CSS for the header, add the following block to style the main image grid:

main {
  margin-bottom: 40px;
}

#hero h2 {
  font-size: 6em;
  margin: 60px 0 40px 0;
  text-align: center;
}

#stories {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 20px;
}

.story img {
  width: 100%;
  border-radius: var(--image-border-radius);
}

.story:nth-child(3) {
  grid-row: 1 / 3;
  grid-column: 2 / 4;
}

Let's break down the CSS grid parts of this style. The first ruleset related to the grid is #stories:

#stories {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 20px;
}
  • display: grid tells the browser to use CSS grid for this element and its children. This is effectively changes it from a block element to a grid element.
  • grid-template-columns allows us to define the column properties for our grid. The space taken up by each column is called a track. We can use the fractional unit (fr) as a value here to tell the browser to create a flexible three-column grid that's evenly spaced among the tracks. There is a lot to unpack with this one feature alone--check out the CSS Tricks page on this property for details.
  • gap is the space between all the individual grid areas in the layout--a grid padding.

Close to the bottom of the CSS file, we one final set of CSS grid properties:

.story:nth-child(3) {
  grid-row: 1 / 3;
  grid-column: 2 / 4;
}
  • .story:nth-child(3) is a CSS pseudo-class that allows us to target a specific element based on its index (order in the list). Here we are targeting the 3rd .story in the group of children.
  • grid-row specifies the row start and end line of this element. 1 / 3 you have the row start on line 1 and end on line 3 (the full height of our grid in this example).
  • grid-column specifies the column start and end line of this element. 2 / 4` you have the column start on line 2 and end on line 4 (the full width of our grid in this example).

Final screen.css

:root {
  --main-bg: #222;
  --main-bg-alt: #111;
  --main-type: #f3f3f3;
  --main-type-alt: #aaa;
  --image-border-radius: 20px;
  --header-border-radius: 40px;
}

body {
  margin: 0 20%;
  background: var(--main-bg);
  color: var(--main-type);
  font-family: Helvetica, sans-serif;
}

header {
  margin-top: 40px;
  padding: 0 30px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--main-bg-alt);
  border-radius: var(--header-border-radius);
}

header h1 {
  margin: 0;
  font-size: 1em;
  text-transform: uppercase;
}

nav ul {
  padding: 0;
  list-style-type: none;
}

nav ul li {
  display: inline-block;
  margin-left: 20px;
}

nav ul li a {
  color: var(--main-type-alt);
  text-decoration: none;
}

main {
  margin-bottom: 40px;
}

#hero h2 {
  font-size: 6em;
  margin: 60px 0 40px 0;
  text-align: center;
}

#stories {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 20px;
}

.story:nth-child(3) {
  grid-row: 1 / 3;
  grid-column: 2 / 4;
}

.story img {
  width: 100%;
  border-radius: var(--image-border-radius);
}

Bonus: ScrollReveal.js

Sometimes small animatinos can have a big impact on the overall UI. I like to use ScrollReveal when I need a little action on the page. Let's use a simple call to add some motion to the main images.

Add the following inside the <head> of index.html:

<script src="https://unpkg.com/scrollreveal"></script>

In your index.js file, add the following JavaScript to initiate the scroll and set some options (checkout the API docs for all the different settings you can change):

ScrollReveal().reveal('.story', {
  delay: 500,
  interval: 200
});

Clone this wiki locally