CSS3 & Responsive Design

Styling Forms & Inputs

30 min Lesson 47 of 60

Why Form Styling Is Challenging

Forms are among the most difficult elements to style consistently in CSS. Unlike most HTML elements that render based entirely on the browser's CSS engine, form controls like inputs, selects, checkboxes, radio buttons, and file inputs are rendered using operating system native widgets. This means that a checkbox on macOS looks different from a checkbox on Windows, and a select dropdown on Chrome looks different from one on Firefox -- even on the same operating system. The browser essentially delegates the rendering of these elements to the operating system's UI toolkit, which is why they historically resisted CSS styling.

This presents a real problem for web developers who want a consistent, branded experience across all platforms. A beautifully designed form mockup from a designer will look different on every browser and OS combination unless you take specific steps to override the native rendering. The good news is that modern CSS provides powerful tools to take full control of form styling, from the appearance property that strips away native styling to pseudo-elements and pseudo-classes that let you style every state and sub-component of form controls.

In this lesson, we will progressively build up from basic input styling to fully custom checkboxes, radio buttons, toggle switches, range sliders, and validated forms. By the end, you will have a complete toolkit for creating beautiful, accessible, and consistent forms across all browsers.

Resetting Native Styles with appearance: none

The first step in custom form styling is removing the native operating system appearance from form controls. The appearance property controls whether an element is rendered using platform-native styling or plain CSS styling. Setting appearance: none tells the browser to stop using the native widget rendering and instead treat the element as a regular box that you can style with CSS.

Resetting Form Control Appearance

/* Reset appearance on common form controls */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="tel"],
input[type="url"],
input[type="search"],
input[type="date"],
textarea,
select {
    appearance: none;
    -webkit-appearance: none; /* Safari fallback */
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 8px 12px;
    font-family: inherit;
    font-size: inherit;
    line-height: 1.5;
    color: inherit;
    background-color: #fff;
}

/* Reset checkboxes and radio buttons */
input[type="checkbox"],
input[type="radio"] {
    appearance: none;
    -webkit-appearance: none;
    width: 20px;
    height: 20px;
    border: 2px solid #ccc;
    background-color: #fff;
    cursor: pointer;
}

input[type="checkbox"] {
    border-radius: 4px;
}

input[type="radio"] {
    border-radius: 50%;
}
Note: The font-family: inherit and font-size: inherit declarations are important. By default, form controls do not inherit the font from their parent elements -- they use the operating system's default font. Adding inherit ensures that your form controls match the typography of the rest of your page.

Styling Text Inputs

Text inputs are the most common form elements and the easiest to style once you have reset the native appearance. The key to good text input styling lies in getting the padding, border, and focus states right. Let us build a comprehensive text input style that covers all the important details.

Comprehensive Text Input Styling

/* Base input styles */
.form-input {
    display: block;
    width: 100%;
    padding: 10px 14px;
    font-family: inherit;
    font-size: 1rem;
    line-height: 1.5;
    color: #1a1a2e;
    background-color: #fff;
    border: 2px solid #d1d5db;
    border-radius: 8px;
    outline: none;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
    box-sizing: border-box;
}

/* Hover state */
.form-input:hover {
    border-color: #9ca3af;
}

/* Focus state with a visible ring */
.form-input:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
}

/* Placeholder text styling */
.form-input::placeholder {
    color: #9ca3af;
    opacity: 1; /* Firefox sets opacity to less than 1 by default */
}

/* Input with an icon inside */
.input-with-icon {
    position: relative;
}

.input-with-icon .form-input {
    padding-left: 40px;
}

.input-with-icon .icon {
    position: absolute;
    left: 12px;
    top: 50%;
    transform: translateY(-50%);
    color: #9ca3af;
    pointer-events: none;
}

/* Small and large variants */
.form-input--sm {
    padding: 6px 10px;
    font-size: 0.875rem;
    border-radius: 6px;
}

.form-input--lg {
    padding: 14px 18px;
    font-size: 1.125rem;
    border-radius: 10px;
}

