Tutorial: A Responsive Form Layout with CSS Grid

Lara Aigmüller

A page of our small example website we didn’t really take care of until now is the contact page. In part three of this tutorial series we’ve built a small accessible contact form with HTML. Today we are going to style and layout it with CSS. 🥳

As always, find the example code from the last part, and open the contact.html page in a text editor and a browser. If you can’t find your own work, you could also check out my version that is available on GitHub, where you can have a look at the code of each step of this web development basics tutorial series.

Recap: HTML form markup

Let’s have a quick look at the HTML code and get familiar again with all the form related tags. We’ve created a <form> and added a lot of different <input> elements with corresponding <label> elements wrapped by <div> containers.

Remember that each input element needs some sort of label and the best way for this is to use the <label> tag and “connect” the two using the id attribute on the input tag and the for attribute (having the exact same value) on the label tag.

This looks as follows for the name input field:

<div>
  <label for="name-input">Name</label>
  <input id="name-input" type="text" name="name" placeholder="Jane Doe" required />
</div>

After we’ve added some basic styles and layout during the last tutorials, this is what the contact page looks in the browser at the moment:

A screenshot of the contact page including the contact form we’ve built in the third part of this tutorial.

There’s definitely some room for improvement here… 😊

Translating wireframes into HTML

Again, we are first going to have a look at wireframes that describe the basic layout of the form visually for small and large screens.

A wireframe with a single column layout of the contact form. Above each input is a label, the send button is centered at the bottom.

A wireframe with a two column layout of the contact form. The labels are placed in the first column, the input elements in the second, which is approximately double the width than the first one, and the send button is centered at the bottom.

The challenge now is to find markup that works with both versions and can be transformed with CSS from one into another with some of the features we’ve learned last time about responsive web design.

For this part of my website I’d like to use CSS grid layout, another powerful layout mode. In order to decide on the HTML structure, a little grid knowledge is required, so I’m going to share my approach with you now and describe each decision step by step.

A quick side note: At the end of last year I wrote an article for HTMHell about how I usually approach this wireframe-to-HTML transformation based on another example. Have a look if you are interested and would like to get more insights into my thought process! 💭

CSS grid layout: an introduction

The CSS grid layout mode is similar to the CSS flexible box layout in certain aspects, but instead of just one direction it defines two directions: rows and columns. (Remember, with flex layout we could choose between flex-direction: row and flex-direction: column.)

What they have in common is the fact that once I set display: grid to an element, this one becomes the grid container and the direct child elements become grid items and can be placed within the grid.

Changing the markup

Our current HTML markup is structured as follows:

<form>
  <div>
   <label>...</label>
   <input ... />
  </div>
  <div>
   <label>...</label>
   <input ... />
  </div>
</form>

I want the form to become the grid container, but then the div elements would become grid items. In the wireframe above, though, the labels and the inputs are placed in different columns, so I need these elements to become my grid items, which means they need to become the direct children of the form element. (Once CSS subgrid is supported in all major browsers, we can talk about this again and choose a different approach. By the time of writing this article it is only supported in Safari and Firefox.)

What we need to do now is to restructure our HTML code in a way that each item (or group of items) that is placed in another column of the two column layout is a direct child element of the <form> tag. Therefore, I remove the <div> wrapper elements from the name, email, age, and message fields.

The checkbox input and its label for the newsletter subscription keep the div wrapper, because I don’t want to tear the checkbox and its label apart.

An accessible radio group

Similar to the checkbox, I keep the radio inputs and their labels grouped as well, wrap them in an additional containing element, and add a label for the group.

<div id="contact-reason-label">What’s the reason of your contact request?</div>
<div role="radiogroup" aria-labelledby="contact-reason-label">
  <div>
    <input type="radio" id="website-request-input" ... />
    <label for="website-request-input">I need a <strong>website</strong>. Could you help me with that?</label>
  </div>
  <!-- more radio input elements -->
</div>

To describe the semantic meaning of the additional group label and where it belongs I added the ARIA role="radiogroup" attribute to the additional div container. The aria-labelledby attribute uses the value of the label’s id, similar to how we connect an input with its label with id and for.

Grouping radio buttons can (and should usually) be done using a <fieldset> with a <legend> and no need of additional ARIA attributes. This comes with drawbacks regarding our desired layout, so I chose the above solution instead.

Building the layout for small screens

As described in the last tutorial series part, I prefer implementing the one column layout first. In my browser, I open the developer tools, switch to the responsive design mode in Firefox, and set my viewport size to 380px.

A screenshot of the small version of our contact form after the markup updates.

