Log in

CSS contact form

Most contact form CSS styles the resting state and stops. This one covers what a field looks like while you hover it, while you are typing in it, after you have got it wrong, and while it is submitting — which is where forms actually feel broken or solid.

Three states frozen side by side so you can see them at once. In the code below they come from :focus-visible and :user-invalid, which only appear once somebody interacts with the field.

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

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

  <label for="name">Name</label>
  <input id="name" type="text" name="name" required>

  <label for="email">Email</label>
  <input id="email" type="email" name="email" required>

  <label for="url">Website</label>
  <input id="url" type="url" name="website">

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="3" placeholder="How can we help?" required></textarea>

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

form {
  display: flex;
  flex-direction: column;
  max-width: 420px;
  margin: 0 auto;
}

h2 {
  margin: 0 0 18px;
  font-size: 20px;
}

label {
  display: flex;
  gap: 8px;
  align-items: baseline;
  margin-bottom: 6px;
  font-size: 13px;
  font-weight: 600;
  color: #374151;
}

.hint {
  font-size: 11px;
  font-weight: 500;
  color: #9ca3af;
}

input,
textarea {
  box-sizing: border-box;
  font: inherit;
  margin-bottom: 14px;
  padding: 10px 12px;
  color: #111827;
  background: #fff;
  border: 1px solid #d1d5db;
  border-radius: 8px;
  transition: border-color 0.15s, box-shadow 0.15s;
}

/* Hover changes colour only. Touching padding or border-width here makes the
   whole form twitch as the pointer crosses it. */
input:hover,
textarea:hover {
  border-color: #9ca3af;
}

/* :focus-visible, not :focus. A mouse click should not leave the keyboard
   ring behind — but tabbing here must show it. */
input:focus-visible,
textarea:focus-visible {
  border-color: #4f46e5;
  box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.18);
  outline: none;
}

/* :user-invalid only matches after the visitor has interacted with the field.
   With :invalid, every required field is red the moment the page loads. */
input:user-invalid,
textarea:user-invalid {
  border-color: #dc2626;
  box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.14);
}

input::placeholder,
textarea::placeholder {
  color: #9ca3af;
}

textarea {
  resize: vertical;
}

button {
  padding: 11px 16px;
  font: inherit;
  font-weight: 600;
  color: #fff;
  background: #4f46e5;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.15s;
}

button:hover {
  background: #4338ca;
}

button:active {
  transform: translateY(1px);
}

button:disabled {
  background: #c7d2fe;
  cursor: default;
}

:focus-visible, not :focus

A focus ring exists for people navigating by keyboard. :focus fires for mouse clicks too, which is why designers keep asking to remove it — it looks like a stray outline appearing after a click. :focus-visible only fires when the browser judges the ring to be useful, which means you can keep it prominent instead of arguing about it.

input:focus-visible {
  border-color: #4f46e5;
  box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.18);
  outline: none;
}

outline: none is only acceptable on the same rule that replaces it. On its own it is the most common accessibility failure in form CSS.

:user-invalid, not :invalid

A required field is invalid from the moment the page loads, so styling :invalid paints every field red before the visitor has typed a single character. :user-invalid waits until they have interacted with the field and left it wrong — which is the moment you actually wanted.

Browser support for :user-invalid is broad now. If you need a fallback, add the error class from your submit handler instead and keep the same styling rule.

States worth styling, in order of payoff

  • focus-visible — non-negotiable. Without it a keyboard user cannot tell where they are.
  • disabled — the button during submit. Grey it out and change the cursor, or people will click it three times.
  • user-invalid — turns a rejected form from mysterious into obvious.
  • hover — the smallest payoff, and the easiest to get wrong. Change colour only; changing padding or border-width makes the layout twitch as the pointer crosses it.
  • placeholder — style it explicitly. The browser default is often too light to read, and it is not inherited from the input colour.

Transitions that do not get in the way

Transition colour and shadow, never size or position. 150ms is the sweet spot: long enough to register as a response, short enough that a fast typist tabbing through fields never waits for it. And transition the specific properties rather than all, so you are not accidentally animating something you add later.

Questions about this contact form

How do I style a contact form without removing the focus outline?
Replace it in the same rule. Set outline: none alongside a border-colour change and a box-shadow ring, so the state is still unmistakable. Setting outline: none by itself removes the only cue keyboard users have.
Why do all my required fields turn red on page load?
You styled :invalid. A required empty field is invalid immediately. Use :user-invalid, which only matches after the visitor has interacted with the field.
Can I style the placeholder text?
Yes, with ::placeholder. Style it explicitly rather than relying on the default — check it against WCAG AA on your background, since placeholder grey is a frequent contrast failure.
How do I style a select to match my inputs?
Set appearance: none to drop the native arrow, then apply the same padding, border and radius as your inputs and draw your own arrow with a background image. Keep color-scheme set on :root so the open dropdown still follows the theme.

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.