Dark mode form
A contact form that follows the visitor light or dark system setting with a few CSS custom properties. Copy-paste ready.
A form that looks right in light mode often turns into a bright white box on a dark page, or loses its borders and placeholder text entirely. The fix is to define every colour once as a CSS custom property, then swap the values when the visitor's system is set to dark. This page gives a complete form that does that, with no JavaScript.
The complete page#
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Contact</title>
<style>
:root {
color-scheme: light dark;
--bg: #ffffff;
--text: #1f2328;
--muted: #59636e;
--field-bg: #ffffff;
--field-border: #8c959f;
--accent: #0757c7;
--accent-text: #ffffff;
--focus: #0757c7;
--error: #b42318;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f1115;
--text: #e6e8eb;
--muted: #a1a8b3;
--field-bg: #171a21;
--field-border: #5c6570;
--accent: #5b9dff;
--accent-text: #0b0d10;
--focus: #8cbaff;
--error: #ff8a80;
}
}
body {
background: var(--bg);
color: var(--text);
font-family: system-ui, sans-serif;
line-height: 1.5;
max-width: 32rem;
margin: 3rem auto;
padding: 0 1rem;
}
.field { margin-bottom: 1rem; }
label { display: block; font-weight: 600; margin-bottom: 0.25rem; }
.hint { color: var(--muted); font-size: 0.875rem; }
input, textarea {
width: 100%;
box-sizing: border-box;
padding: 0.6rem 0.75rem;
background: var(--field-bg);
color: var(--text);
border: 1px solid var(--field-border);
border-radius: 6px;
font: inherit;
}
::placeholder { color: var(--muted); opacity: 1; }
input:focus-visible, textarea:focus-visible, button:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
input:user-invalid, textarea:user-invalid { border-color: var(--error); }
button {
padding: 0.65rem 1.25rem;
border: 0;
border-radius: 6px;
background: var(--accent);
color: var(--accent-text);
font: inherit;
font-weight: 600;
cursor: pointer;
}
</style>
</head>
<body>
<h1>Contact us</h1>
<form action="https://silentforms.com/api/submit" method="POST">
<input type="hidden" name="accessKey" value="YOUR_ACCESS_KEY_HERE">
<div class="field">
<label for="name">Name</label>
<input id="name" name="name" type="text" autocomplete="name" required>
</div>
<div class="field">
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<span class="hint">We only use it to reply.</span>
</div>
<div class="field">
<label for="message">Message</label>
<textarea id="message" name="message" rows="6" required placeholder="How can we help?"></textarea>
</div>
<input type="checkbox" name="botcheck" style="display: none;" tabindex="-1" autocomplete="off">
<button type="submit">Send message</button>
</form>
</body>
</html>How it works#
Custom properties hold every colour. The rules for body, inputs and the button only ever use var(--…). Switching themes means changing nine values in one place instead of hunting through every selector.
prefers-color-scheme: dark matches when the visitor's operating system or browser is set to dark. Inside it, the same property names get dark values. Nothing else in the stylesheet changes.
color-scheme: light dark tells the browser the page supports both, so the parts you did not style follow the theme too: scrollbars, the autofill background, date pickers, the select drop-down and the default checkbox. Without it, a dark page can still show bright white native controls. The meta name="color-scheme" tag does the same before the stylesheet loads, which avoids a white flash on slow connections.
Placeholders get their own colour. Some browsers render ::placeholder with reduced opacity by default; setting opacity: 1 makes the colour you chose the one you get. Keep important instructions in the label or a hint, not the placeholder, since it disappears once someone types.
Focus stays visible. :focus-visible shows a clear ring for keyboard users in both themes. The dark theme uses a lighter blue for it, because the light-theme blue is hard to see on a near-black background.
Contrast#
Each pair above was chosen to pass WCAG AA: at least 4.5:1 for text against its background, and at least 3:1 for field borders and the focus ring against the page. Two things are easy to get wrong in dark mode:
- Muted text. Grey hint text that reads well on white often falls below 4.5:1 on a dark background. Lighten it rather than reusing the light value.
- The button. A saturated blue with white text works in light mode; in dark mode a lighter blue with dark text is usually more readable and stays above 4.5:1.
If you change the colours, check each pair with a contrast checker, such as the one in your browser's developer tools (the colour picker in Chrome and Firefox shows the contrast ratio).
Testing both themes#
You do not have to change your system setting to test. In Chrome or Edge, open developer tools, then the Rendering panel, and set "Emulate CSS media feature prefers-color-scheme" to dark. Firefox has a toggle for light and dark simulation in the Inspector's style panel. Submit once in each theme to make sure the success page is readable as well.
Using it inside an existing site#
If your site already has a theme toggle that sets a class or attribute on the html element, put the dark values under that selector too, for example :root[data-theme="dark"], so the form follows the site's choice and not only the system setting.
Next steps#
- The minimal version of this form, explained field by field: Basic contact form.
- A wider layout for desktop: Multi-column form.
- Show the result without leaving the page: AJAX form with JavaScript.