Log in

Responsive contact form

Paired fields sit side by side when there is room and stack when there is not. It takes one grid declaration and one media query — no framework grid, no column classes, and nothing that needs recompiling when you change a breakpoint.

Live preview: this is the HTML and CSS below, rendered as-is.

Replace YOUR_FORM_ID with a form of your own and this sends to your inbox.

Create your form
contact.html
<form class="grid-form" action="https://submit.formpost.ai/YOUR_FORM_ID" method="POST">
  <h2>Contact us</h2>

  <div class="row">
    <p class="field">
      <label for="first">First name</label>
      <input id="first" type="text" name="first_name" required>
    </p>
    <p class="field">
      <label for="last">Last name</label>
      <input id="last" type="text" name="last_name" required>
    </p>
  </div>

  <div class="row">
    <p class="field">
      <label for="email">Email</label>
      <input id="email" type="email" name="email" required>
    </p>
    <p class="field">
      <label for="phone">Phone <span>(optional)</span></label>
      <input id="phone" type="tel" name="phone">
    </p>
  </div>

  <p class="field">
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="5" required></textarea>
  </p>

  <button type="submit">Send message</button>
</form>
contact.css
body {
  margin: 0;
  padding: 28px 16px;
  background: #fafafa;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

.grid-form {
  max-width: 640px;
  margin: 0 auto;
}

.grid-form h2 {
  margin: 0 0 20px;
  font-size: 22px;
  color: #18181b;
}

/* One column is the default, not a small-screen special case. Mobile-first
   means a narrow screen reads the shortest stylesheet and undoes nothing. */
.row {
  display: grid;
  gap: 0 16px;
  grid-template-columns: 1fr;
}

.field {
  margin: 0 0 16px;
}

.field label {
  display: block;
  margin-bottom: 6px;
  font-size: 13px;
  font-weight: 600;
  color: #3f3f46;
}

.field label span {
  font-weight: 400;
  color: #a1a1aa;
}

.field input,
.field textarea {
  box-sizing: border-box;
  width: 100%;
  padding: 10px 12px;
  font: inherit;
  color: #18181b;
  background: #fff;
  border: 1px solid #d4d4d8;
  border-radius: 6px;
}

.field input:focus,
.field textarea:focus {
  border-color: #18181b;
  outline: 2px solid rgba(24, 24, 27, 0.12);
}

.field textarea {
  resize: vertical;
}

.grid-form button {
  width: 100%;
  padding: 12px 16px;
  font: inherit;
  font-weight: 600;
  color: #fff;
  background: #18181b;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
}

/* The breakpoint comes from the content, not from a device width: 540px is
   where these inputs stop being comfortable to tap two to a row. */
@media (min-width: 540px) {
  .row {
    grid-template-columns: 1fr 1fr;
  }

  .grid-form button {
    width: auto;
    min-width: 180px;
  }
}

Mobile-first means the phone gets no CSS

The single-column layout is the default rule, not a special case inside a max-width query. A narrow screen therefore reads the smallest possible stylesheet and never has to undo anything. The media query only ever adds — which is also why there is exactly one of them.

.row { grid-template-columns: 1fr; }

@media (min-width: 540px) {
  .row { grid-template-columns: 1fr 1fr; }
}

Pick breakpoints from the content

540px is not a device. It is the width at which these particular inputs stop being comfortable to tap when they sit two to a row — found by dragging the browser window until it looked wrong. Copying a phone or tablet width instead gives you a form that breaks on the next device that ships.

Which fields deserve to be paired

  • Pair fields of similar length and related meaning: first and last name, email and phone, city and postcode.
  • Never pair a short field with a long one — the empty space beside the short one reads as a missing field.
  • Keep the message textarea full width at every size. It is the field people spend the longest in.
  • Keep the pairs in a sensible tab order. Grid can visually reorder columns; the keyboard still follows the source, and a form that tabs sideways unexpectedly is worse than one that stacks.

Two name fields, one sender

Splitting the name means neither field is called name any more, so the endpoint can no longer guess who sent the message on its own. Set the notification subject in your form settings to pull both parts in:

subject template
New enquiry from {{first_name}} {{last_name}}
The email field keeps its name, so reply-to still works and you can answer straight from your inbox.

Questions about this contact form

Should I use CSS grid or flexbox for a responsive form?
Grid, for rows of fields. You declare the columns once on the container and the children need no width rules at all, so changing from one column to two is a single line. Flexbox needs a basis or width on every child.
How many breakpoints does a contact form need?
Usually one. A form is a single narrow column of controls; there is nothing to rearrange beyond deciding whether pairs of fields fit side by side. If you find yourself writing a third breakpoint, the layout is probably fighting you.
Do I need a meta viewport tag?
Yes, on the page — without it a phone renders at desktop width and zooms out, and your media query never fires. It goes in the head of the document, not in the form.
Why are the inputs box-sizing: border-box?
So width: 100% includes the padding and border. Without it a full-width input inside a grid column overflows its track by the padding on both sides, which is the usual cause of a form that scrolls sideways on a phone.

Related examples

Or go back to all 21 contact form examples.

Give this form an endpoint

Formpost takes the submission, emails it to you and keeps a searchable copy. Free for 250 messages a month, with unlimited forms and no card.