Focus Styling with :focus and :focus-visible

Focus styling is critical for accessibility. When a user tabs through a form using the keyboard, they need a clear visual indicator of which element is currently focused. However, you often do not want to show focus styles when the user clicks on an input with a mouse, because the mouse click itself makes it obvious which element is active. This is where :focus-visible comes in.

The :focus pseudo-class matches whenever an element has focus, regardless of how it received focus (keyboard, mouse, or programmatically). The :focus-visible pseudo-class matches only when the browser determines that focus should be visually indicated -- typically when the user is navigating with the keyboard. This allows you to show a prominent focus ring for keyboard users while keeping the interface clean for mouse users.

Focus vs Focus-Visible

/* Remove default outline for all focus */
.form-input:focus {
    outline: none;
}

/* Subtle style for all focus (mouse and keyboard) */
.form-input:focus {
    border-color: #3b82f6;
}

/* Prominent ring only for keyboard navigation */
.form-input:focus-visible {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4);
}

/* You can also use :focus:not(:focus-visible) for mouse-only focus */
.form-input:focus:not(:focus-visible) {
    box-shadow: none; /* No ring when clicked with mouse */
}

/* Custom button with clear keyboard focus indication */
.form-button:focus-visible {
    outline: 3px solid #3b82f6;
    outline-offset: 2px;
}
Warning: Never remove focus styles entirely without providing an alternative. Writing outline: none on all elements without replacing the focus indicator makes your form inaccessible to keyboard users. If you remove the default outline, always replace it with a custom focus style using border-color, box-shadow, or a custom outline.

Styling the Placeholder with ::placeholder

The ::placeholder pseudo-element lets you style the placeholder text inside input fields and textareas. Placeholder text serves as a hint to the user about what to enter, so it should be visually distinct from actual input text -- typically lighter in color and sometimes in italic.

Placeholder Styling

/* Basic placeholder styling */
.form-input::placeholder {
    color: #9ca3af;
    opacity: 1; /* Important for Firefox */
    font-style: italic;
}

/* Different placeholder style for search inputs */
.search-input::placeholder {
    color: #6b7280;
    font-style: normal;
    font-weight: 500;
}

/* Placeholder that disappears on focus */
.form-input:focus::placeholder {
    opacity: 0;
    transition: opacity 0.2s ease;
}

/* Animated placeholder that slides up on focus */
.floating-label-group {
    position: relative;
}

.floating-label-group .form-input::placeholder {
    color: transparent;
}

.floating-label-group .label {
    position: absolute;
    left: 14px;
    top: 50%;
    transform: translateY(-50%);
    color: #9ca3af;
    font-size: 1rem;
    pointer-events: none;
    transition: all 0.2s ease;
}

.floating-label-group .form-input:focus ~ .label,
.floating-label-group .form-input:not(:placeholder-shown) ~ .label {
    top: -8px;
    left: 10px;
    font-size: 0.75rem;
    color: #3b82f6;
    background-color: #fff;
    padding: 0 4px;
}
Tip: The :placeholder-shown pseudo-class matches an input element when its placeholder is visible (meaning the input is empty). Combined with the adjacent sibling combinator ~, this is the key to creating pure CSS floating labels without any JavaScript. When the user types something, :not(:placeholder-shown) triggers and you can move the label up.

Styling Textareas

Textareas share most styling properties with text inputs, but they have an important unique property: resize. By default, textareas are resizable by the user (they show a drag handle in the bottom-right corner). You can control this behavior with the resize property.

Textarea Styling and Resize Control

/* Base textarea styles */
.form-textarea {
    display: block;
    width: 100%;
    min-height: 120px;
    padding: 10px 14px;
    font-family: inherit;
    font-size: 1rem;
    line-height: 1.6;
    color: #1a1a2e;
    background-color: #fff;
    border: 2px solid #d1d5db;
    border-radius: 8px;
    outline: none;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
    box-sizing: border-box;
}