What we can see in the screenshot above is that now all labels and input elements sit in one line, because they are inline elements (and we’ve removed the wrapping div containers which are block elements). We can also spot an overflow problem with the text area that makes our page horizontally scrollable, which is something we should try to avoid. The reason is that we set the cols attribute of the <textarea> to 50. Once removed, the page looks better. 🙂

Now I want most of my labels and input elements to sit in their own line (except the ones of the checkbox and radio elements) according to the wireframe. I could start making block elements out of inline elements using display: block and fiddle around until I get the expected result, but I have another idea… 💡

I add class="contact-form" to the form tag and use this as selector in my CSS:

.contact-form {
  display: grid;
  gap: 8px;
}

What happens now is that the form becomes a grid container and all its direct children turn into grid items. Because I didn’t specify anything else, the grid consists of one column, each item creates an implicit grid row and between the rows is a gap of 8px (we already know this property from flexbox). Have a look in the browser and see that we’re a huge step closer to the desired single column design! 🎉 In the screenshot below the grid inspector is turned on, and you can see the CSS grid in action.

A screenshot of the contact form in a small viewport with the grid inspector turned on in the developer tools showing the single column and the implicit row as well as the grid gap.

Spacing and color adjustments

A few spacing and design adjustments are still missing:

  • increasing the line height
  • increasing the distance between one input field and the next
  • choosing a different color and font for the labels

I’d like to have a slightly increased line height to make the labels of the radio group better readable, so I update my code as follows:

.contact-form {
  display: grid;
  gap: 8px;
  line-height: 1.3;
}

To separate one input field from the next, we need to increase the space between an input and the label of the next one, so that a label and its associated input field are visually grouped. I can’t just select all the label elements in the form, because I don’t want to change the labels of the radios and the checkbox or the first label on top. We also have custom labels and wrappers in our grid. Therefore, I make the following changes to my HTML code:

<!-- adding a class for styling to the contact reason label -->
<div id="contact-reason-label" class="contact-reason-label">What’s the reason of your contact request?</div>

<!-- adding a class to the checkbox wrapper -->
<div class="newsletter-subscription">
  <!-- newsletter checkbox here -->
</div>

The following code snippet shows the CSS code for the adjusted spacing.

.contact-form > label:not(:first-child),
.contact-reason-label,
.newsletter-subscription,
.contact-form [type="submit"] {
  margin-block-start: 16px;
}

Now each input is clearly separated from its siblings by applying the law of proximiy.

Last but not least, I’d like to style the labels differently. Let’s update the CSS code as follows:

.contact-form > label,
.contact-reason-label {
  font-family: "Georgia", "Times", serif;
  color: indigo;
}

For now, I’m happy 😊—let’s dig a little deeper into CSS grid layout and its features!

Building the two column layout

As shown in the wireframe above, there should be a two column layout for the form on larger screens. To define explicit grid columns we can use the grid-template-columns property. I hope, you remember the last article? In order to activate styles on specific screen sizes only, we can use CSS media queries, so here’s what I’m going to do:

@media screen and (min-width: 800px) {
  .contact-form {
    grid-template-columns: 1fr 2fr;
  }
}

Hey, there’s a new CSS length unit: fr, which stands for fraction. In our example, the browser calculates the available space for the contact form and divides it by three. One part goes to the first column and two to the second one, so the second column takes up twice as much space as the first column. This is a great feature that saves us a lot of time calculating widths and media query breakpoints! 🧮

Similar to the text with image sections we’ve built last time, the content container’s width is too small for the two column layout. The solution: we add the content-container-wide class to the contact form’s content container and the page looks great again. Go to your browser and check it out!

A screenshot of the contact form with the two column grid layout we’ve just built.

There’s something that looks a bit off, though… The second and third input fields are larger than the first one and the contact request label is not aligned with the first radio item. The reason is the margin we’ve added before, so we need to reset it again here:

@media screen and (min-width: 800px) {
  .contact-form {
    grid-template-columns: 1fr 2fr;
    gap: 24px;
  }

  .contact-form > label:not(:first-child),
  .contact-reason-label,
  .newsletter-subscription,
  .contact-form [type="submit"] {
    margin-block-start: 0;
  }
}

I’ve also adjusted the gap property and set it to 24px, because I want the content to have more space on larger viewport sizes. In a two column layout this is a shorthand for column-gap and row-gap, which means the spacing is adjusted in both directions.

Defining a grid item’s position

According to the wireframe, the checkbox for the newsletter subscription should be positioned directly below the textarea. By default, the grid algorithm fills the empty cells one by one with the items as they are sorted in the HTML code. We can change that by defining an explicit position for the item, for example by adding the desired starting line number.

