Form styling should protect relationships. A user needs to know which label belongs to which control, what help text applies, and what went wrong after validation.
Start with markup that works before layout.
<label for="email">Email address</label>
<input id="email" name="email" type="email" aria-describedby="email-help" />
<p id="email-help">Use the address where you want replies.</p>
CSS should enhance that structure, not replace it.
Use a field wrapper
.field {
display: grid;
gap: 0.35rem;
max-inline-size: 34rem;
}
.field label {
font-weight: 700;
}
.field input,
.field textarea,
.field select {
min-block-size: 2.75rem;
border: 2px solid var(--field-border, currentColor);
border-radius: 6px;
padding-inline: 0.75rem;
}
The wrapper keeps the label, control, hint, and error in one layout context. This makes responsive behavior predictable.
Do not depend on placeholder text
Placeholders disappear when the user types, can have low contrast, and are not a substitute for labels. If a design wants a compact form, use visible labels that can wrap.
Error layout must be tested
Errors are usually longer than designers expect. They may include a correction, a rule, or a server response. Give errors normal text flow.
.field__error {
color: #a33a25;
font-weight: 700;
}
Avoid absolutely positioning error text unless the reserved space is explicit. Floating errors often overlap controls or push content unpredictably.
Form grids need fallback behavior
Two-column form grids can be useful, but the control and its label should stay together.
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: 1rem;
}
The field wrapper moves as a unit. That matters more than keeping columns perfectly filled.