/* Resize options */
.form-textarea--vertical {
    resize: vertical;     /* Only resize vertically (most common) */
}

.form-textarea--horizontal {
    resize: horizontal;   /* Only resize horizontally (rare) */
}

.form-textarea--both {
    resize: both;         /* Resize in both directions (default) */
}

.form-textarea--none {
    resize: none;         /* Prevent resizing entirely */
}

/* Auto-growing textarea effect (requires min and max height) */
.form-textarea--auto {
    resize: none;
    min-height: 80px;
    max-height: 300px;
    overflow-y: auto;
}

/* Focus state */
.form-textarea:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
}
Note: If you set resize: vertical (which is the most common choice), also set a min-height so the user cannot shrink the textarea to an unusable size, and optionally a max-height to prevent it from growing beyond the available space.

Custom Select Dropdowns

Select elements are notoriously difficult to style because the dropdown portion (the options list) is rendered by the operating system and is largely immune to CSS. However, you can style the select trigger (the closed state that shows the selected value) by resetting its appearance and adding custom styles and a custom dropdown arrow.

Styling the Select Element

/* Custom select styling */
.form-select {
    display: block;
    width: 100%;
    padding: 10px 40px 10px 14px;
    font-family: inherit;
    font-size: 1rem;
    line-height: 1.5;
    color: #1a1a2e;
    background-color: #fff;
    border: 2px solid #d1d5db;
    border-radius: 8px;
    outline: none;
    appearance: none;
    -webkit-appearance: none;
    cursor: pointer;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;

    /* Custom dropdown arrow using background-image */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%236b7280' d='M1.41 0L6 4.58 10.59 0 12 1.41l-6 6-6-6z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 14px center;
    background-size: 12px 8px;
}

.form-select:hover {
    border-color: #9ca3af;
}

.form-select:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
}

/* Disabled select */
.form-select:disabled {
    background-color: #f3f4f6;
    color: #9ca3af;
    cursor: not-allowed;
}

/* Multiple select (shows as a list box) */
.form-select[multiple] {
    padding: 8px;
    height: auto;
    background-image: none;
}

.form-select[multiple] option {
    padding: 6px 10px;
    border-radius: 4px;
}

.form-select[multiple] option:checked {
    background-color: #3b82f6;
    color: white;
}
Tip: The custom arrow is created using an inline SVG in the background-image property. The SVG is URL-encoded so it can be embedded directly in the CSS without a separate file. This technique gives you complete control over the arrow's color, size, and shape while keeping everything in a single CSS file. You can also use a Unicode character like the chevron or a CSS triangle made with borders, but the SVG approach offers the most flexibility.

Custom Checkboxes and Radio Buttons

Checkboxes and radio buttons are perhaps the trickiest form elements to style because their native rendering is deeply embedded in the operating system. The modern approach is to hide the native control using appearance: none and then build the visual representation entirely with CSS, including the checked state, focus ring, and any animations.

Custom Checkbox

/* Custom checkbox */
.custom-checkbox {
    appearance: none;
    -webkit-appearance: none;
    width: 22px;
    height: 22px;
    border: 2px solid #d1d5db;
    border-radius: 5px;
    background-color: #fff;
    cursor: pointer;
    position: relative;
    transition: all 0.15s ease;
    flex-shrink: 0;
    vertical-align: middle;
}

.custom-checkbox:hover {
    border-color: #3b82f6;
    background-color: #eff6ff;
}

.custom-checkbox:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4);
}

/* Checked state */
.custom-checkbox:checked {
    background-color: #3b82f6;
    border-color: #3b82f6;
}

/* Checkmark using a pseudo-element */
.custom-checkbox:checked::after {
    content: "";
    position: absolute;
    left: 6px;
    top: 2px;
    width: 6px;
    height: 12px;
    border: solid white;
    border-width: 0 2.5px 2.5px 0;
    transform: rotate(45deg);
}

/* Indeterminate state (partially checked) */
.custom-checkbox:indeterminate {
    background-color: #3b82f6;
    border-color: #3b82f6;
}

