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.
Replace YOUR_FORM_ID with a form of your own and this sends to your inbox.
<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>/* 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.
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
Modern contact form
A soft card, a gradient button and a proper focus ring. Pure CSS, no library.
Responsive contact form
Two columns on desktop, one on a phone. CSS grid and a single media query.
CSS contact form
Every state styled: hover, focus-visible, invalid, disabled, placeholder.
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.