
Mentre implementavo un radio button nel mio sito web, mi sono reso conto che dovevo adattare i colori del componente di Shadcn/UI. Così ho deciso di dare un'occhiata a come era implementato e ho scoperto che, in realtà, tanto piccolo non era!
Come sapete, nel mio sito utilizzo Shadcn/UI per velocizzare la costruzione dei componenti UI.
Aprendo il codice del radio button, mi aspettavo di trovare il classico <input type="radio" />. Invece, mi sono trovato davanti a un componente decisamente più complesso, che utilizza Radix UI.
Questo è il codice utilizzato da Shadcn/UI per implementare il radio button:
"use client";
import * as React from "react";
import { cn } from "cn";
import { CircleIcon } from "lucide-react";
import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
);
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="absolute top-1/2 left-1/2 size-4 -translate-x-1/2 -translate-y-1/2 fill-white" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
}
export { RadioGroup, RadioGroupItem };Amettiamolo, un bel po' di codice per gestire un radio group. Senza contare l'utilizzo di una libreria di icone come lucide-react per disegnare un semplice cerchio, e le oltre 30 classi Tailwind CSS necessarie per definire l'aspetto del componente!
Ma la cosa che più mi ha lasciato senza parole è stata un’altra.
Se provate ad ispezionare l'HTML generato, non troverete il classico <input type="radio" />, quello che mi aspettavo fin dall'inizio. Troverete l'icona SVG utilizzato per visualizzare il cerchio interno e un elemento button con diversi attributi ARIA.
<div role="radiogroup" data-slot="radio-group" tabindex="0">
<div class="flex items-center gap-3">
<button
type="button"
role="radio"
aria-checked="true"
data-state="checked"
value="gintonic"
data-slot="radio-group-item"
id="rs1"
tabindex="0"
>
<span data-state="checked" data-slot="radio-group-indicator">
<!-- SVG dell'icona -->
</span>
</button>
<label for="rs1">Gin Tonic</label>
</div>
</div>Notiamo che abbiamo un button a cui viene assegnato il ruolo radio attraverso l'attributo role. Il suo comportamento è gestito da Radix UI, compresa l'interazione da tastiera.
Gli attributi ARIA permettono di comunicare semantica, stati e proprietà alle tecnologie assistive, ma non trasformano un elemento HTML in un altro né gli conferiscono automaticamente il comportamento nativo di quest'ultimo.
Infatti, se andiamo a rivedere le regole di utilizzo di ARIA, ci dicono proprio questo:
When developing HTML, it is important to use native HTML elements where ever possible. Do not use an ARIA role or property if it is possible to use an HTML element that has equivalent semantics.
(W3C – First Rule of ARIA Use)
In altre parole: se esiste già un elemento HTML nativo che offre la semantica e il comportamento di cui hai bisogno, usalo invece di ricostruirlo con altri elementi e attributi ARIA.
Eppure, applicare uno stile personalizzato a un radio button non è così complicato.
In questo esempio, a sinistra abbiamo un semplice <input type="radio" /> con qualche regola CSS, mentre a destra troviamo il componente di Shadcn/UI.
A livello visivo possiamo ottenere risultati praticamente identici, ma la differenza nel codice e nella quantità di comportamento da gestire è notevole.
Basta applicare qualche regola CSS:
appearance: none per rimuovere l'aspetto predefinito del browser, mantenendo l'elemento HTML nativo.::before per disegnare il cerchio.:checked per mostrare il cerchio quando il radio button è selezionato.::focus-visible per evidenziare il controllo quando riceve il focus da tastiera.Ecco un esempio concreto.
<div className="flex items-center gap-3">
<input id="r1" type="radio" name="drink" value="gintonic" />
<label htmlFor="r1">Gin Tonic</label>
</div>input[type="radio"] {
appearance: none;
box-sizing: border-box;
width: 1rem;
height: 1rem;
margin: 0;
border: 1px solid currentColor;
border-radius: 50%;
display: inline-grid;
place-content: center;
}
input[type="radio"]::before {
content: "";
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: currentColor;
transform: scale(0);
transition: transform 120ms ease-in-out;
}
input[type="radio"]:checked::before {
transform: scale(1);
}
input[type="radio"]:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
}Naturalmente, questo è soltanto un esempio minimale: in un progetto reale andrebbero gestiti anche gli stati disabilitati, i colori del Design System e gli eventuali requisiti di contrasto. Ma il punto rimane lo stesso: possiamo personalizzare l'aspetto di un controllo senza doverne riscrivere il comportamento.
Sicuramente è più semplice che gestire oltre 30 classi Tailwind CSS e delegare parte del lavoro a una libreria aggiuntiva.
È anche vero, però, che chi sceglie Shadcn/UI potrebbe non essere particolarmente esperto di CSS. Magari vuole semplicemente completare una funzionalità, e la cosa più importante è che il componente funzioni senza dover perdere tempo a implementare tutto da zero.
Alla fine, utilizziamo queste librerie proprio per risparmiare tempo ed essere più produttivi. È comprensibile, quindi, affidarsi a soluzioni già pronte e dare per scontato che siano la scelta migliore per ogni situazione.
Ma non sempre è così.
Personalmente, sono stato abituato a ragionare in questo modo fin dai miei studi: mantenere le cose semplici e chiare, utilizzare gli elementi HTML che abbiamo già a disposizione e non reinventare la ruota. E, se proprio vogliamo reinventarla, almeno facciamolo bene.
Non voglio farne un dramma, né sostenere che librerie come Shadcn/UI o Radix UI siano inutili. In molti casi offrono un grande valore, soprattutto quando dobbiamo gestire componenti complessi, stati articolati e requisiti di accessibilità che richiedono attenzione.
Il problema nasce quando iniziamo ad aggiungere livelli di astrazione anche dove non sono realmente necessari.
La somma di tante piccole decisioni di questo tipo può portare a una maggiore complessità, a più codice da mantenere e a nuovi potenziali errori.
Sviluppare per il web sta diventando sempre più complesso. Realizzare funzionalità che si comportino correttamente su un'ampia gamma di dispositivi e browser richiede già parecchio lavoro. Per questo, quando possiamo delegare al browser comportamenti che sa gestire nativamente, dovremmo almeno chiederci se abbia senso farlo.
Pensateci: perché dovremmo ricostruire un radio button con React, JavaScript e una serie di astrazioni, quando il browser sa già come gestirlo?
Un input HTML nativo non ha bisogno di aspettare che JavaScript venga caricato ed eseguito per offrire il proprio comportamento di base. Un componente personalizzato che dipende da JavaScript, invece, deve poter contare sulla corretta inizializzazione del codice per gestire l'interazione.
Magari, la prossima volta che scegliamo una libreria o aggiungiamo un componente al nostro Design System, potremmo partire da una domanda molto semplice:
Abbiamo davvero bisogno di ricostruire qualcosa che la piattaforma ci offre già?