.custom-checkbox:indeterminate::after {
    content: "";
    position: absolute;
    left: 4px;
    top: 8px;
    width: 10px;
    height: 2.5px;
    background-color: white;
    border-radius: 1px;
}

Custom Radio Button

/* Custom radio button */
.custom-radio {
    appearance: none;
    -webkit-appearance: none;
    width: 22px;
    height: 22px;
    border: 2px solid #d1d5db;
    border-radius: 50%;
    background-color: #fff;
    cursor: pointer;
    position: relative;
    transition: all 0.15s ease;
    flex-shrink: 0;
    vertical-align: middle;
}

.custom-radio:hover {
    border-color: #3b82f6;
    background-color: #eff6ff;
}

.custom-radio:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4);
}

/* Selected state -- inner filled circle */
.custom-radio:checked {
    border-color: #3b82f6;
    background-color: #fff;
}

.custom-radio:checked::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background-color: #3b82f6;
}

/* Checkbox and radio labels */
.form-check {
    display: flex;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    padding: 6px 0;
    font-size: 0.95rem;
    color: #374151;
}

.form-check:hover .custom-checkbox,
.form-check:hover .custom-radio {
    border-color: #3b82f6;
}

CSS Toggle Switches

Toggle switches are a popular UI pattern for binary on/off settings. They are not a native HTML form control, but you can build them using a styled checkbox. The trick is to visually transform a checkbox into a switch track with a sliding knob using CSS alone, while keeping the underlying checkbox for accessibility and form submission.

Pure CSS Toggle Switch

/* Toggle switch built from a checkbox */
.toggle-switch {
    appearance: none;
    -webkit-appearance: none;
    width: 48px;
    height: 26px;
    background-color: #d1d5db;
    border: none;
    border-radius: 13px;
    cursor: pointer;
    position: relative;
    transition: background-color 0.25s ease;
    flex-shrink: 0;
    vertical-align: middle;
}

/* The sliding knob */
.toggle-switch::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 20px;
    height: 20px;
    background-color: white;
    border-radius: 50%;
    transition: transform 0.25s ease;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}

/* Checked (ON) state */
.toggle-switch:checked {
    background-color: #3b82f6;
}

.toggle-switch:checked::after {
    transform: translateX(22px);
}

/* Focus ring */
.toggle-switch:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4);
}

/* Disabled state */
.toggle-switch:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Toggle with label */
.toggle-label {
    display: flex;
    align-items: center;
    gap: 12px;
    cursor: pointer;
    font-size: 0.95rem;
}

.toggle-label span {
    user-select: none;
}

Styling File Inputs

File inputs (<input type="file">) are historically the most stubborn form control to style. The traditional approach was to hide the file input and trigger it from a styled label. However, modern CSS now supports the ::file-selector-button pseudo-element, which lets you directly style the "Choose File" button portion of the file input.

Styling File Inputs

/* Modern approach: style the file selector button */
.form-file {
    display: block;
    width: 100%;
    padding: 8px;
    font-family: inherit;
    font-size: 0.95rem;
    color: #374151;
    background-color: #fff;
    border: 2px dashed #d1d5db;
    border-radius: 8px;
    cursor: pointer;
    transition: border-color 0.2s ease;
}

.form-file:hover {
    border-color: #3b82f6;
}

.form-file::file-selector-button {
    padding: 8px 16px;
    margin-right: 12px;
    font-family: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    color: white;
    background-color: #3b82f6;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    transition: background-color 0.2s ease;
}

.form-file::file-selector-button:hover {
    background-color: #2563eb;
}

/* Alternative: Custom file input using a label */
.file-input-custom {
    position: relative;
    display: inline-block;
}

.file-input-custom input[type="file"] {
    position: absolute;
    width: 0;
    height: 0;
    opacity: 0;
    overflow: hidden;
}

