Custom Inputs

What You'll Learn

  • Create fully custom checkboxes by hiding native inputs with opacity: 0
  • Build accessible custom radio buttons while maintaining keyboard navigation
  • Design toggle switches using CSS-only techniques
  • Use the :checked pseudo-class with sibling selectors for state management
  • Maintain accessibility by preserving native input functionality
  • Implement focus states for custom inputs with :focus and :focus-visible
  • Create disabled states for custom form controls

Introduction to Custom Input Styling

Default browser checkboxes and radio buttons are difficult to style and look different across platforms. Creating custom inputs allows you to design controls that match your brand while maintaining full accessibility and functionality. The key technique is hiding the native input while preserving its behavior.

Custom inputs must remain accessible to keyboard users and screen readers. Never use display: none on the native input, as it removes the element from the accessibility tree. Instead, use opacity: 0 to visually hide the input while keeping it functional for assistive technologies and keyboard navigation.

Custom Checkbox Pattern

The custom checkbox pattern involves three key elements: the native input (hidden but functional), a custom visual indicator, and proper label association. Use absolute positioning to overlay the custom design on the hidden input.

<label class="custom-checkbox"> Subscribe to newsletter <input type="checkbox"> <span class="checkmark"></span> </label> .custom-checkbox { display: block; position: relative; padding-left: 35px; cursor: pointer; user-select: none; } /* Hide native checkbox but keep it functional */ .custom-checkbox input { position: absolute; opacity: 0; cursor: pointer; height: 0; width: 0; } /* Create custom checkbox */ .checkmark { position: absolute; top: 0; left: 0; height: 24px; width: 24px; background-color: white; border: 2px solid #d1d5db; border-radius: 4px; transition: all 0.2s ease; } /* Hover state */ .custom-checkbox:hover input ~ .checkmark { border-color: #3b82f6; background-color: #eff6ff; } /* Checked state */ .custom-checkbox input:checked ~ .checkmark { background-color: #3b82f6; border-color: #3b82f6; } /* Create checkmark (hidden by default) */ .checkmark:after { content: ""; position: absolute; display: none; left: 7px; top: 3px; width: 6px; height: 11px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg); } /* Show checkmark when checked */ .custom-checkbox input:checked ~ .checkmark:after { display: block; }

Custom Radio Buttons

Custom radio buttons follow the same pattern as checkboxes but use circular shapes and different checked state indicators. Remember that radio buttons in the same name group are mutually exclusive.

<label class="custom-radio"> Small <input type="radio" name="size" checked> <span class="radiomark"></span> </label> <label class="custom-radio"> Medium <input type="radio" name="size"> <span class="radiomark"></span> </label> .custom-radio { display: block; position: relative; padding-left: 35px; cursor: pointer; user-select: none; } .custom-radio input { position: absolute; opacity: 0; cursor: pointer; } /* Create custom radio button */ .radiomark { position: absolute; top: 0; left: 0; height: 24px; width: 24px; background-color: white; border: 2px solid #d1d5db; border-radius: 50%; transition: all 0.2s ease; } .custom-radio:hover input ~ .radiomark { border-color: #10b981; background-color: #d1fae5; } .custom-radio input:checked ~ .radiomark { border-color: #10b981; } /* Create inner circle (hidden by default) */ .radiomark:after { content: ""; position: absolute; display: none; top: 4px; left: 4px; width: 12px; height: 12px; border-radius: 50%; background: #10b981; } /* Show inner circle when checked */ .custom-radio input:checked ~ .radiomark:after { display: block; }

Toggle Switches

Toggle switches are stylized checkboxes with a sliding animation. They're commonly used for on/off settings and boolean preferences. The visual design mimics physical toggle switches found on electronic devices.

<label class="toggle-switch"> <input type="checkbox"> <span class="slider"></span> </label> .toggle-switch { position: relative; width: 60px; height: 30px; display: inline-block; } .toggle-switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #d1d5db; transition: 0.3s; border-radius: 30px; } /* Create circular knob */ .slider:before { position: absolute; content: ""; height: 22px; width: 22px; left: 4px; bottom: 4px; background-color: white; transition: 0.3s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } /* Checked state */ .toggle-switch input:checked + .slider { background-color: #10b981; } /* Slide knob when checked */ .toggle-switch input:checked + .slider:before { transform: translateX(30px); } /* Focus state */ .toggle-switch input:focus + .slider { box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2); }

Focus States for Custom Inputs

Custom inputs must have clear focus indicators for keyboard navigation. Use the :focus pseudo-class on the hidden input combined with the sibling selector to style the custom visual element.

/* Focus state for custom checkbox */ .custom-checkbox input:focus ~ .checkmark { outline: 3px solid #f59e0b; outline-offset: 2px; } /* Focus-visible for keyboard-only focus */ .custom-checkbox input:focus:not(:focus-visible) ~ .checkmark { outline: none; } .custom-checkbox input:focus-visible ~ .checkmark { outline: 3px solid #f59e0b; outline-offset: 2px; }

Disabled States

Disabled custom inputs should be visually distinct and non-interactive. Style both the native input's disabled state and update the custom visual indicator accordingly.

<label class="custom-checkbox"> Disabled option <input type="checkbox" disabled> <span class="checkmark"></span> </label> .custom-checkbox input:disabled ~ .checkmark { background-color: #f3f4f6; border-color: #d1d5db; cursor: not-allowed; opacity: 0.6; } /* Change cursor for entire label when disabled */ .custom-checkbox:has(input:disabled) { cursor: not-allowed; opacity: 0.6; }

Practical Implementation Tips

When implementing custom inputs in production, consider these best practices for maintainability and user experience.

Label Association

Always wrap inputs in labels or use the for attribute to associate labels with inputs. This increases the clickable area and improves accessibility.

<!-- Method 1: Wrapping label --> <label class="custom-checkbox"> Option text <input type="checkbox"> <span class="checkmark"></span> </label> <!-- Method 2: Associated label --> <input type="checkbox" id="option1"> <label for="option1" class="custom-checkbox"> Option text <span class="checkmark"></span> </label>

Z-Index Considerations

The hidden native input should have a higher z-index than the custom visual element to ensure it receives clicks and focus. However, with opacity: 0, the input is naturally on top.

Color Contrast

Ensure your custom inputs meet WCAG color contrast requirements. The border and background should have at least 3:1 contrast ratio with surrounding content, and text should have 4.5:1 contrast.

Key Takeaways