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.