.file-input-custom .file-label {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 10px 20px;
    font-size: 0.95rem;
    font-weight: 600;
    color: #3b82f6;
    background-color: #eff6ff;
    border: 2px solid #3b82f6;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.2s ease;
}

.file-input-custom .file-label:hover {
    background-color: #3b82f6;
    color: white;
}

Styling Range Inputs

Range inputs (<input type="range">) create slider controls. They consist of a track (the horizontal bar) and a thumb (the draggable handle). To style them, you need to target browser-specific pseudo-elements for the track and thumb, because there is no unified standard pseudo-element yet. The main pseudo-elements are ::-webkit-slider-runnable-track and ::-webkit-slider-thumb for Chrome and Safari, and ::-moz-range-track and ::-moz-range-thumb for Firefox.

Custom Range Slider

/* Base range input reset */
.form-range {
    appearance: none;
    -webkit-appearance: none;
    width: 100%;
    height: 8px;
    background: transparent;
    cursor: pointer;
    outline: none;
}

/* Track (Chrome, Safari, Edge) */
.form-range::-webkit-slider-runnable-track {
    height: 8px;
    background: #e5e7eb;
    border-radius: 4px;
}

/* Track (Firefox) */
.form-range::-moz-range-track {
    height: 8px;
    background: #e5e7eb;
    border-radius: 4px;
    border: none;
}

/* Thumb (Chrome, Safari, Edge) */
.form-range::-webkit-slider-thumb {
    appearance: none;
    -webkit-appearance: none;
    width: 22px;
    height: 22px;
    background: #3b82f6;
    border-radius: 50%;
    border: 3px solid white;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
    margin-top: -7px; /* Center thumb on track */
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

/* Thumb (Firefox) */
.form-range::-moz-range-thumb {
    width: 22px;
    height: 22px;
    background: #3b82f6;
    border-radius: 50%;
    border: 3px solid white;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

/* Hover effect on thumb */
.form-range::-webkit-slider-thumb:hover {
    transform: scale(1.15);
    box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);
}

.form-range::-moz-range-thumb:hover {
    transform: scale(1.15);
    box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);
}

/* Focus ring */
.form-range:focus-visible::-webkit-slider-thumb {
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4);
}

.form-range:focus-visible::-moz-range-thumb {
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4);
}

/* Filled portion of the track (Firefox only natively supports this) */
.form-range::-moz-range-progress {
    height: 8px;
    background-color: #3b82f6;
    border-radius: 4px;
}
Note: Coloring the filled portion of a range track (the part before the thumb) is only natively supported in Firefox via ::-moz-range-progress. For Chrome and Safari, you need to use a CSS gradient on the track and update it dynamically with JavaScript, or use a CSS custom property approach. This is one of the remaining inconsistencies in cross-browser form styling.

Form Validation Pseudo-Classes

CSS provides a rich set of pseudo-classes for styling form elements based on their validation state. These pseudo-classes respond to HTML5 validation attributes like required, pattern, min, max, minlength, maxlength, and type. By using these pseudo-classes, you can provide immediate visual feedback to users without any JavaScript.

Validation Pseudo-Classes

/* :valid -- the input value satisfies all validation constraints */
.form-input:valid {
    border-color: #10b981;
}

/* :invalid -- the input value does NOT satisfy validation constraints */
.form-input:invalid {
    border-color: #ef4444;
}

/* :required -- the input has the required attribute */
.form-input:required {
    border-left: 3px solid #f59e0b;
}

/* :optional -- the input does NOT have the required attribute */
.form-input:optional {
    border-left: 3px solid #d1d5db;
}

/* :in-range -- number/date input value is within min/max range */
.form-input:in-range {
    border-color: #10b981;
}

/* :out-of-range -- number/date input value is outside min/max range */
.form-input:out-of-range {
    border-color: #ef4444;
    background-color: #fef2f2;
}

/* :placeholder-shown -- the placeholder is currently visible (input is empty) */
.form-input:placeholder-shown {
    border-color: #d1d5db; /* Neutral border when empty */
}

