<< Home

Chapter 16: Styling Forms and UI Elements

Forms are where design meets interaction. When a reader types into a field or taps a button, the smallest visual details affect confidence and speed. In this chapter you will learn how to shape inputs that feel clear, readable and well spaced. You will also learn to communicate state with focus and validation, craft accessible custom controls and assemble a polished contact page that works across devices.

Visually clear input fields

Good form fields are easy to scan and understand at a glance. Labels need a clear relationship to their inputs; spacing must guide the eye from one step to the next; touch targets should be comfortable. Begin with consistent font sizes, generous line height and a layout that does not rely on color alone to explain meaning.

A sensible base for <input> and <label>

This base style sets a readable type scale, spacing and clear borders. It also uses system fonts to keep rendering crisp on every platform.

/* Base form styles */
form { max-width: 42rem; margin: 0 auto; padding: 1rem; }

label { display: block; font-weight: 600; margin-bottom: 0.25rem; }

input[type="text"],
input[type="email"],
input[type="tel"],
input[type="password"],
textarea,
select {
  width: 100%;
  padding: 0.6rem 0.75rem;
  border: 1px solid #c9ccd1;
  border-radius: 0.5rem;
  font: 1rem/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
  background-color: #fff;
  color: #1a1f2a;
}

textarea { min-height: 7rem; resize: vertical; }

.form-row { margin-bottom: 1rem; }
💡 Keep labels visible. Placeholder text is not a label; it disappears when the user types and can create uncertainty.

Helpful placeholder and hint text

Placeholders should be suggestions, not instructions. If you need to guide the user, add a short hint under the field. Keep the wording brief and specific.

.hint { font-size: 0.9rem; color: #5a6473; margin-top: 0.35rem; }

States for focus and validation

Every interactive control should make its state obvious. Focus shows where the keyboard will act next; validation shows whether the value is acceptable. Use shape, contrast and a small amount of motion to communicate state without confusion.

Accessible focus

Use a strong outline that contrasts with the background. :focus-visible reduces visual noise for pointer users while retaining a clear indicator for keyboard users.

input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 3px solid #3b82f6;
  outline-offset: 2px;
  border-color: #3b82f6;
  transition: outline-offset 120ms ease, border-color 120ms ease;
}
⚠️ Do not remove focus outlines. People who navigate with a keyboard or assistive tech depend on them to know where they are.

Clear validation

Native HTML validation handles many cases. Use it to color borders, then provide concise messages that explain how to fix issues. Avoid relying on color alone; include an icon or short text where possible.