The following screenshot shows the line numbers of the grid when the grid inspector is activated. Maybe you need to activate this feature in the Firefox developer tool’s layout section first.

A screenshot of the contact form with the two column grid layout. The grid inspector is activated and shows the line numbers. This feature can be activated in the Firefox developer tools’s layout tab in the grid section, which is highlighted in the screenshot.

We want the newsletter checkbox to go from the vertical line 2 to line 3. This is how we write this in CSS:

.newsletter-subscription {
  grid-column-start: 2;
}

We only need to define the start, because the item spans one column by default and that’s our desired goal.

The last question: how do we center the button at the bottom of the two column form? The answer in CSS:

.contact-form [type="submit"] {
  grid-column: 1 / span 2;
  justify-self: center;
}

The first line defines the item’s position by setting the starting line and the amount of columns it should span, separated by a slash. Additionally, we need to adjust the item’s justification, which is the horizontal alignment within a grid cell. By default this value is set to stretch which makes the button span the entire row.

Looks like we’ve built an HTML form layout with CSS grid as defined by the wireframes above! 🎉

CSS grid takeaways

Here’s a summary of the features about CSS grid we’ve heard about in this article:

  • When adding display: grid to an HTML element, the element itself becomes the grid container and its direct children become grid items.
  • The CSS grid layout algorithm implicitly creates one grid column and one row for each child when not specified otherwise.
  • Using the grid-template-columns property you can define the desired number and size of the columns.
  • The gap property is a shorthand for row-gap and column-gap and defines the space between rows and columns.
  • By default, each grid item is placed in the next free grid cell.
  • Using grid-column-start (or grid-row-start) you can define the starting position of a specific grid item.
  • The grid-column property let’s you define the start line and, separated by a slash, the amount of columns the item should span across.
  • By default, the grid items are stretched within their available space. The justify-self and the align-self properties can be used to change this for single grid items in horizontal and vertical direction, respectively. (Use justify-items and align-items on the grid container when you want to change the behavior for all children.)

Obviously, we’re only scratching the surface here. CSS grid is much more powerful.

There are many articles and tutorials about the inner workings of CSS grid out there. Here are some of my favorites and good places to start when you want to learn more:

  • CSS Grid Layout on MDN: an introduction with many continuing links
  • A complete guide to CSS Grid on CSS Tricks: check it out when you want to have an overview about all grid related CSS properties for the container and the items
  • Grid by Example by Rachel Andrew: guides and (video) tutorials—everything you need to know about CSS Grid

Keep in mind that the HTML code structure is an important basis for building layouts. It can happen that you start writing HTML code and once you add styles, you need to update the markup again and again in order to work with the CSS algorithms you choose. This is perfectly fine and part of the development process.

Bonus: Input and button styles

We can use our CSS knowledge and update the styles of the interactive elements in the form—inputs and buttons—and adjust it to the look and feel of the rest of the site. It’s mostly about updating spacing, borders, colors, and font sizes.

This is my code. Feel free to play around with the different CSS properties and find styles that you like and work with your design! 🎨

/* Increase the spacing between the radio controls. */
[role="radiogroup"] > div + div {
  margin-top: 8px;
}

/* Styles for the input fields and the textarea. */
.contact-form input,
.contact-form textarea {
  font-size: 0.875rem;
  font-family: "Helvetica", "Arial", sans-serif;
  padding: 8px 12px;
  border-radius: 4px;
  border: 2px solid lightgray;
}

.contact-form input:hover,
.contact-form textarea:hover {
  border-color: darkmagenta;
}

/* Styles for the submit button. */
.contact-form [type="submit"] {
  border-radius: 4px;
  border-color: indigo;
  background-color: indigo;
  color: #ffffff;
  padding: 8px 16px;
  min-width: 100px;
  font-weight: 700;
}

.contact-form [type="submit"]:hover {
  background-color: darkmagenta;
  border-color: darkmagenta;
}

Accent color

The default color of elements like radios and checkboxes is blue in most browsers. Using the CSS property accent-color, which is supported in all modern browsers, you can update the color to match your design.

.contact-form {
  accent-color: indigo;
}

And we’re done! Here’s a final screenshot of the contact form in Firefox.

A screenshot of the two column contact form with all the styles we applied during this part of the tutorial.

Now we’ve made our tiny website fully responsive, there’s one more related topic I’d like to talk about in the next article: responsive images using HTML—stay tuned! 😊

Any thoughts or comments?

Say hello on Mastodon or contact us via email or contact form!