/* Combine to only show validation colors when user has typed something */
.form-input:not(:placeholder-shown):valid {
    border-color: #10b981;
}

.form-input:not(:placeholder-shown):invalid {
    border-color: #ef4444;
}

The pattern of combining :not(:placeholder-shown) with :valid and :invalid is particularly important. Without it, an empty required input would show the invalid (red) style as soon as the page loads, before the user has even interacted with the form. By checking that the placeholder is not shown (meaning the user has typed something), you only show validation feedback after the user has made an input attempt.

Visual Feedback with Icons

/* Validation icons using pseudo-elements on a wrapper */
.input-group {
    position: relative;
}

.input-group .form-input {
    padding-right: 40px;
}

/* Success icon */
.input-group .form-input:not(:placeholder-shown):valid ~ .validation-icon::after {
    content: "\2713"; /* Checkmark */
    position: absolute;
    right: 12px;
    top: 50%;
    transform: translateY(-50%);
    color: #10b981;
    font-size: 1.25rem;
    font-weight: bold;
}

/* Error icon */
.input-group .form-input:not(:placeholder-shown):invalid ~ .validation-icon::after {
    content: "\2717"; /* X mark */
    position: absolute;
    right: 12px;
    top: 50%;
    transform: translateY(-50%);
    color: #ef4444;
    font-size: 1.25rem;
    font-weight: bold;
}

/* Error message that shows below invalid inputs */
.error-message {
    display: none;
    font-size: 0.8rem;
    color: #ef4444;
    margin-top: 4px;
    padding-left: 2px;
}

.form-input:not(:placeholder-shown):invalid ~ .error-message {
    display: block;
}

Disabled and Read-Only States

Forms often contain inputs that are not currently editable, either because they are disabled (the user cannot interact with them at all) or read-only (the user can see and select the text but cannot modify it). CSS provides pseudo-classes for both states, and it is important to style them clearly so users understand which fields they can and cannot edit.

Disabled and Read-Only Styles

/* Disabled state -- grayed out, no interaction */
.form-input:disabled,
.form-select:disabled,
.form-textarea:disabled {
    background-color: #f3f4f6;
    color: #9ca3af;
    border-color: #e5e7eb;
    cursor: not-allowed;
    opacity: 0.7;
}

/* Read-only state -- visible but not editable */
.form-input:read-only {
    background-color: #f9fafb;
    color: #4b5563;
    border-color: #e5e7eb;
    cursor: default;
}

/* Remove focus ring on read-only since user cannot edit */
.form-input:read-only:focus {
    border-color: #e5e7eb;
    box-shadow: none;
}

/* Disabled checkbox and radio */
.custom-checkbox:disabled,
.custom-radio:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.custom-checkbox:disabled + label,
.custom-radio:disabled + label {
    color: #9ca3af;
    cursor: not-allowed;
}

Accessible Form Styling

Accessible form styling goes beyond just visual appearance. It involves ensuring that all users, including those using screen readers, keyboard navigation, and other assistive technologies, can effectively use your forms. Here are the key principles for accessible form styling and the CSS techniques that support them.

Accessibility Best Practices in CSS

/* 1. Ensure sufficient color contrast for all states */
.form-input {
    color: #1a1a2e;           /* Dark text on light background: 15:1 ratio */
    border-color: #6b7280;    /* 4.6:1 ratio against white */
}

.form-input::placeholder {
    color: #6b7280;           /* At least 4.5:1 contrast ratio */
}

/* 2. Never rely on color alone for validation -- add icons, borders, or text */
.form-input:not(:placeholder-shown):invalid {
    border-color: #ef4444;
    border-width: 2px;
    background-image: url("data:image/svg+xml,..."); /* Error icon */
    background-repeat: no-repeat;
    background-position: right 12px center;
    padding-right: 40px;
}

