Log in

Dark mode contact form

One set of rules, two themes. Every colour is a custom property, prefers-color-scheme redefines that set, and not a single layout or component rule knows which theme it is in. No JavaScript and no flash of the wrong colours on load.

The dark palette, forced on for this preview. In your own page the same stylesheet follows the visitor's system setting.

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

Create your form
contact.html
<form class="theme-form" action="https://submit.formpost.ai/YOUR_FORM_ID" method="POST">
  <h2>Contact us</h2>
  <p class="sub">Dark by default, light if your system says so.</p>

  <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="message">Message</label>
  <textarea id="message" name="message" rows="4" required></textarea>

  <button type="submit">Send message</button>
</form>
contact.css
/* Every colour is a custom property, so switching themes rewrites this one
   block and not a single rule below it. */
:root {
  color-scheme: light dark;
  --bg: #ffffff;
  --surface: #f8fafc;
  --text: #0f172a;
  --muted: #64748b;
  --line: #e2e8f0;
  --accent: #0ea5e9;
  --on-accent: #ffffff;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0b1120;
    --surface: #111a2e;
    --text: #e2e8f0;
    --muted: #94a3b8;
    --line: #1e293b;
    --accent: #38bdf8;
    --on-accent: #0b1120;
  }
}

body {
  margin: 0;
  padding: 40px 16px;
  background: var(--bg);
  color: var(--text);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

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

.theme-form h2 {
  margin: 0;
  font-size: 22px;
}

.sub {
  margin: 6px 0 24px;
  font-size: 14px;
  color: var(--muted);
}

.theme-form label {
  margin-bottom: 6px;
  font-size: 13px;
  font-weight: 600;
}

.theme-form input,
.theme-form textarea {
  font: inherit;
  margin-bottom: 18px;
  padding: 10px 12px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
}

.theme-form input:focus,
.theme-form textarea:focus {
  border-color: var(--accent);
  outline: 2px solid color-mix(in srgb, var(--accent) 30%, transparent);
}

.theme-form textarea {
  resize: vertical;
}

.theme-form button {
  padding: 11px 16px;
  font: inherit;
  font-weight: 600;
  color: var(--on-accent);
  background: var(--accent);
  border: 0;
  border-radius: 8px;
  cursor: pointer;
}

Define the palette once, redefine it once

The seven custom properties on :root are the entire colour system. The media query below rewrites those seven values and nothing else — no duplicated selectors, no .dark class threaded through every rule, and no way for the two themes to drift apart as the form grows.

:root {
  --bg: #ffffff;
  --text: #0f172a;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0b1120;
    --text: #e2e8f0;
  }
}

color-scheme is doing real work

The color-scheme: light dark declaration tells the browser to render its own chrome in the matching theme: scrollbars, the text caret, the date picker, and the dropdown that opens out of a select. Without it you get a dark form with a bright white select popup — the giveaway of a dark mode that was only ever skin deep.

Do not use pure black

  • The dark background here is #0b1120, not #000. Pure black against light text produces halation — the text appears to smear — and gives you nowhere to go for a raised surface.
  • The body text is #e2e8f0, not #fff. Full-white text on a dark field is noticeably harsher than a slightly warmed grey.
  • Surfaces get lighter as they come forward, which is the opposite of the light theme, where they get darker. That is why --surface is a separate property rather than a tint calculated from --bg.
  • The accent goes lighter in dark mode (#0ea5e9 to #38bdf8) and the text on it goes dark. An accent that passes contrast on white will usually fail on near-black.

Adding a manual toggle later

Because every rule reads from the custom properties, a toggle only has to set the same seven values on a data attribute — for example :root[data-theme="dark"] — placed after the media query so an explicit choice beats the system preference. The form itself never changes.

The submission side is identical in either theme. The action attribute is what sends the message; the colours are entirely your page's business.

Questions about this contact form

Does dark mode need JavaScript?
Not for following the system setting — prefers-color-scheme is pure CSS and applies before the first paint, so there is no flash of the wrong theme. You only need JavaScript if you want a manual toggle that overrides the system.
Why does my select still open with a white background?
That popup is drawn by the operating system, not your CSS. color-scheme: light dark on :root is what tells it to follow the theme. The same declaration fixes scrollbars and the text caret.
What contrast ratio should the placeholder text hit?
Aim for 4.5:1 against the input background, same as body text. Placeholder grey is where dark themes fail accessibility checks most often, because a grey that looks fine on white becomes almost invisible on near-black.
Can I ship dark mode only?
You can — delete the light block and set the dark values directly on :root, keeping color-scheme: dark. Just be aware you are overriding a preference the visitor already expressed.

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.