Step-by-step
-
1
Reset Built-In Browser Appearance
appearance: nonestrips the platform-native widget styling (the gradient on iOS Safari inputs, the bevelled edge on Firefox select boxes).border-radius: 0prevents iOS from rounding corners you did not ask for. Set these first, then build the style you actually want on top.cssinput, select, textarea { appearance: none; -webkit-appearance: none; border-radius: 0; /* iOS Safari override */ box-sizing: border-box; font-family: inherit; /* browsers do NOT inherit this by default */ font-size: 1rem; } -
2
Define a Consistent Base Style
After the reset, apply the same foundation to all text-like inputs. A 1px border, comfortable padding, and explicit line-height create a reliable baseline across Chrome, Firefox, Safari, and Edge.
cssinput[type="text"], input[type="email"], input[type="password"], input[type="tel"], input[type="url"], input[type="search"], input[type="number"], textarea, select { width: 100%; padding: 0.625rem 0.875rem; line-height: 1.5; border: 1px solid #d1d5db; border-radius: 0.375rem; background: #fff; color: #111827; transition: border-color 0.15s ease, box-shadow 0.15s ease; } -
3
Style :focus-visible — Not :focus
:focus-visibleonly activates when the browser determines that a visible focus indicator is needed (keyboard navigation, not a mouse click). This gives you precise focus rings for keyboard users without the ring appearing on every mouse click — which is why so many designs incorrectly useoutline: none. Never remove the outline entirely.cssinput:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; border-color: #2563eb; } /* Remove the default (non-visible) focus ring from browsers that support :focus-visible */ input:focus:not(:focus-visible), textarea:focus:not(:focus-visible), select:focus:not(:focus-visible) { outline: none; } -
4
Mark Invalid Fields Without Highlighting Empty Ones
CSS
:invalidmatches an empty required input immediately on page load — before the user has touched anything. Combining it with:not(:placeholder-shown)limits the error state to fields the user has interacted with and left invalid.css/* Only show error state after the user has typed something */ input:invalid:not(:placeholder-shown), textarea:invalid:not(:placeholder-shown) { border-color: #dc2626; background: #fef2f2; } /* Optional: paired error icon or message */ input:invalid:not(:placeholder-shown) + .field-error { display: block; } .field-error { display: none; font-size: 0.8125rem; color: #dc2626; margin-top: 0.25rem; } -
5
Style Checkboxes and Radios with accent-color
The modern way to style checkboxes and radios without replacing them with custom elements is
accent-color. One line — the browser does the rest, adapting the checked state, indeterminate state, and disabled state automatically. For anything beyond a colour change, you still need custom elements.css/* Global tint for all native checkboxes and radios */ :root { accent-color: #2563eb; } /* Or scope to specific elements */ input[type="checkbox"], input[type="radio"] { accent-color: #2563eb; width: 1rem; height: 1rem; cursor: pointer; } -
6
Style the Select Element
After
appearance: none, the native dropdown arrow disappears. Add a custom SVG arrow viabackground-image. This is far simpler than a full custom select and covers 95% of design needs. For a fully custom dropdown, you need a JavaScript library or the new<selectlist>element (experimental).cssselect { appearance: none; padding-right: 2.5rem; /* room for the arrow */ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%236b7280' d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.75rem center; background-size: 1rem; cursor: pointer; } -
7
Wrap File Inputs for Custom Styling
The file input button cannot be styled directly. The standard pattern is to visually hide the real input and show a styled label that triggers it. The label's
forattribute links it to the input — clicking the label opens the file picker.html<label class="file-upload"> <input type="file" class="sr-only" id="avatar" accept="image/*"> <span class="file-upload__btn">Choose file</span> <span class="file-upload__name" id="file-name">No file chosen</span> </label> -
8
Support Dark Mode with CSS Variables
Hard-coded hex values break in dark mode. Use CSS custom properties for every colour in your form styles, then override the variables inside
@media (prefers-color-scheme: dark). All inputs update automatically.css:root { --input-bg: #ffffff; --input-border: #d1d5db; --input-text: #111827; --input-focus-ring: #2563eb; --input-error-border: #dc2626; --input-error-bg: #fef2f2; } @media (prefers-color-scheme: dark) { :root { --input-bg: #1f2937; --input-border: #374151; --input-text: #f9fafb; --input-focus-ring: #60a5fa; --input-error-border: #f87171; --input-error-bg: #450a0a; } } input, select, textarea { background: var(--input-bg); border-color: var(--input-border); color: var(--input-text); }
Tips & gotchas
- Always set <code>font-family: inherit</code> and <code>font-size: 1rem</code> on form elements — browsers explicitly exclude them from font inheritance, which is almost never what you want.
- The <code>placeholder</code> attribute should describe the expected format (e.g. "mm/dd/yyyy"), not repeat the label. Label text should be in a <code><label></code> element, always — never rely on placeholder as a substitute.
- For disabled inputs, add <code>cursor: not-allowed; opacity: 0.5</code> — the visual affordance helps users understand the field is intentionally inactive.
- Input height should be at least 44px for touch targets — the Apple HIG minimum. Use <code>min-height: 2.75rem</code> rather than a fixed height so tall content (e.g. multiline) can still grow.
Wrapping up
Consistent form styling comes down to four steps: reset appearance, set a shared base, handle focus and validation states correctly, and use CSS variables for dark mode. The modern additions — :focus-visible, accent-color, and :invalid:not(:placeholder-shown) — eliminate entire categories of hacks that used to require JavaScript. Apply them and you get forms that look professional, work across browsers, and stay accessible without extra effort.