/* 3. Respect reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
    .form-input,
    .custom-checkbox,
    .custom-radio,
    .toggle-switch,
    .toggle-switch::after {
        transition: none;
    }
}

/* 4. High contrast mode support */
@media (forced-colors: active) {
    .custom-checkbox:checked,
    .custom-radio:checked {
        forced-color-adjust: none;
        background-color: Highlight;
        border-color: Highlight;
    }

    .form-input:focus {
        outline: 2px solid Highlight;
    }
}

/* 5. Large touch targets for mobile (at least 44x44px) */
@media (pointer: coarse) {
    .custom-checkbox,
    .custom-radio {
        min-width: 44px;
        min-height: 44px;
    }

    .form-check {
        min-height: 44px;
        padding: 10px 0;
    }

    .toggle-switch {
        min-width: 52px;
        min-height: 44px;
    }
}

/* 6. Visible labels -- never hide them, but you can visually hide if needed */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
Warning: The .sr-only class should only be used when a visible label is truly not possible (such as a search input with a visible magnifying glass icon). In almost all cases, form inputs should have a visible <label> element associated via the for attribute. Placeholders are not a replacement for labels -- they disappear when the user starts typing, leaving no context for what the field is for.

Complete Styled Form Example

Let us bring everything together into a complete, production-ready styled form. This example combines all the techniques we have covered: reset styles, custom text inputs, select dropdowns, checkboxes, radio buttons, a toggle switch, validation states, and accessibility features.

Complete Contact Form with Custom Styling

<style>
    .styled-form {
        max-width: 560px;
        margin: 0 auto;
        padding: 32px;
        background: #ffffff;
        border-radius: 16px;
        box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
        font-family: system-ui, -apple-system, sans-serif;
    }

    .styled-form h2 {
        margin: 0 0 24px 0;
        font-size: 1.5rem;
        color: #1a1a2e;
    }

    .form-group {
        margin-bottom: 20px;
    }

    .form-group label {
        display: block;
        margin-bottom: 6px;
        font-size: 0.875rem;
        font-weight: 600;
        color: #374151;
    }

    .form-group label .required {
        color: #ef4444;
        margin-left: 2px;
    }

    .form-group .hint {
        display: block;
        font-size: 0.8rem;
        color: #6b7280;
        margin-top: 4px;
    }

    /* Inputs and textarea */
    .styled-form .input,
    .styled-form select,
    .styled-form textarea {
        display: block;
        width: 100%;
        padding: 10px 14px;
        font-family: inherit;
        font-size: 1rem;
        line-height: 1.5;
        color: #1a1a2e;
        background-color: #fff;
        border: 2px solid #d1d5db;
        border-radius: 8px;
        outline: none;
        appearance: none;
        -webkit-appearance: none;
        box-sizing: border-box;
        transition: border-color 0.2s ease, box-shadow 0.2s ease;
    }

    .styled-form .input:focus,
    .styled-form select:focus,
    .styled-form textarea:focus {
        border-color: #3b82f6;
        box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
    }

    .styled-form .input:not(:placeholder-shown):valid {
        border-color: #10b981;
    }

    .styled-form .input:not(:placeholder-shown):invalid {
        border-color: #ef4444;
    }

    .styled-form textarea {
        resize: vertical;
        min-height: 120px;
    }

    .styled-form select {
        padding-right: 40px;
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath fill='%236b7280' d='M1.41 0L6 4.58 10.59 0 12 1.41l-6 6-6-6z'/%3E%3C/svg%3E");
        background-repeat: no-repeat;
        background-position: right 14px center;
        cursor: pointer;
    }

    /* Checkbox and radio groups */
    .check-group {
        display: flex;
        flex-direction: column;
        gap: 8px;
    }

    .check-item {
        display: flex;
        align-items: center;
        gap: 10px;
        cursor: pointer;
    }

    /* Submit button */
    .styled-form .btn-submit {
        display: block;
        width: 100%;
        padding: 12px 24px;
        margin-top: 28px;
        font-family: inherit;
        font-size: 1rem;
        font-weight: 700;
        color: white;
        background-color: #3b82f6;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        transition: background-color 0.2s ease, transform 0.1s ease;
    }

    .styled-form .btn-submit:hover {
        background-color: #2563eb;
    }

    .styled-form .btn-submit:active {
        transform: scale(0.98);
    }

    .styled-form .btn-submit:focus-visible {
        outline: 3px solid #3b82f6;
        outline-offset: 2px;
    }

    @media (prefers-reduced-motion: reduce) {
        .styled-form * {
            transition: none !important;
        }
    }