input:required:invalid { border-color: #e11d48; }
input:required:valid { border-color: #22c55e; }

.error { color: #b91c1c; font-size: 0.9rem; margin-top: 0.35rem; }
.success { color: #166534; font-size: 0.9rem; margin-top: 0.35rem; }

Custom checkboxes and toggles

Custom controls can look refined and still remain accessible. Keep the native input in the document for semantics, then style a visual replacement using an adjacent element. Ensure labels toggle the control and that focus styles are still visible.

A styled checkbox that keeps native semantics

This pattern hides the default box and draws a custom box using ::before. The input remains focusable and screen readers still understand it.

.check {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
}

.check input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.check .box {
  width: 1.1rem;
  height: 1.1rem;
  border: 2px solid #475569;
  border-radius: 0.25rem;
  display: inline-block;
  position: relative;
}

.check input[type="checkbox"]:focus-visible + .box { outline: 3px solid #3b82f6; outline-offset: 2px; }

.check input[type="checkbox"]:checked + .box::before {
  content: "";
  position: absolute;
  inset: 0.1rem;
  background: #16a34a;
  border-radius: 0.2rem;
}

A smooth on off toggle

A toggle switch can be built from a checkbox and a rounded track with a sliding knob. Keep the motion brief; fast changes feel responsive and do not distract.

.toggle { display: inline-flex; align-items: center; gap: 0.5rem; cursor: pointer; }

.toggle input { position: absolute; opacity: 0; width: 1px; height: 1px; }

.toggle .track {
  width: 2.5rem;
  height: 1.4rem;
  background: #cbd5e1;
  border-radius: 1rem;
  position: relative;
  transition: background 160ms ease-in-out;
}

.toggle .knob {
  position: absolute;
  top: 0.15rem;
  left: 0.15rem;
  width: 1.1rem;
  height: 1.1rem;
  background: #fff;
  border-radius: 50%;
  box-shadow: 0 1px 2px rgba(0,0,0,.2);
  transition: left 160ms ease-in-out;
}

.toggle input:checked + .track { background: #22c55e; }
.toggle input:checked + .track .knob { left: 1.25rem; }

.toggle input:focus-visible + .track { outline: 3px solid #3b82f6; outline-offset: 2px; }
💡 Always wrap the input and its visual parts in a single <label>. This expands the hit area; taps on text toggle the control which feels natural on touch screens.

A polished contact page

Now bring the pieces together. This layout groups fields, shows clear focus and validation, and uses a button style that looks clickable without relying on strong color alone. It is compact on phones and comfortable on larger screens.

<form action="/contact" method="post" novalidate>
  <div class="form-row">
    <label for="name">Your name</label>
    <input id="name" name="name" type="text" autocomplete="name" required placeholder="Ada Lovelace">
    <p class="hint">Please use your full name.</p>
    <p class="error" aria-live="polite" hidden>Enter your name.</p>
  </div>

  <div class="form-row">
    <label for="email">Email address</label>
    <input id="email" name="email" type="email" autocomplete="email" required placeholder="name@example.com">
    <p class="error" aria-live="polite" hidden>Enter a valid email address.</p>
  </div>

  <div class="form-row">
    <label for="topic">Topic</label>
    <select id="topic" name="topic" required>
      <option value="">Select a topic…</option>
      <option>Speaking request</option>
      <option>Project inquiry</option>
      <option>General feedback</option>
    </select>
  </div>

  <div class="form-row">
    <label class="check">
      <input id="updates" name="updates" type="checkbox">
      <span class="box"></span>
      Receive occasional updates
    </label>
  </div>

  <div class="form-row">
    <label for="message">Message</label>
    <textarea id="message" name="message" required placeholder="How can we help…"></textarea>
    <p class="error" aria-live="polite" hidden>Please enter a message.</p>
  </div>

  <div class="form-row">
    <label class="toggle">
      <input id="urgent" name="urgent" type="checkbox">
      <span class="track"><span class="knob"></span></span>
      Mark as urgent
    </label>
  </div>

  <div class="form-row">
    <button class="btn" type="submit">Send message</button>
  </div>
</form>
/* Button that feels clickable without shouting */
.btn {
  appearance: none;
  display: inline-block;
  padding: 0.7rem 1.1rem;
  border-radius: 0.6rem;
  border: 1px solid #0f172a;
  background: linear-gradient(#0b63f6, #0952c6);
  color: #fff;
  font-weight: 600;
  letter-spacing: 0.25px;
  cursor: pointer;
  transition: transform 80ms ease, filter 120ms ease;
}
.btn:hover { filter: brightness(1.05); }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 3px solid #3b82f6; outline-offset: 3px; }

For client side validation you can toggle message visibility with a small script that checks field validity after each input event. Keep logic focused and avoid blocking submission for small formatting differences when the message is still understandable.

<script>document.querySelectorAll("form [required]").forEach(function(el){
  el.addEventListener("input", function(){
    var error = el.closest(".form-row").querySelector(".error");
    if (!error) return;
    var invalid = el.value.trim() === "" || (el.type === "email" && !el.checkValidity());
    error.hidden = !invalid;
  });
});</script>

Well designed forms reduce friction. Clear labels, visible focus, concise validation and controls that feel tactile will help readers complete tasks with confidence and ease.