CSSForm UI Patterns

Styling forms & UI components

Form elements (<input>, <button>, <select>, etc.) are notoriously difficult to style consistently across browsers. Modern CSS provides appearance: none to strip default styling, then pseudo-elements like ::before and ::after to create custom designs. Understanding form styling is essential for building accessible, beautiful user interfaces.

Removing default form styling

CSS
/* appearance: none removes default browser styling -->
input, button, select, textarea {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  /* Now you can style them like normal elements -->
}

/* Style inputs -->
input[type="text"],
input[type="email"],
input[type="password"],
textarea {
  appearance: none;
  width: 100%;
  padding: 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
  font-family: inherit;
  transition: all 0.3s ease;
}

input:focus,
textarea:focus {
  outline: none;
  border-color: #0066cc;
  box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}

/* Style buttons -->
button {
  appearance: none;
  padding: 10px 20px;
  background: #0066cc;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: all 0.3s ease;
}

button:hover {
  background: #0052a3;
}

button:active {
  transform: translateY(2px);
}

button:disabled {
  background: #cccccc;
  cursor: not-allowed;
  opacity: 0.6;
}
Custom checkboxes and radio buttons

CSS
<!-- HTML structure -->
<label class="checkbox">
  <input type="checkbox">
  <span>Accept terms</span>
</label>

/* Hide default checkbox -->
.checkbox input {
  appearance: none;
  width: 0;
  height: 0;
  opacity: 0;
}

/* Create custom checkbox with ::before -->
.checkbox {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  user-select: none;
}

.checkbox::before {
  content: '';
  display: inline-block;
  width: 20px;
  height: 20px;
  border: 2px solid #ddd;
  border-radius: 4px;
  transition: all 0.3s ease;
}

/* Style when checked -->
.checkbox input:checked + span::before,
.checkbox input:checked ~ ::before {
  background: #0066cc;
  border-color: #0066cc;
  box-shadow: inset 0 0 0 3px white;
}

/* Focus state -->
.checkbox input:focus + span::before,
.checkbox input:focus ~ ::before {
  box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2);
}

<!-- Custom radio button -->
.radio {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}

.radio::before {
  content: '';
  width: 20px;
  height: 20px;
  border: 2px solid #ddd;
  border-radius: 50%;
  transition: all 0.3s ease;
}

.radio input:checked ~ ::before {
  border-color: #0066cc;
  box-shadow: inset 0 0 0 6px #0066cc;
}
Custom select dropdowns

CSS
<!-- HTML -->
<div class="select-wrapper">
  <select>
    <option>Choose an option</option>
    <option>Option 1</option>
    <option>Option 2</option>
    <option>Option 3</option>
  </select>
</div>

/* Wrapper styling -->
.select-wrapper {
  position: relative;
  display: inline-block;
  width: 100%;
}

/* Remove default select styling -->
select {
  appearance: none;
  width: 100%;
  padding: 10px 15px;
  padding-right: 40px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
  background-color: white;
  cursor: pointer;
  transition: all 0.3s ease;
}

select:hover {
  border-color: #999;
}

select:focus {
  outline: none;
  border-color: #0066cc;
  box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}

/* Add custom arrow with ::after -->
.select-wrapper::after {
  content: '▼';
  position: absolute;
  right: 15px;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
  color: #666;
}

/* Disabled state -->
select:disabled {
  background-color: #f5f5f5;
  opacity: 0.6;
  cursor: not-allowed;
}
File input styling

CSS
<!-- HTML -->
<input type="file" class="file-input" id="file">
<label for="file" class="file-label">Choose file</label>

/* Hide default file input -->
.file-input {
  display: none;
}

/* Style label as button -->
.file-label {
  display: inline-block;
  padding: 10px 20px;
  background: #0066cc;
  color: white;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.file-label:hover {
  background: #0052a3;
}

/* JavaScript to show selected filename -->
const input = document.getElementById('file');
const label = document.querySelector('.file-label');

input.addEventListener('change', (e) => {
  if (e.target.files[0]) {
    label.textContent = e.target.files[0].name;
  }
});
Range input styling

CSS
<!-- HTML -->
<input type="range" min="0" max="100" value="50">

/* Basic range styling -->
input[type="range"] {
  width: 100%;
  height: 5px;
  border-radius: 5px;
  background: #ddd;
  outline: none;
  -webkit-appearance: none;
  appearance: none;
}

/* Track (the line) -->
input[type="range"]::-webkit-slider-runnable-track {
  width: 100%;
  height: 5px;
  background: linear-gradient(to right, #0066cc 0%, #0066cc 50%, #ddd 50%, #ddd 100%);
  border-radius: 5px;
}

/* Thumb (the circle you drag) -->
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #0066cc;
  cursor: pointer;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

/* Firefox track -->
input[type="range"]::-moz-range-track {
  background: #ddd;
  border-radius: 5px;
  height: 5px;
}

/* Firefox thumb -->
input[type="range"]::-moz-range-thumb {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #0066cc;
  cursor: pointer;
  border: none;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
Form validation styling

CSS
<!-- HTML with validation -->
<input
  type="email"
  required
  class="form-input"
>

/* Style for valid input -->
input:valid {
  border-color: #4caf50;
  box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
}

/* Style for invalid input -->
input:invalid {
  border-color: #f44336;
  box-shadow: 0 0 0 3px rgba(244, 67, 54, 0.1);
}

/* Only show on blur or after user interaction -->
input:invalid:not(:placeholder-shown) {
  border-color: #f44336;
}

/* Disabled state -->
input:disabled {
  background-color: #f5f5f5;
  cursor: not-allowed;
  opacity: 0.6;
}

/* Show validation message -->
.form-group {
  margin-bottom: 20px;
}

.error-message {
  display: none;
  color: #f44336;
  font-size: 12px;
  margin-top: 4px;
}

input:invalid ~ .error-message {
  display: block;
}
Accessible form labels

CSS
<!-- Good: label with for attribute -->
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>

/* Style labels -->
label {
  display: block;
  margin-bottom: 8px;
  font-weight: 500;
  color: #333;
}

/* Required field indicator -->
label[required]::after {
  content: ' *';
  color: #f44336;
}

/* Style label-input pairs -->
.form-group {
  display: flex;
  flex-direction: column;
  margin-bottom: 20px;
}

.form-group label {
  margin-bottom: 8px;
}

.form-group input,
.form-group textarea,
.form-group select {
  padding: 10px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

/* Inline labels for checkboxes -->
.checkbox-group label {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  margin-bottom: 10px;
}
Note
Good form styling requires understanding both CSS (appearance, pseudo-elements, states) and HTML semantics (labels, required, disabled). Always pair CSS with proper HTML for accessibility.
Be careful with custom styling
When customizing form elements, test across browsers carefully. Some properties like `::placeholder` styling vary widely. Always ensure custom styles don't break usability or accessibility.
Next
Advanced UI patterns and component design techniques.