</style>

<form class="styled-form" novalidate>
    <h2>Contact Us</h2>

    <div class="form-group">
        <label for="name">Full Name <span class="required">*</span></label>
        <input type="text" id="name" class="input" placeholder="John Doe"
               required minlength="2">
    </div>

    <div class="form-group">
        <label for="email">Email Address <span class="required">*</span></label>
        <input type="email" id="email" class="input" placeholder="john@example.com"
               required>
    </div>

    <div class="form-group">
        <label for="subject">Subject</label>
        <select id="subject">
            <option value="">Select a subject...</option>
            <option value="general">General Inquiry</option>
            <option value="support">Support</option>
            <option value="feedback">Feedback</option>
        </select>
    </div>

    <div class="form-group">
        <label for="message">Message <span class="required">*</span></label>
        <textarea id="message" placeholder="Write your message..." required></textarea>
    </div>

    <div class="form-group">
        <label>Preferred Contact Method</label>
        <div class="check-group">
            <label class="check-item">
                <input type="radio" name="contact" value="email" class="custom-radio" checked>
                Email
            </label>
            <label class="check-item">
                <input type="radio" name="contact" value="phone" class="custom-radio">
                Phone
            </label>
        </div>
    </div>

    <div class="form-group">
        <label class="check-item">
            <input type="checkbox" class="custom-checkbox" required>
            I agree to the terms and conditions <span class="required">*</span>
        </label>
    </div>

    <div class="form-group">
        <label class="toggle-label">
            <input type="checkbox" class="toggle-switch">
            <span>Subscribe to newsletter</span>
        </label>
    </div>

    <button type="submit" class="btn-submit">Send Message</button>
</form>

Exercise 1: Custom Registration Form

Create a complete user registration form with the following fields: username (text input with a minimum length of 3), email (email input), password (password input with a minimum length of 8), confirm password, date of birth (date input), country (custom styled select dropdown with at least 5 countries), gender (custom styled radio buttons), interests (custom styled checkboxes with at least 4 options), profile bio (textarea with vertical-only resize), profile picture (custom styled file input), notification preference (toggle switch), and an age range slider (range input from 18 to 100). Style all inputs consistently using the techniques from this lesson. Add hover, focus, and focus-visible states to every interactive element. Implement CSS-only validation feedback using the :valid, :invalid, :not(:placeholder-shown), and :required pseudo-classes. Show validation icons (checkmark for valid, X for invalid) next to inputs. Include a floating label on the email field. Ensure all custom controls (checkboxes, radios, toggle, range) are fully keyboard accessible with visible focus indicators. Add prefers-reduced-motion support to disable all transitions for users who prefer reduced motion.

Exercise 2: Multi-Step Form with Progress

Build a multi-step form interface where each step is a fieldset that is shown or hidden (you can simulate this with CSS classes). Step 1 collects personal information (name, email, phone), Step 2 collects preferences (favorite color via radio buttons, hobbies via checkboxes, budget range via a range slider), and Step 3 is a review step with a terms checkbox and a toggle for marketing emails. Style a progress bar at the top that visually indicates the current step using CSS (such as three circles connected by a line, with completed steps highlighted). Style each fieldset with consistent spacing and grouping. Make the form fully responsive: on small screens, labels should stack above inputs, and on wider screens, labels can sit beside their inputs. Use the :disabled pseudo-class to style the "Previous" button as disabled on Step 1 and the "Next" button as disabled on Step 3. Style the final "Submit" button distinctly from the navigation buttons. Add validation styling to every required field and ensure the form meets accessibility standards with proper label associations, focus management, and color contrast.