Parent : [[INDEX-WEB-UIUX]]
Web UI/UX - Custom Accessible Form Controls : radio/checkbox sans lib, sr-only input, styled span
Problème
Les <input type="radio"> et <input type="checkbox"> natifs sont moches et impossibles à styler cohéremment cross-browser. Importer une lib de composants (Radix, MUI) alourdit le bundle pour 2 composants simples.
Solution
Pattern "hidden native input + styled visual span" : l'input natif est caché (sr-only) mais reste fonctionnel (keyboard, screen reader, form submission). Le span visible affiche l'état.
// Radio custom
function Radio({ name, checked, onChange, label }: {
name: string; checked: boolean; onChange: () => void; label: string
}) {
return (
<label className="flex items-center gap-2.5 cursor-pointer group py-1">
<span
className="w-3.5 h-3.5 rounded-full border flex items-center justify-center shrink-0 transition-colors"
style={{ borderColor: checked ? 'var(--accent)' : 'var(--border-hover)', background: checked ? 'var(--accent)' : 'transparent' }}
>
{checked && <span className="w-1.5 h-1.5 rounded-full bg-white block" />}
</span>
<input type="radio" name={name} checked={checked} onChange={onChange} className="sr-only" />
<span className="text-xs transition-colors" style={{ color: checked ? 'var(--text)' : 'var(--text-2)' }}>{label}</span>
</label>
)
}
// Checkbox custom
function Checkbox({ checked, onChange, label }: {
checked: boolean; onChange: () => void; label: string
}) {
return (
<label className="flex items-center gap-2.5 cursor-pointer py-1">
<span
className="w-3.5 h-3.5 rounded-[3px] border flex items-center justify-center shrink-0 transition-colors"
style={{ borderColor: checked ? 'var(--accent)' : 'var(--border-hover)', background: checked ? 'var(--accent)' : 'transparent' }}
>
{checked && <span className="text-white text-[9px] font-bold leading-none">✓</span>}
</span>
<input type="checkbox" checked={checked} onChange={onChange} className="sr-only" />
<span className="text-xs transition-colors" style={{ color: checked ? 'var(--text)' : 'var(--text-2)' }}>{label}</span>
</label>
)
}
Règles d'accessibilité
className="sr-only"sur l'input natif : invisible visuellement mais accessible aux screen readers<label>englobe tout : clic sur le texte OU le span toggle le inputcursor-pointersur le label : indique la zone cliquable- Transitions sur border et color : feedback visuel immédiat
- Couleur checked = accent color : cohérence avec le design system
Pourquoi
- Zéro dépendance, zéro bundle supplémentaire
- 100% accessible (keyboard navigation, screen readers)
- Totalement stylable avec le design system
- Fonctionne dans n'importe quel formulaire HTML
Quand l'utiliser
- Filtres, formulaires, préférences, settings
- Quand une lib de composants est overkill
Quand NE PAS l'utiliser
- Formulaire complexe avec 50+ champs (utiliser une lib comme Radix)
- Besoin de composants avancés (switch toggle, slider) → Radix UI
Références
- ProjectAlpha/GlobalSearch.tsx L53-81